From 4903561755369aae8c3d701e6affe2035f054502 Mon Sep 17 00:00:00 2001 From: seed-master Date: Sat, 2 Sep 2023 22:54:52 +0200 Subject: [PATCH] dbobject validation updates 4 --- .../org/seed/core/config/SchemaManager.java | 74 +++++++++++++-- .../org/seed/core/data/dbobject/DBObject.java | 3 +- .../core/data/dbobject/DBObjectMetadata.java | 7 +- .../core/data/dbobject/DBObjectService.java | 5 +- .../data/dbobject/DBObjectServiceImpl.java | 89 ++++++++++++++----- .../core/data/dbobject/DBObjectValidator.java | 20 +++-- .../org/seed/core/entity/EntityValidator.java | 17 +++- .../resources/metainfo/zk-label.properties | 3 + .../resources/metainfo/zk-label_de.properties | 3 + 9 files changed, 170 insertions(+), 51 deletions(-) diff --git a/src/main/java/org/seed/core/config/SchemaManager.java b/src/main/java/org/seed/core/config/SchemaManager.java index 5ec9c022b..a1d90066a 100644 --- a/src/main/java/org/seed/core/config/SchemaManager.java +++ b/src/main/java/org/seed/core/config/SchemaManager.java @@ -19,18 +19,22 @@ import java.io.IOException; import java.sql.Connection; -import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; import java.util.SortedSet; +import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.persistence.Table; import javax.sql.DataSource; import org.hibernate.Session; +import org.hibernate.jdbc.ReturningWork; +import org.seed.C; import org.seed.core.config.changelog.ChangeLog; import org.seed.core.util.Assert; import org.seed.core.util.MiscUtils; @@ -97,7 +101,7 @@ private void init() { public synchronized DatabaseInfo getDatabaseInfo() { if (databaseInfo == null) { try (Connection connection = dataSource.getConnection()) { - final DatabaseMetaData dbMeta = connection.getMetaData(); + final var dbMeta = connection.getMetaData(); databaseInfo = new DatabaseInfo(dbMeta.getDatabaseProductName(), dbMeta.getDatabaseProductVersion()); } @@ -108,6 +112,32 @@ public synchronized DatabaseInfo getDatabaseInfo() { return databaseInfo; } + public List findDependencies(Session session, String objectName, @Nullable String fieldName) { + Assert.notNull(session, C.SESSION); + Assert.notNull(objectName, "object name"); + + return session.doReturningWork(new ReturningWork>() { + + @Override + public List execute(Connection connection) throws SQLException { + final var result = new ArrayList(); + final String query = buildDependencyQuery(fieldName != null); + try (var statement = connection.prepareStatement(query)) { + statement.setString(1, objectName); + if (fieldName != null) { + statement.setString(2, fieldName); + } + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + result.add(resultSet.getString(1)); + } + } + } + return result; + } + }); + } + synchronized SchemaConfiguration loadSchemaConfiguration(Session session) { return session.createQuery("from SchemaConfiguration", SchemaConfiguration.class) .uniqueResult(); @@ -127,7 +157,8 @@ synchronized boolean updateSchema() { final long startTime = System.currentTimeMillis(); try (Connection connection = dataSource.getConnection()) { final String customChangeSets = loadCustomChangeSets(connection); - final String changeLog = replaceLimits(systemChangeLog.replace("<#CHANGE_SETS#>", customChangeSets)); + final String changeLog = replaceLimits( + systemChangeLog.replace("<#CHANGE_SETS#>", customChangeSets)); if (log.isDebugEnabled()) { log.debug("Changelog content:\r\n{}", changeLog); } @@ -185,7 +216,8 @@ private String loadCustomChangeSets(Connection connection) throws SQLException { final StringBuilder buf = new StringBuilder(); if (existTable(connection, CHANGELOG_TABLE)) { try (Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select changeset from " + CHANGELOG_TABLE + " order by id")) { + ResultSet resultSet = statement.executeQuery("select changeset from " + CHANGELOG_TABLE + + " order by id")) { while (resultSet.next()) { if (buf.length() > 0) { buf.append(','); @@ -209,7 +241,8 @@ private String loadSchemaUpdateChangeSet(SchemaVersion version) { private static boolean existLock(Connection connection) throws SQLException { try (Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select count(*) from " + CHANGELOG_LOCKTABLE + " where locked = true")) { + ResultSet resultSet = statement.executeQuery("select count(*) from " + CHANGELOG_LOCKTABLE + + " where locked = true")) { return resultSet.next() && resultSet.getInt(1) == 1; } } @@ -221,12 +254,33 @@ private static void removeLock(Connection connection) throws SQLException { private static void removeLastChangeLog(Connection connection) throws SQLException { try (Statement statement = connection.createStatement()) { - statement.executeUpdate("delete from " + CHANGELOG_TABLE + " where id = (select max(id) from " + CHANGELOG_TABLE + ')'); + statement.executeUpdate("delete from " + CHANGELOG_TABLE + + " where id = (select max(id) from " + CHANGELOG_TABLE + ')'); + } + } + + private static String buildDependencyQuery(boolean withAttribute) { + final var buf = new StringBuffer() + .append("select distinct dep_obj.relname from pg_depend") + .append(" join pg_rewrite on pg_depend.objid = pg_rewrite.oid") + .append(" join pg_class as dep_obj on pg_rewrite.ev_class = dep_obj.oid") + .append(" join pg_class as source_obj on pg_depend.refobjid = source_obj.oid") + .append(" join pg_attribute on pg_depend.refobjid = pg_attribute.attrelid") + .append(" and pg_depend.refobjsubid = pg_attribute.attnum") + .append(" join pg_namespace dep_ns on dep_ns.oid = dep_obj.relnamespace") + .append(" join pg_namespace source_ns on source_ns.oid = source_obj.relnamespace") + .append(" where source_ns.oid = dep_ns.oid") + .append(" and source_ns.nspname = 'public'") + .append(" and source_obj.relname = ?"); + if (withAttribute) { + buf.append(" and pg_attribute.attname = ?"); } + return buf.toString(); } private static boolean existTable(Connection connection, String tableName) throws SQLException { - try (ResultSet resultSet = connection.getMetaData().getTables(null, null, tableName, new String[] { "TABLE" })) { + try (ResultSet resultSet = connection.getMetaData().getTables(null, null, tableName, + new String[] { "TABLE" })) { while (resultSet.next()) { if (tableName.equalsIgnoreCase(resultSet.getString("TABLE_NAME"))) { return true; @@ -254,7 +308,8 @@ private StringResourceAccessor(String text) { } @Override - public InputStreamList openStreams(String relativeTo, String streamPath) throws IOException { + public InputStreamList openStreams(String relativeTo, String streamPath) + throws IOException { Assert.state(CHANGELOG_FILENAME.equals(streamPath), "unknown path: " + streamPath); return new InputStreamList(null, StreamUtils.getStringAsStream(text)); @@ -262,7 +317,8 @@ public InputStreamList openStreams(String relativeTo, String streamPath) throws @Override public SortedSet list(String relativeTo, String path, boolean recursive, - boolean includeFiles, boolean includeDirectories) throws IOException { + boolean includeFiles, boolean includeDirectories) + throws IOException { throw new UnsupportedOperationException(); } diff --git a/src/main/java/org/seed/core/data/dbobject/DBObject.java b/src/main/java/org/seed/core/data/dbobject/DBObject.java index 8e46e5446..b98023a81 100644 --- a/src/main/java/org/seed/core/data/dbobject/DBObject.java +++ b/src/main/java/org/seed/core/data/dbobject/DBObject.java @@ -18,7 +18,6 @@ package org.seed.core.data.dbobject; import org.seed.core.application.ApplicationEntity; -import org.seed.core.data.SystemEntity; public interface DBObject extends ApplicationEntity { @@ -34,7 +33,7 @@ public interface DBObject extends ApplicationEntity { boolean contains(String text); - boolean contains(SystemEntity other); + boolean contains(DBObject other); boolean isOrderHigherThan(DBObject dbObject); diff --git a/src/main/java/org/seed/core/data/dbobject/DBObjectMetadata.java b/src/main/java/org/seed/core/data/dbobject/DBObjectMetadata.java index ab17e4a4a..c423ee3af 100644 --- a/src/main/java/org/seed/core/data/dbobject/DBObjectMetadata.java +++ b/src/main/java/org/seed/core/data/dbobject/DBObjectMetadata.java @@ -30,7 +30,6 @@ import org.seed.C; import org.seed.core.application.AbstractApplicationEntity; import org.seed.core.application.ContentObject; -import org.seed.core.data.SystemEntity; import org.seed.core.util.Assert; import org.seed.core.util.CDATAXmlAdapter; @@ -94,10 +93,10 @@ public String getObjectName() { } @Override - public boolean contains(SystemEntity entity) { - Assert.notNull(entity, C.ENTITY); + public boolean contains(DBObject dbObject) { + Assert.notNull(dbObject, C.DBOBJECT); - return DBObjectUtils.containsName(content, entity.getInternalName()); + return DBObjectUtils.containsName(content, dbObject.getInternalName()); } @Override diff --git a/src/main/java/org/seed/core/data/dbobject/DBObjectService.java b/src/main/java/org/seed/core/data/dbobject/DBObjectService.java index 141419f4f..9cd05ff19 100644 --- a/src/main/java/org/seed/core/data/dbobject/DBObjectService.java +++ b/src/main/java/org/seed/core/data/dbobject/DBObjectService.java @@ -20,12 +20,9 @@ import java.util.List; import org.seed.core.application.ApplicationEntityService; -import org.seed.core.data.SystemEntity; public interface DBObjectService extends ApplicationEntityService { - List findUsage(SystemEntity entity); - - List findViewsContains(SystemEntity entity); + List findUsage(DBObject object); } diff --git a/src/main/java/org/seed/core/data/dbobject/DBObjectServiceImpl.java b/src/main/java/org/seed/core/data/dbobject/DBObjectServiceImpl.java index 99f66f279..3da55aafa 100644 --- a/src/main/java/org/seed/core/data/dbobject/DBObjectServiceImpl.java +++ b/src/main/java/org/seed/core/data/dbobject/DBObjectServiceImpl.java @@ -20,6 +20,7 @@ import static org.seed.core.util.CollectionUtils.*; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.hibernate.Session; @@ -32,11 +33,19 @@ import org.seed.core.application.module.ImportAnalysis; import org.seed.core.application.module.Module; import org.seed.core.application.module.TransferContext; +import org.seed.core.config.SchemaManager; import org.seed.core.config.changelog.ChangeLog; import org.seed.core.data.AbstractSystemObject; -import org.seed.core.data.SystemEntity; import org.seed.core.data.ValidationException; +import org.seed.core.entity.Entity; +import org.seed.core.entity.EntityDependent; +import org.seed.core.entity.EntityField; +import org.seed.core.entity.EntityFieldGroup; +import org.seed.core.entity.EntityFunction; +import org.seed.core.entity.EntityRelation; import org.seed.core.entity.EntityService; +import org.seed.core.entity.EntityStatus; +import org.seed.core.entity.NestedEntity; import org.seed.core.util.Assert; import org.springframework.beans.factory.annotation.Autowired; @@ -45,7 +54,7 @@ @Service public class DBObjectServiceImpl extends AbstractApplicationEntityService - implements DBObjectService { + implements DBObjectService, EntityDependent { @Autowired private DBObjectRepository repository; @@ -53,6 +62,9 @@ public class DBObjectServiceImpl extends AbstractApplicationEntityService findUsage(SystemEntity entity) { - Assert.notNull(entity, C.ENTITY); - - return findUsage(null, entity); - } - - @Override - public List findViewsContains(SystemEntity entity) { - Assert.notNull(entity, C.ENTITY); - - return findUsage(DBObjectType.VIEW, entity); - } - @Override public void initObject(DBObject dbObject) throws ValidationException { Assert.notNull(dbObject, C.DBOBJECT); @@ -110,6 +108,52 @@ public void initObject(DBObject dbObject) throws ValidationException { ((DBObjectMetadata) dbObject).setContent(content); } + @Override + public List findUsage(DBObject dbObject) { + Assert.notNull(dbObject, C.DBOBJECT); + + return subList(repository.find(), object -> object.isEnabled() && object.contains(dbObject)); + } + + @Override + public List findUsage(Entity entity, Session session) { + return convertedList(schemaManager.findDependencies(session, entity.getEffectiveTableName(), null), + DBObjectServiceImpl::createDummyObject); + } + + @Override + public List findUsage(EntityField entityField, Session session) { + return convertedList(schemaManager.findDependencies(session, + entityField.getEntity().getEffectiveTableName(), + entityField.getEffectiveColumnName()), + DBObjectServiceImpl::createDummyObject); + } + + @Override + public List findUsage(EntityFieldGroup fieldGroup) { + return Collections.emptyList(); + } + + @Override + public List findUsage(EntityStatus entityStatus, Session session) { + return Collections.emptyList(); + } + + @Override + public List findUsage(EntityFunction entityFunction, Session session) { + return Collections.emptyList(); + } + + @Override + public List findUsage(NestedEntity nestedEntity, Session session) { + return Collections.emptyList(); + } + + @Override + public List findUsage(EntityRelation entityRelation, Session session) { + return Collections.emptyList(); + } + @Override protected void analyzeNextVersionObjects(ImportAnalysis analysis, Module currentVersionModule) { if (analysis.getModule().getDBObjects() != null) { @@ -264,13 +308,6 @@ public void saveObject(DBObject dbObject) throws ValidationException { updateConfiguration(); } - private List findUsage(DBObjectType type, SystemEntity entity) { - final var objects = type != null - ? repository.find(queryParam(C.TYPE, type)) - : repository.find(); - return subList(objects, object -> object.isEnabled() && object.contains(entity)); - } - private static ChangeLog createChangeLog(DBObject currentVersionObject, DBObject nextVersionObject) { return new DBObjectChangeLogBuilder() .setCurrentVersionObject(currentVersionObject) @@ -278,4 +315,10 @@ private static ChangeLog createChangeLog(DBObject currentVersionObject, DBObject .build(); } + private static DBObject createDummyObject(String name) { + final var object = new DBObjectMetadata(); + object.setName(name); + return object; + } + } diff --git a/src/main/java/org/seed/core/data/dbobject/DBObjectValidator.java b/src/main/java/org/seed/core/data/dbobject/DBObjectValidator.java index bb30316eb..22ffdad57 100644 --- a/src/main/java/org/seed/core/data/dbobject/DBObjectValidator.java +++ b/src/main/java/org/seed/core/data/dbobject/DBObjectValidator.java @@ -25,6 +25,7 @@ import org.hibernate.Transaction; import org.seed.C; +import org.seed.core.config.SchemaManager; import org.seed.core.config.SessionProvider; import org.seed.core.data.AbstractSystemEntityValidator; import org.seed.core.data.DataException; @@ -45,6 +46,9 @@ public class DBObjectValidator extends AbstractSystemEntityValidator { @Autowired private DBObjectRepository repository; + @Autowired + private SchemaManager schemaManager; + @Override public void validateCreate(DBObject dbObject) throws ValidationException { Assert.notNull(dbObject, C.DBOBJECT); @@ -60,11 +64,17 @@ public void validateDelete(DBObject dbObject) throws ValidationException { Assert.notNull(dbObject, C.DBOBJECT); final var errors = createValidationErrors(dbObject); final var service = getBean(DBObjectService.class); - - filterAndForEach(service.findUsage(dbObject), - not(dbObject::equals), - object -> errors.addError("val.inuse.dbobjectdelete", object.getName(), - getEnumLabel(object.getType()))); + try (Session session = sessionProvider.getSession()) { + schemaManager.findDependencies(session, dbObject.getInternalName(), null) + .forEach(view -> errors.addError("val.inuse.dbobjectdelete", view, + getEnumLabel(DBObjectType.VIEW))); + } + if (errors.isEmpty()) { + filterAndForEach(service.findUsage(dbObject), + not(dbObject::equals), + object -> errors.addError("val.inuse.dbobjectdelete", object.getName(), + getEnumLabel(object.getType()))); + } validate(errors); } diff --git a/src/main/java/org/seed/core/entity/EntityValidator.java b/src/main/java/org/seed/core/entity/EntityValidator.java index 6cc2d9cdb..beef49171 100644 --- a/src/main/java/org/seed/core/entity/EntityValidator.java +++ b/src/main/java/org/seed/core/entity/EntityValidator.java @@ -25,11 +25,11 @@ import org.seed.C; import org.seed.core.codegen.CodeUtils; +import org.seed.core.config.SchemaManager; import org.seed.core.data.AbstractSystemEntityValidator; import org.seed.core.data.SystemEntity; import org.seed.core.data.ValidationErrors; import org.seed.core.data.ValidationException; -import org.seed.core.data.dbobject.DBObjectService; import org.seed.core.util.Assert; import org.seed.core.util.MiscUtils; import org.seed.core.util.NameUtils; @@ -45,7 +45,7 @@ public class EntityValidator extends AbstractSystemEntityValidator { private EntityRepository repository; @Autowired - private DBObjectService dbObjectService; + private SchemaManager schemaManager; private List> entityDependents; @@ -321,8 +321,9 @@ private void validateFieldAttributeChange(Entity entity, Entity currentVersionEn // field type change if (field.getType() != currentVersionField.getType()) { - dbObjectService.findViewsContains(entity) - .forEach(view -> errors.addError("val.inuse.entityview", view.getName())); + schemaManager.findDependencies(session, entity.getEffectiveTableName(), + field.getEffectiveColumnName()) + .forEach(view -> errors.addError("val.inuse.fieldviewtype", field.getName(), view)); } // change to unique if (field.isUnique() && !currentVersionField.isUnique() && @@ -657,6 +658,10 @@ private void validateDeleteEntityDependent(Entity entity, EntityDependent"{0}" forces an o val.inuse.dbobjectnestedorder = The used database item "{0}" forces an order greater than {1} val.inuse.dbobjectrename = Item cannot be renamed because it is still in use in the database item "{0}" ({1}) val.inuse.entitydatasource = Entity is still used in a parameter of query "{0}" +val.inuse.entitydbobject = Entity is still referenced by view "{0}" val.inuse.entityentity = Entity is still referenced by entity "{0}" val.inuse.entityfilter = Entity is still used in filter "{0}" val.inuse.entityform = Entity is still in use in form "{0}" @@ -1067,6 +1068,8 @@ val.inuse.fieldfilter = Field is still used in filter "{0}" val.inuse.fieldform = Field is still used in form "{0}" val.inuse.fieldformula = Field is still used in a calculation formula in field "{0}" val.inuse.fieldtransform = Field is still used in transformation "{0}" +val.inuse.fieldview = Field is still used in view "{0}" +val.inuse.fieldviewtype = Data type of field "{0}" cannot be changed because it is still used in the view "{1}" val.inuse.filterform = Filter is still in use in form "{0}" val.inuse.functionform = Function is still used in form "{0}" val.inuse.functionstatus = Function is still used in a transition diff --git a/src/main/resources/metainfo/zk-label_de.properties b/src/main/resources/metainfo/zk-label_de.properties index 12d8db212..24eadfdbc 100644 --- a/src/main/resources/metainfo/zk-label_de.properties +++ b/src/main/resources/metainfo/zk-label_de.properties @@ -1054,6 +1054,7 @@ val.inuse.dbobjectdependentorder = Die Verwendung im Datenbankelement "{0}""{0}" erzwingt eine Reihenfolge größer {1} val.inuse.dbobjectrename = Das Element kann nicht umbenannt werden, da es noch im Datenbankelement "{0}" ({1}) verwendet wird val.inuse.entitydatasource = Die Entität wird noch in einem Parameter der Abfrage "{0}" verwendet +val.inuse.entitydbobject = Die Entität wird noch von der View "{0}" referenziert val.inuse.entityentity = Die Entität wird noch von der Entität "{0}" referenziert val.inuse.entityfilter = Die Entität wird noch im Filter "{0}" verwendet val.inuse.entityform = Die Entität wird noch im Formular "{0}" verwendet @@ -1067,6 +1068,8 @@ val.inuse.fieldfilter = Das Feld wird noch im Filter "{0}" verwendet val.inuse.fieldform = Das Feld wird noch im Formular "{0}" verwendet val.inuse.fieldformula = Das Feld wird noch in einer Berechnungsformel im Feld "{0}" verwendet val.inuse.fieldtransform = Das Feld wird noch in der Transformation "{0}" verwendet +val.inuse.fieldview = Das Feld wird noch von der View "{0}" verwendet +val.inuse.fieldviewtype = Der Datentyp des Felds "{0}" kann nicht geändert werden, da es noch in der View "{1}" verwendet val.inuse.filterform = Der Filter wird noch im Formular "{0}" verwendet val.inuse.functionform = Die Funktion wird noch im Formular "{0}" verwendet val.inuse.functionstatus = Die Funktion wird noch in einem Statusübergang verwendet