All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.babyfish.jimmer.sql.runtime.DatabaseValidators Maven / Gradle / Ivy

There is a newer version: 0.9.19
Show newest version
package org.babyfish.jimmer.sql.runtime;

import org.babyfish.jimmer.lang.Ref;
import org.babyfish.jimmer.meta.ImmutableProp;
import org.babyfish.jimmer.meta.ImmutableType;
import org.babyfish.jimmer.meta.TargetLevel;
import org.babyfish.jimmer.sql.DatabaseValidationIgnore;
import org.babyfish.jimmer.sql.association.meta.AssociationType;
import org.babyfish.jimmer.sql.ast.tuple.Tuple2;
import org.babyfish.jimmer.sql.ast.tuple.Tuple3;
import org.babyfish.jimmer.sql.meta.*;
import org.babyfish.jimmer.sql.meta.impl.DatabaseIdentifiers;
import org.jetbrains.annotations.Nullable;

import java.sql.*;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;

public class DatabaseValidators {

    private final EntityManager entityManager;

    private final String microServiceName;

    private final boolean defaultDissociationActionCheckable;

    private final MetadataStrategy strategy;

    private final String catalog;

    private final String schema;

    private final Connection con;

    private final List items;

    private final Map> tableRefMap = new HashMap<>();

    private final Map> middleTableRefMap = new HashMap<>();

    @Nullable
    public static DatabaseValidationException validate(
            EntityManager entityManager,
            String microServiceName,
            boolean defaultDissociationActionCheckable,
            MetadataStrategy strategy,
            String catalog,
            String schema,
            Connection con
    ) throws SQLException {
        return new DatabaseValidators(
                entityManager,
                microServiceName,
                defaultDissociationActionCheckable,
                strategy,
                catalog,
                schema,
                con
        ).validate();
    }

    private DatabaseValidators(
            EntityManager entityManager,
            String microServiceName,
            boolean defaultDissociationActionCheckable,
            MetadataStrategy strategy,
            String catalog,
            String schema,
            Connection con
    ) {
        this.entityManager = entityManager;
        this.microServiceName = microServiceName;
        this.defaultDissociationActionCheckable = defaultDissociationActionCheckable;
        this.strategy = strategy;
        this.catalog = catalog != null && !catalog.isEmpty() ? catalog : null;
        this.schema = schema != null && !schema.isEmpty() ? schema : null;
        this.con = con;
        this.items = new ArrayList<>();
    }

    private DatabaseValidationException validate() throws SQLException {
        for (ImmutableType type : entityManager.getAllTypes(microServiceName)) {
            if (type.isEntity() && !(type instanceof AssociationType) && !type.getJavaClass().isAnnotationPresent(DatabaseValidationIgnore.class)) {
                validateSelf(type);
            }
        }
        for (ImmutableType type : entityManager.getAllTypes(microServiceName)) {
            if (type.isEntity() && !(type instanceof AssociationType) && !type.getJavaClass().isAnnotationPresent(DatabaseValidationIgnore.class)) {
                validateForeignKey(type);
            }
        }
        if (!items.isEmpty()) {
            return new DatabaseValidationException(items);
        }
        return null;
    }

