is.codion.framework.domain.db.SchemaDomain Maven / Gradle / Ivy
/*
* This file is part of Codion.
*
* Codion is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Codion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Codion. If not, see .
*
* Copyright (c) 2020 - 2024, Björn Darri Sigurðsson.
*/
package is.codion.framework.domain.db;
import is.codion.framework.domain.DomainModel;
import is.codion.framework.domain.entity.EntityDefinition;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.KeyGenerator;
import is.codion.framework.domain.entity.attribute.AttributeDefinition;
import is.codion.framework.domain.entity.attribute.Column;
import is.codion.framework.domain.entity.attribute.ColumnDefinition;
import is.codion.framework.domain.entity.attribute.ForeignKey;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static is.codion.common.Text.nullOrEmpty;
import static is.codion.framework.domain.DomainType.domainType;
import static is.codion.framework.domain.entity.attribute.ForeignKey.reference;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
/**
* For instances use the available factory methods.
* @see #schemaDomain(DatabaseMetaData, String)
* @see #schemaDomain(DatabaseMetaData, String, SchemaSettings)
*/
public final class SchemaDomain extends DomainModel {
private static final int MAXIMUM_COLUMN_SIZE = 2_147_483_647;
private final Map tableEntityTypes = new HashMap<>();
private final SchemaSettings settings;
private SchemaDomain(DatabaseMetaData metaData, String schemaName, SchemaSettings settings) throws SQLException {
super(domainType(schemaName));
this.settings = settings;
validateForeignKeys(false);
new MetaDataModel(metaData, schemaName)
.schema().tables().values().forEach(this::defineEntity);
}
/**
* Factory method for creating a new {@link SchemaDomain} instance.
* @param metaData the database metadata
* @param schemaName the schema name
* @return a new {@link SchemaDomain} instance
*/
public static SchemaDomain schemaDomain(DatabaseMetaData metaData, String schemaName) {
return schemaDomain(metaData, schemaName, SchemaSettings.builder().build());
}
/**
* Factory method for creating a new {@link SchemaDomain} instance.
* @param metaData the database metadata
* @param schemaName the schema name
* @param settings the configuration
* @return a new {@link SchemaDomain} instance
*/
public static SchemaDomain schemaDomain(DatabaseMetaData metaData, String schemaName, SchemaSettings settings) {
try {
return new SchemaDomain(requireNonNull(metaData), requireNonNull(schemaName), requireNonNull(settings));
}
catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* @param entityType the entity type
* @return the table type for the given entity type, table or view
*/
public String tableType(EntityType entityType) {
return tableEntityTypes.entrySet().stream()
.filter(entry -> entry.getValue().equals(entityType))
.findFirst()
.map(Map.Entry::getKey)
.map(MetaDataTable::tableType)
.orElseThrow(IllegalArgumentException::new);
}
private void defineEntity(MetaDataTable table) {
if (!tableEntityTypes.containsKey(table)) {
EntityType entityType = type().entityType(table.schema().name() + "." + table.tableName());
tableEntityTypes.put(table, entityType);
table.foreignKeys().stream()
.map(MetaDataForeignKeyConstraint::referencedTable)
.filter(referencedTable -> !referencedTable.equals(table))
.forEach(this::defineEntity);
defineEntity(table, entityType);
}
}
private void defineEntity(MetaDataTable table, EntityType entityType) {
List> attributeDefinitionBuilders = defineAttributes(table, entityType, new ArrayList<>(table.foreignKeys()));
if (!attributeDefinitionBuilders.isEmpty()) {
EntityDefinition.Builder entityDefinitionBuilder = entityType.define(attributeDefinitionBuilders.toArray(new AttributeDefinition.Builder[0]));
entityDefinitionBuilder.caption(caption(table.tableName()));
if (tableHasAutoIncrementPrimaryKeyColumn(table)) {
entityDefinitionBuilder.keyGenerator(KeyGenerator.identity());
}
if (!nullOrEmpty(table.comment())) {
entityDefinitionBuilder.description(table.comment());
}
entityDefinitionBuilder.readOnly("view".equalsIgnoreCase(table.tableType()));
add(entityDefinitionBuilder.build());
}
}
private List> defineAttributes(MetaDataTable table, EntityType entityType,
Collection foreignKeyConstraints) {
List> builders = new ArrayList<>();
table.columns().forEach(column -> {
builders.add(columnDefinitionBuilder(column, entityType));
if (column.foreignKeyColumn()) {
foreignKeyConstraints.stream()
//if this is the last column in the foreign key
.filter(foreignKeyConstraint -> lastKeyColumn(foreignKeyConstraint, column))
.findFirst()
.ifPresent(foreignKeyConstraint -> {
//we add the foreign key just below it
foreignKeyConstraints.remove(foreignKeyConstraint);
builders.add(foreignKeyDefinitionBuilder(foreignKeyConstraint, entityType));
});
}
});
return builders;
}
private AttributeDefinition.Builder, ?> foreignKeyDefinitionBuilder(MetaDataForeignKeyConstraint foreignKeyConstraint, EntityType entityType) {
MetaDataTable referencedTable = foreignKeyConstraint.referencedTable();
EntityType referencedEntityType = tableEntityTypes.get(referencedTable);
ForeignKey foreignKey = entityType.foreignKey(createForeignKeyName(foreignKeyConstraint) + "_FK",
foreignKeyConstraint.references().entrySet().stream()
.map(entry -> reference(column(entityType, entry.getKey()), column(referencedEntityType, entry.getValue())))
.collect(toList()));
return foreignKey.define().foreignKey().caption(caption(referencedTable.tableName().toLowerCase()));
}
private ColumnDefinition.Builder, ?> columnDefinitionBuilder(MetaDataColumn metadataColumn, EntityType entityType) {
String caption = caption(metadataColumn.columnName());
Column> column = column(entityType, metadataColumn);
ColumnDefinition.Builder, ?> builder;
if (metadataColumn.primaryKeyColumn()) {
builder = column.define().primaryKey(metadataColumn.primaryKeyIndex() - 1);
}
else if (isAuditColumn(column)) {
builder = auditColumnDefinitionBuilder(column)
.caption(caption);
}
else {
builder = column.define().column().caption(caption);
}
if (!metadataColumn.primaryKeyColumn() && metadataColumn.nullable() == DatabaseMetaData.columnNoNulls) {
builder.nullable(false);
}
if (column.type().isString() && metadataColumn.columnSize() > 0 && metadataColumn.columnSize() < MAXIMUM_COLUMN_SIZE) {
builder.maximumLength(metadataColumn.columnSize());
}
if (column.type().isDecimal() && metadataColumn.decimalDigits() >= 1) {
builder.maximumFractionDigits(metadataColumn.decimalDigits());
}
if (!metadataColumn.primaryKeyColumn() && metadataColumn.defaultValue() != null) {
builder.columnHasDefaultValue(true);
}
if (!nullOrEmpty(metadataColumn.comment())) {
builder.description(metadataColumn.comment());
}
return builder;
}
private boolean isAuditColumn(Column> column) {
return isAuditInsertUserColumn(column)
|| isAuditInsertTimeColumn(column)
|| isAuditUpdateUserColumn(column)
|| isAuditUpdateTimeColumn(column);
}
private ColumnDefinition.Builder, ?> auditColumnDefinitionBuilder(Column> column) {
if (isAuditInsertUserColumn(column)) {
return column.define().auditColumn().insertUser();
}
if (isAuditInsertTimeColumn(column)) {
return column.define().auditColumn().insertTime();
}
if (isAuditUpdateUserColumn(column)) {
return column.define().auditColumn().updateUser();
}
if (isAuditUpdateTimeColumn(column)) {
return column.define().auditColumn().updateTime();
}
throw new IllegalArgumentException("Unknown audit column type: " + column);
}
private boolean isAuditUpdateTimeColumn(Column> column) {
return column.name().equalsIgnoreCase(settings.auditUpdateTimeColumnName().orElse(null));
}
private boolean isAuditUpdateUserColumn(Column> column) {
return column.name().equalsIgnoreCase(settings.auditUpdateUserColumnName().orElse(null));
}
private boolean isAuditInsertTimeColumn(Column> column) {
return column.name().equalsIgnoreCase(settings.auditInsertTimeColumnName().orElse(null));
}
private boolean isAuditInsertUserColumn(Column> column) {
return column.name().equalsIgnoreCase(settings.auditInsertUserColumnName().orElse(null));
}
private static Column column(EntityType entityType, MetaDataColumn column) {
return (Column) entityType.column(column.columnName(), column.columnClass());
}
private static String caption(String name) {
String caption = name.toLowerCase().replace("_", " ");
return caption.substring(0, 1).toUpperCase() + caption.substring(1);
}
private static boolean lastKeyColumn(MetaDataForeignKeyConstraint foreignKeyConstraint, MetaDataColumn column) {
return foreignKeyConstraint.references().keySet().stream()
.mapToInt(MetaDataColumn::position)
.max()
.orElse(-1) == column.position();
}
private String createForeignKeyName(MetaDataForeignKeyConstraint foreignKeyConstraint) {
return foreignKeyConstraint.references().keySet().stream()
.map(MetaDataColumn::columnName)
.map(String::toUpperCase)
.map(this::removePrimaryKeyColumnSuffix)
.collect(joining("_"));
}
private String removePrimaryKeyColumnSuffix(String columnName) {
return settings.primaryKeyColumnSuffix()
.map(suffix -> removeSuffix(columnName, suffix))
.orElse(columnName);
}
private static String removeSuffix(String columnName, String suffix) {
return columnName.toLowerCase().endsWith(suffix.toLowerCase()) ?
columnName.substring(0, columnName.length() - suffix.length()) : columnName;
}
private static boolean tableHasAutoIncrementPrimaryKeyColumn(MetaDataTable table) {
return table.columns().stream()
.filter(MetaDataColumn::primaryKeyColumn)
.anyMatch(MetaDataColumn::autoIncrement);
}
/**
* Specifies the settings used when deriving a domain model from a database schema.
* @see #builder()
*/
public interface SchemaSettings {
Optional primaryKeyColumnSuffix();
Optional auditInsertUserColumnName();
Optional auditInsertTimeColumnName();
Optional auditUpdateUserColumnName();
Optional auditUpdateTimeColumnName();
/**
* @return a new builder
*/
static Builder builder() {
return new DefaultSchemaSettings.DefaultBuilder();
}
/**
* Builds a {@link SchemaSettings} instance.
*/
interface Builder {
Builder primaryKeyColumnSuffix(String primaryKeyColumnSuffix);
Builder auditInsertUserColumnName(String auditInsertUserColumnName);
Builder auditInsertTimeColumnName(String auditInsertTimeColumnName);
Builder auditUpdateUserColumnName(String auditUpdateUserColumnName);
Builder auditUpdateTimeColumnName(String auditUpdateTimeColumnName);
SchemaSettings build();
}
}
private static final class DefaultSchemaSettings implements SchemaSettings {
private final String primaryKeyColumnSuffix;
private final String auditInsertUserColumnName;
private final String auditInsertTimeColumnName;
private final String auditUpdateUserColumnName;
private final String auditUpdateTimeColumnName;
private DefaultSchemaSettings(DefaultBuilder builder) {
this.primaryKeyColumnSuffix = builder.primaryKeyColumnSuffix;
this.auditInsertUserColumnName = builder.auditInsertUserColumnName;
this.auditInsertTimeColumnName = builder.auditInsertTimeColumnName;
this.auditUpdateUserColumnName = builder.auditUpdateUserColumnName;
this.auditUpdateTimeColumnName = builder.auditUpdateTimeColumnName;
}
@Override
public Optional primaryKeyColumnSuffix() {
return Optional.ofNullable(primaryKeyColumnSuffix);
}
@Override
public Optional auditInsertUserColumnName() {
return Optional.ofNullable(auditInsertUserColumnName);
}
@Override
public Optional auditInsertTimeColumnName() {
return Optional.ofNullable(auditInsertTimeColumnName);
}
@Override
public Optional auditUpdateUserColumnName() {
return Optional.ofNullable(auditUpdateUserColumnName);
}
@Override
public Optional auditUpdateTimeColumnName() {
return Optional.ofNullable(auditUpdateTimeColumnName);
}
private static final class DefaultBuilder implements Builder {
private String primaryKeyColumnSuffix;
private String auditInsertUserColumnName;
private String auditInsertTimeColumnName;
private String auditUpdateUserColumnName;
private String auditUpdateTimeColumnName;
@Override
public Builder primaryKeyColumnSuffix(String primaryKeyColumnSuffix) {
this.primaryKeyColumnSuffix = requireNonNull(primaryKeyColumnSuffix);
return this;
}
@Override
public Builder auditInsertUserColumnName(String auditInsertUserColumnName) {
this.auditInsertUserColumnName = requireNonNull(auditInsertUserColumnName);
return this;
}
@Override
public Builder auditInsertTimeColumnName(String auditInsertTimeColumnName) {
this.auditInsertTimeColumnName = requireNonNull(auditInsertTimeColumnName);
return this;
}
@Override
public Builder auditUpdateUserColumnName(String auditUpdateUserColumnName) {
this.auditUpdateUserColumnName = requireNonNull(auditUpdateUserColumnName);
return this;
}
@Override
public Builder auditUpdateTimeColumnName(String auditUpdateTimeColumnName) {
this.auditUpdateTimeColumnName = requireNonNull(auditUpdateTimeColumnName);
return this;
}
@Override
public SchemaSettings build() {
return new DefaultSchemaSettings(this);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy