/***************************************************************** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. ****************************************************************/ package org.apache.cayenne.access; import org.apache.cayenne.ashwood.AshwoodEntitySorter; import org.apache.cayenne.conn.DataSourceInfo; import org.apache.cayenne.datasource.DriverDataSource; import org.apache.cayenne.dba.DbAdapter; import org.apache.cayenne.dba.PkGenerator; import org.apache.cayenne.dba.TypesMapping; import org.apache.cayenne.log.JdbcEventLogger; import org.apache.cayenne.map.DataMap; import org.apache.cayenne.map.DbAttribute; import org.apache.cayenne.map.DbEntity; import org.apache.cayenne.map.DbJoin; import org.apache.cayenne.map.DbRelationship; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.map.EntitySorter; import org.apache.cayenne.validation.SimpleValidationFailure; import org.apache.cayenne.validation.ValidationResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.sql.DataSource; import java.sql.Connection; import java.sql.Driver; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; /** * Utility class that generates database schema based on Cayenne mapping. It is * a logical counterpart of DbLoader class. */ public class DbGenerator { private Logger logObj = LoggerFactory.getLogger(DbGenerator.class); protected DbAdapter adapter; protected DataMap map; // optional DataDomain needed for correct FK generation in cross-db // situations protected DataDomain domain; protected JdbcEventLogger jdbcEventLogger; // stores generated SQL statements protected Map<String, Collection<String>> dropTables; protected Map<String, String> createTables; protected Map<String, List<String>> createConstraints; protected List<String> createPK; protected List<String> dropPK; /** * Contains all DbEntities ordered considering their interdependencies. * DerivedDbEntities are filtered out of this list. */ protected List<DbEntity> dbEntitiesInInsertOrder; protected List<DbEntity> dbEntitiesRequiringAutoPK; protected boolean shouldDropTables; protected boolean shouldCreateTables; protected boolean shouldDropPKSupport; protected boolean shouldCreatePKSupport; protected boolean shouldCreateFKConstraints; protected ValidationResult failures; /** * @since 3.1 */ public DbGenerator(DbAdapter adapter, DataMap map, JdbcEventLogger logger) { this(adapter, map, logger, Collections.<DbEntity> emptyList()); } /** * @since 3.1 */ public DbGenerator(DbAdapter adapter, DataMap map, JdbcEventLogger logger, Collection<DbEntity> excludedEntities) { this(adapter, map, excludedEntities, null, logger); } /** * Creates and initializes new DbGenerator instance. * * @param adapter * DbAdapter corresponding to the database * @param map * DataMap whose entities will be used in schema generation * @param excludedEntities * entities that should be ignored during schema generation * @param domain * optional DataDomain used to detect cross-database * relationships. * @since 3.1 */ public DbGenerator(DbAdapter adapter, DataMap map, Collection<DbEntity> excludedEntities, DataDomain domain, JdbcEventLogger logger) { // sanity check if (adapter == null) { throw new IllegalArgumentException("Adapter must not be null."); } if (map == null) { throw new IllegalArgumentException("DataMap must not be null."); } this.domain = domain; this.map = map; this.adapter = adapter; this.jdbcEventLogger = logger; prepareDbEntities(excludedEntities); resetToDefaults(); buildStatements(); } protected void resetToDefaults() { this.shouldDropTables = false; this.shouldDropPKSupport = false; this.shouldCreatePKSupport = true; this.shouldCreateTables = true; this.shouldCreateFKConstraints = true; } /** * Creates and stores internally a set of statements for database schema * creation, ignoring configured schema creation preferences. Statements are * NOT executed in this method. */ protected void buildStatements() { dropTables = new HashMap<>(); createTables = new HashMap<>(); createConstraints = new HashMap<>(); DbAdapter adapter = getAdapter(); for (final DbEntity dbe : this.dbEntitiesInInsertOrder) { String name = dbe.getName(); // build "DROP TABLE" dropTables.put(name, adapter.dropTableStatements(dbe)); // build "CREATE TABLE" createTables.put(name, adapter.createTable(dbe)); // build constraints createConstraints.put(name, createConstraintsQueries(dbe)); } PkGenerator pkGenerator = adapter.getPkGenerator(); dropPK = pkGenerator.dropAutoPkStatements(dbEntitiesRequiringAutoPK); createPK = pkGenerator.createAutoPkStatements(dbEntitiesRequiringAutoPK); } /** * Returns <code>true</code> if there is nothing to be done by this * generator. If <code>respectConfiguredSettings</code> is <code>true</code> * , checks are done applying currently configured settings, otherwise check * is done, assuming that all possible generated objects. */ public boolean isEmpty(boolean respectConfiguredSettings) { if (dbEntitiesInInsertOrder.isEmpty() && dbEntitiesRequiringAutoPK.isEmpty()) { return true; } if (!respectConfiguredSettings) { return false; } return !(shouldDropTables || shouldCreateTables || shouldCreateFKConstraints || shouldCreatePKSupport || shouldDropPKSupport); } /** Returns DbAdapter associated with this DbGenerator. */ public DbAdapter getAdapter() { return adapter; } /** * Returns a list of all schema statements that should be executed with the * current configuration. */ public List<String> configuredStatements() { List<String> list = new ArrayList<>(); if (shouldDropTables) { ListIterator<DbEntity> it = dbEntitiesInInsertOrder.listIterator(dbEntitiesInInsertOrder.size()); while (it.hasPrevious()) { DbEntity ent = it.previous(); list.addAll(dropTables.get(ent.getName())); } } if (shouldCreateTables) { for (final DbEntity ent : dbEntitiesInInsertOrder) { list.add(createTables.get(ent.getName())); } } if (shouldCreateFKConstraints) { for (final DbEntity ent : dbEntitiesInInsertOrder) { List<String> fks = createConstraints.get(ent.getName()); list.addAll(fks); } } if (shouldDropPKSupport) { list.addAll(dropPK); } if (shouldCreatePKSupport) { list.addAll(createPK); } return list; } /** * Creates a temporary DataSource out of DataSourceInfo and invokes * <code>public void runGenerator(DataSource ds)</code>. */ public void runGenerator(DataSourceInfo dsi) throws Exception { this.failures = null; // do a pre-check. Maybe there is no need to run anything // and therefore no need to create a connection if (isEmpty(true)) { return; } Driver driver = (Driver) Class.forName(dsi.getJdbcDriver()).newInstance(); DataSource dataSource = new DriverDataSource(driver, dsi.getDataSourceUrl(), dsi.getUserName(), dsi.getPassword()); runGenerator(dataSource); } /** * Executes a set of commands to drop/create database objects. This is the * main worker method of DbGenerator. Command set is built based on * pre-configured generator settings. */ public void runGenerator(DataSource ds) throws Exception { this.failures = null; try (Connection connection = ds.getConnection();) { // drop tables if (shouldDropTables) { ListIterator<DbEntity> it = dbEntitiesInInsertOrder.listIterator(dbEntitiesInInsertOrder.size()); while (it.hasPrevious()) { DbEntity ent = it.previous(); for (String statement : dropTables.get(ent.getName())) { safeExecute(connection, statement); } } } // create tables List<String> createdTables = new ArrayList<>(); if (shouldCreateTables) { for (final DbEntity ent : dbEntitiesInInsertOrder) { // only create missing tables safeExecute(connection, createTables.get(ent.getName())); createdTables.add(ent.getName()); } } // create FK if (shouldCreateTables && shouldCreateFKConstraints) { for (DbEntity ent : dbEntitiesInInsertOrder) { if (createdTables.contains(ent.getName())) { List<String> fks = createConstraints.get(ent.getName()); for (String fk : fks) { safeExecute(connection, fk); } } } } // drop PK if (shouldDropPKSupport) { List<String> dropAutoPKSQL = getAdapter().getPkGenerator().dropAutoPkStatements( dbEntitiesRequiringAutoPK); for (final String sql : dropAutoPKSQL) { safeExecute(connection, sql); } } // create pk if (shouldCreatePKSupport) { List<String> createAutoPKSQL = getAdapter().getPkGenerator().createAutoPkStatements( dbEntitiesRequiringAutoPK); for (final String sql : createAutoPKSQL) { safeExecute(connection, sql); } } new DbGeneratorPostprocessor().execute(connection, getAdapter()); } } /** * Builds and executes a SQL statement, catching and storing SQL exceptions * resulting from invalid SQL. Only non-recoverable exceptions are rethrown. * * @since 1.1 */ protected boolean safeExecute(Connection connection, String sql) throws SQLException { try (Statement statement = connection.createStatement();) { jdbcEventLogger.log(sql); statement.execute(sql); return true; } catch (SQLException ex) { if (this.failures == null) { this.failures = new ValidationResult(); } failures.addFailure(new SimpleValidationFailure(sql, ex.getMessage())); jdbcEventLogger.logQueryError(ex); return false; } } /** * Creates FK and UNIQUE constraint statements for a given table. * * @since 3.0 */ public List<String> createConstraintsQueries(DbEntity table) { List<String> list = new ArrayList<>(); for (final DbRelationship rel : table.getRelationships()) { if (rel.isToMany()) { continue; } // skip FK to a different DB if (domain != null) { DataMap srcMap = rel.getSourceEntity().getDataMap(); DataMap targetMap = rel.getTargetEntity().getDataMap(); if (srcMap != null && targetMap != null && srcMap != targetMap) { if (domain.lookupDataNode(srcMap) != domain.lookupDataNode(targetMap)) { continue; } } } // create an FK CONSTRAINT only if the relationship is to PK // and if this is not a dependent PK // create UNIQUE CONSTRAINT on FK if reverse relationship is to-one if (rel.isToPK() && !rel.isToDependentPK()) { if (getAdapter().supportsUniqueConstraints()) { DbRelationship reverse = rel.getReverseRelationship(); if (reverse != null && !reverse.isToMany() && !reverse.isToPK()) { String unique = getAdapter().createUniqueConstraint((DbEntity) rel.getSourceEntity(), rel.getSourceAttributes()); if (unique != null) { list.add(unique); } } } String fk = getAdapter().createFkConstraint(rel); if (fk != null) { list.add(fk); } } } return list; } /** * Returns an object representing a collection of failures that occurred on * the last "runGenerator" invocation, or null if there were no failures. * Failures usually indicate problems with generated DDL (such as * "create...", "drop...", etc.) and usually happen due to the DataMap being * out of sync with the database. * * @since 1.1 */ public ValidationResult getFailures() { return failures; } /** * Returns whether DbGenerator is configured to create primary key support * for DataMap entities. */ public boolean shouldCreatePKSupport() { return shouldCreatePKSupport; } /** * Returns whether DbGenerator is configured to create tables for DataMap * entities. */ public boolean shouldCreateTables() { return shouldCreateTables; } public boolean shouldDropPKSupport() { return shouldDropPKSupport; } public boolean shouldDropTables() { return shouldDropTables; } public boolean shouldCreateFKConstraints() { return shouldCreateFKConstraints; } public void setShouldCreatePKSupport(boolean shouldCreatePKSupport) { this.shouldCreatePKSupport = shouldCreatePKSupport; } public void setShouldCreateTables(boolean shouldCreateTables) { this.shouldCreateTables = shouldCreateTables; } public void setShouldDropPKSupport(boolean shouldDropPKSupport) { this.shouldDropPKSupport = shouldDropPKSupport; } public void setShouldDropTables(boolean shouldDropTables) { this.shouldDropTables = shouldDropTables; } public void setShouldCreateFKConstraints(boolean shouldCreateFKConstraints) { this.shouldCreateFKConstraints = shouldCreateFKConstraints; } /** * Returns a DataDomain used by the DbGenerator to detect cross-database * relationships. By default DataDomain is null. * * @since 1.2 */ public DataDomain getDomain() { return domain; } /** * Helper method that orders DbEntities to satisfy referential constraints * and returns an ordered list. It also filters out DerivedDbEntities. */ private void prepareDbEntities(Collection<DbEntity> excludedEntities) { if (excludedEntities == null) { excludedEntities = Collections.emptyList(); } List<DbEntity> tables = new ArrayList<>(); List<DbEntity> tablesWithAutoPk = new ArrayList<>(); for (DbEntity nextEntity : map.getDbEntities()) { // do sanity checks... // tables with no columns are not included if (nextEntity.getAttributes().size() == 0) { logObj.info("Skipping entity with no attributes: " + nextEntity.getName()); continue; } // check if this entity is explicitly excluded if (excludedEntities.contains(nextEntity)) { continue; } // tables with invalid DbAttributes are not included boolean invalidAttributes = false; for (final DbAttribute attr : nextEntity.getAttributes()) { if (attr.getType() == TypesMapping.NOT_DEFINED) { logObj.info("Skipping entity, attribute type is undefined: " + nextEntity.getName() + "." + attr.getName()); invalidAttributes = true; break; } } if (invalidAttributes) { continue; } tables.add(nextEntity); // check if an automatic PK generation can be potentially supported // in this entity. For now simply check that the key is not // propagated Iterator<DbRelationship> relationships = nextEntity.getRelationships().iterator(); // create a copy of the original PK list, // since the list will be modified locally List<DbAttribute> pkAttributes = new ArrayList<>(nextEntity.getPrimaryKeys()); while (pkAttributes.size() > 0 && relationships.hasNext()) { DbRelationship nextRelationship = relationships.next(); if (!nextRelationship.isToMasterPK()) { continue; } // supposedly all source attributes of the relationship // to master entity must be a part of primary key, // so for (DbJoin join : nextRelationship.getJoins()) { pkAttributes.remove(join.getSource()); } } // primary key is needed only if at least one of the primary key // attributes // is not propagated via relationship if (pkAttributes.size() > 0) { tablesWithAutoPk.add(nextEntity); } } // sort table list if (tables.size() > 1) { EntitySorter sorter = new AshwoodEntitySorter(); sorter.setEntityResolver(new EntityResolver(Collections.singleton(map))); sorter.sortDbEntities(tables, false); } this.dbEntitiesInInsertOrder = tables; this.dbEntitiesRequiringAutoPK = tablesWithAutoPk; } }