    private void validateSelf(ImmutableType type) throws SQLException {
        Table table = tableOf(type);
        if (table == null) {
            return;
        }
        if (!(type instanceof AssociationType) && type.getIdProp().getAnnotation(DatabaseValidationIgnore.class) != null) {
            ColumnDefinition idColumnDefinition = type.getIdProp().getStorage(strategy);
            Set idColumnNames = new LinkedHashSet<>((idColumnDefinition.size() * 4 + 2) / 3);
            for (int i = 0; i < idColumnDefinition.size(); i++) {
                idColumnNames.add(
                        DatabaseIdentifiers.comparableIdentifier(idColumnDefinition.name(i))
                );
            }
            if (!idColumnNames.equals(table.primaryKeyColumns)) {
                items.add(
                        new DatabaseValidationException.Item(
                                type,
                                null,
                                "Expected primary key columns are " +
                                        type.getIdProp().getStorage(strategy).toColumnNames() +
                                        ", but actual primary key columns are " +
                                        table.primaryKeyColumns
                        )
                );
            }
        }
        for (ImmutableProp prop : type.getProps().values()) {
            if (prop.getAnnotation(DatabaseValidationIgnore.class) != null) {
                continue;
            }
            Storage storage = prop.getStorage(strategy);
            if (storage instanceof ColumnDefinition) {
                ColumnDefinition columnDefinition = (ColumnDefinition)storage;
                for (int i = 0; i < columnDefinition.size(); i++) {
                    Column column = table.columnMap.get(
                            DatabaseIdentifiers.comparableIdentifier(columnDefinition.name(i))
                    );
                    if (column == null) {
                        items.add(
                                new DatabaseValidationException.Item(
                                        type,
                                        prop,
                                        "There is no column \"" +
                                                columnDefinition.name(i) +
                                                "\" in table \"" +
                                                table +
                                                "\""
                                )
                        );
                    }
                }
            }
            if (storage instanceof SingleColumn) {
                Column column = table.columnMap.get(
                        DatabaseIdentifiers.comparableIdentifier(((SingleColumn)storage).getName())
                );
                if (column != null && (!prop.isAssociation(TargetLevel.ENTITY) || prop.isTargetForeignKeyReal(strategy))) {
                    boolean nullable = prop.isNullable() && !prop.isInputNotNull();
                    if (nullable != column.nullable) {
                        items.add(
                                new DatabaseValidationException.Item(
                                        type,
                                        prop,
                                        "The property is " +
                                                (nullable ? "nullable" : "nonnull(include inputNotNull)") +
                                                ", but the database column \"" +
                                                ((SingleColumn) storage).getName() +
                                                "\" in table \"" +
                                                table +
                                                "\" is " +
                                                (column.nullable ? "nullable" : "nonnull")
                                )
                        );
                    }
                }
            }
        }
    }

    private void validateForeignKey(ImmutableType type) throws SQLException {
        Table table = tableOf(type);
        if (table == null) {
            return;
        }
        for (ImmutableProp prop : type.getProps().values()) {
            if (!prop.isAssociation(TargetLevel.PERSISTENT) ||
                    prop.getAnnotation(DatabaseValidationIgnore.class) != null ||
                    prop.getTargetType().getJavaClass().isAnnotationPresent(DatabaseValidationIgnore.class)) {
                continue;
            }
            ForeignKeyContext ctx = new ForeignKeyContext(this, type, prop);
            Storage storage = prop.getStorage(strategy);
            if (storage instanceof MiddleTable) {
                Table middleTable = middleTableOf(prop);
                if (middleTable != null) {
                    MiddleTable middleTableMeta = (MiddleTable) storage;
                    if (middleTableMeta.getColumnDefinition().isForeignKey()) {
                        ForeignKey thisForeignKey = middleTable.getForeignKey(
                                ctx,
                                middleTableMeta.getColumnDefinition(),
                                defaultDissociationActionCheckable
                        );
                        if (thisForeignKey != null) {
                            thisForeignKey.assertReferencedColumns(ctx, type);
                        }
                    }
                    if (middleTableMeta.getTargetColumnDefinition().isForeignKey()) {
                        ForeignKey targetForeignKey = middleTable.getForeignKey(
                                ctx,
                                middleTableMeta.getTargetColumnDefinition(),
                                defaultDissociationActionCheckable
                        );
                        if (targetForeignKey != null) {
                            targetForeignKey.assertReferencedColumns(ctx, prop.getTargetType());
                        }
                    }
                }
            } else if (storage != null && prop.isReference(TargetLevel.PERSISTENT)) {
                ColumnDefinition columnDefinition = prop.getStorage(strategy);
                if (columnDefinition.isForeignKey()) {
                    ForeignKey foreignKey = table.getForeignKey(
                            ctx,
                            columnDefinition,
                            defaultDissociationActionCheckable
                    );
                    if (foreignKey != null) {
                        foreignKey.assertReferencedColumns(ctx, prop.getTargetType());
                    }
                }
            }
        }
    }

    private Table tableOf(ImmutableType type) throws SQLException {
        org.babyfish.jimmer.lang.Ref tableRef = tableRefMap.get(type);
        if (tableRef == null) {
            Set
tables = tablesOf(DatabaseIdentifiers.rawIdentifier(type.getTableName(strategy))); if (tables.isEmpty()) { items.add( new DatabaseValidationException.Item( type, null, "There is no table \"" + DatabaseIdentifiers.rawIdentifier(type.getTableName(strategy)) + "\"" ) ); tableRef = Ref.empty(); } else if (tables.size() > 1) { items.add( new DatabaseValidationException.Item( type, null, "Too many matched tables: " + tables ) ); tableRef = Ref.empty(); } else { Table table = tables.iterator().next(); table = new Table(table, columnsOf(table), primaryKeyColumns(table)); tableRef = Ref.of(table); } tableRefMap.put(type, tableRef); } return tableRef.getValue(); } private Table middleTableOf(ImmutableProp prop) throws SQLException { Ref
tableRef = middleTableRefMap.get(prop); if (tableRef == null) { Storage storage = prop.getStorage(strategy); if (storage instanceof MiddleTable) { MiddleTable middleTable = (MiddleTable) storage; Set
tables = tablesOf(middleTable.getTableName()); if (tables.isEmpty()) { items.add( new DatabaseValidationException.Item( prop.getDeclaringType(), prop, "There is no table \"" + middleTable.getTableName() + "\"" ) ); tableRef = Ref.empty(); } else if (tables.size() > 1) { items.add( new DatabaseValidationException.Item( prop.getDeclaringType(), prop, "Too many matched tables: " + tables ) ); tableRef = Ref.empty(); } else { Table table = tables.iterator().next(); table = new Table( table, columnsOf(table), primaryKeyColumns(table) ); tableRef = Ref.of(table); } } else { tableRef = Ref.empty(); } middleTableRefMap.put(prop, tableRef); } return tableRef.getValue(); } private Set
tablesOf(String table) throws SQLException { String catalogName = null; String schemaName = null; String tableName = table; int index = tableName.lastIndexOf('.'); if (index != -1) { catalogName = tableName.substring(0, index); tableName = tableName.substring(index + 1); index = catalogName.lastIndexOf('.'); if (index != -1) { schemaName = catalogName.substring(index + 1); catalogName = catalogName.substring(0, index); } } return tablesOf(optional(catalogName), optional(schemaName), tableName); } private static String optional(String value) { if ("".equals(value) || "null".equals(value)) { return null; } return value; } private Set
tablesOf(String catalogName, String schemaName, String tableName) throws SQLException { Set> tuples = new LinkedHashSet<>(); new TableNameCollector( new String[] { catalogName, schemaName, tableName }, arr -> tuples.add(new Tuple3<>(arr[0], arr[1], arr[2])) ).emit(); for (Tuple3 tuple : tuples) { Set
tables = tablesOf0(tuple.get_1(), tuple.get_2(), tuple.get_3()); if (!tables.isEmpty()) { return tables; } } return Collections.emptySet(); } private Set
tablesOf0(String catalogName, String schemaName, String tableName) throws SQLException { Set
tables = new LinkedHashSet<>(); try (ResultSet rs = con.getMetaData().getTables( catalogName, schemaName, tableName, null )) { while (rs.next()) { tables.add( new Table( rs.getString("TABLE_CAT"), rs.getString("TABLE_SCHEM"), rs.getString("TABLE_NAME") ) ); } } return tables .stream() .filter(it -> it.catalog == null || catalog == null || it.catalog.equalsIgnoreCase(catalog)) .filter(it -> it.schema == null || schema == null || it.schema.equalsIgnoreCase(schema)) .filter(it -> it.name.equalsIgnoreCase(tableName)) .collect(Collectors.toSet()); } private Map columnsOf(Table table) throws SQLException { Map columnMap = new HashMap<>(); try (ResultSet rs = con.getMetaData().getColumns( table.catalog, table.schema, table.name, null )) { while (rs.next()) { Column column = new Column( table, rs.getString("COLUMN_NAME").toUpperCase(), rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable ); columnMap.put(column.name.toUpperCase(), column); } } return columnMap; } private Set primaryKeyColumns(Table table) throws SQLException { Set columnNames = new HashSet<>(); try (ResultSet rs = con.getMetaData().getPrimaryKeys( table.catalog, table.schema, table.name )) { while (rs.next()) { columnNames.add( rs.getString("COLUMN_NAME").toUpperCase() ); } } return columnNames; } private Map, ForeignKey> foreignKeys(Table table) throws SQLException { Map, Map> map = new HashMap<>(); try (ResultSet rs = con.getMetaData().getImportedKeys( table.catalog, table.schema, table.name )) { while (rs.next()) { String constraintName = rs.getString("FK_NAME").toUpperCase(); Table referencedTable = tablesOf( upper(rs.getString("PKTABLE_CAT")), upper(rs.getString("PKTABLE_SCHEM")), rs.getString("PKTABLE_NAME").toUpperCase() ).iterator().next(); String columnName = upper(rs.getString("FKCOLUMN_NAME")); String referencedColumnName = upper(rs.getString("PKCOLUMN_NAME")); map.computeIfAbsent( new Tuple2<>(constraintName, referencedTable), it -> new LinkedHashMap<>() ).put(columnName, referencedColumnName); } } if (map.isEmpty()) { return Collections.emptyMap(); } Map, ForeignKey> foreignKeyMap = new HashMap<>(); for (Map.Entry, Map> e : map.entrySet()) { String constraintName = e.getKey().get_1(); Table referencedTable = e.getKey().get_2(); Map subMap = e.getValue(); Set columnNames = subMap.keySet(); Collection referencedColumnNames = subMap.values(); ForeignKey foreignKey = new ForeignKey( constraintName, columnNames, referencedTable, new LinkedHashSet<>(referencedColumnNames) ); foreignKeyMap.put( columnNames, foreignKey ); } return foreignKeyMap; } private static String upper(String text) { return text == null ? null : text.toUpperCase(); } private static class Table { // keep the same case final String catalog; // keep the same case final String schema; // keep the same case final String name; final Map columnMap; final Set primaryKeyColumns; private Map, ForeignKey> _foreignKeyMap; Table(String catalog, String schema, String name) { this.catalog = catalog; this.schema = schema; this.name = name; this.columnMap = Collections.emptyMap(); this.primaryKeyColumns = Collections.emptySet(); } public Table( Table base, Map columnMap, Set primaryKeyColumns ) { this.catalog = base.catalog; this.schema = base.schema; this.name = base.name; this.columnMap = columnMap; this.primaryKeyColumns = primaryKeyColumns; } public ForeignKey getForeignKey( ForeignKeyContext ctx, ColumnDefinition columnDefinition, boolean defaultDissociationActionCheckable ) throws SQLException{ ForeignKey foreignKey; if (columnDefinition instanceof MultipleJoinColumns) { MultipleJoinColumns multipleJoinColumns = (MultipleJoinColumns) columnDefinition; Set columnNames = new LinkedHashSet<>(); for (int i = 0; i < multipleJoinColumns.size(); i++) { columnNames.add(multipleJoinColumns.name(i).toUpperCase()); } foreignKey = getForeignKeyMap(ctx).get(columnNames); } else { foreignKey = getForeignKeyMap(ctx).get( Collections.singleton( ((SingleColumn) columnDefinition).getName().toUpperCase() ) ); } if (columnDefinition.isForeignKey() && foreignKey == null) { ctx.databaseValidators.items.add( new DatabaseValidationException.Item( ctx.type, ctx.prop, "No foreign key constraint for columns: " + columnDefinition.toColumnNames() + ". If this column(s) is(are) a real foreign key, " + "please add foreign key constraint in database" + "; If this column is a fake foreign key, " + "please use `@JoinColumn(foreignKey = false, ...)`" ) ); } if (!defaultDissociationActionCheckable && !columnDefinition.isForeignKey() && foreignKey != null) { ctx.databaseValidators.items.add( new DatabaseValidationException.Item( ctx.type, ctx.prop, "Unnecessary foreign key constraint for columns: " + columnDefinition.toColumnNames() + ". If this column(s) is(are) a fake foreign key, " + "please remove foreign key constraint in database" + "; If this column is a real foreign key, " + "please use `@JoinColumn(foreignKey = true, ...)`" ) ); } return foreignKey; } @Override public int hashCode() { return Objects.hash(catalog, schema, name); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Table table = (Table) o; return catalog.equals(table.catalog) && schema.equals(table.schema) && name.equals(table.name); } @Override public String toString() { return catalog + '.' + schema + '.' + name; } private Map, ForeignKey> getForeignKeyMap(ForeignKeyContext ctx) throws SQLException { Map, ForeignKey> map = _foreignKeyMap; if (map == null) { map = ctx.databaseValidators.foreignKeys(this); _foreignKeyMap = map; } return map; } } private static class Column { final Table table; // Always capitalized final String name; final boolean nullable; private Column(Table table, String name, boolean nullable) { this.table = table; this.name = name.toUpperCase(); this.nullable = nullable; } } private static class ForeignKey { // Always capitalized final String constraintName; // Always capitalized final Set columnNames; final Table referencedTable; // Always capitalized final Set referenceColumNames; ForeignKey( String constraintName, Set columnNames, Table referencedTable, Set referenceColumNames ) { this.constraintName = constraintName; this.columnNames = columnNames; this.referencedTable = referencedTable; this.referenceColumNames = referenceColumNames; } void assertReferencedColumns( ForeignKeyContext ctx, ImmutableType referencedType ) { if (!referencedType .getIdProp() .getStorage(ctx.databaseValidators.strategy) .toColumnNames() .equals(referenceColumNames)) { ctx.databaseValidators.items.add( new DatabaseValidationException.Item( ctx.type, ctx.prop, "Illegal foreign key \"" + constraintName + "\", expected referenced columns are " + referenceColumNames + ", but actual referenced columns are " + referencedType .getIdProp() .getStorage(ctx.databaseValidators.strategy) .toColumnNames() ) ); } } } private static class ForeignKeyContext { final DatabaseValidators databaseValidators; final ImmutableType type; final ImmutableProp prop; private ForeignKeyContext(DatabaseValidators databaseValidators, ImmutableType type, ImmutableProp prop) { this.databaseValidators = databaseValidators; this.type = type; this.prop = prop; } } private class TableNameCollector { private final String[] originalNames; private final String[] currentNames; private final Consumer emitter; private TableNameCollector(String[] originalNames, Consumer emitter) { this.originalNames = originalNames; this.currentNames = new String[originalNames.length]; this.emitter = emitter; } public void emit() { emit(0); } private void emit(int depth) { String text = originalNames[depth]; currentNames[depth] = text; if (depth + 1 < originalNames.length) { emit(depth + 1); } else { emitter.accept(currentNames); } if (text != null && !text.isEmpty()) { currentNames[depth] = text.toUpperCase(); if (depth + 1 < originalNames.length) { emit(depth + 1); } else { emitter.accept(currentNames); } currentNames[depth] = text.toLowerCase(); if (depth + 1 < originalNames.length) { emit(depth + 1); } else { emitter.accept(currentNames); } } } } }