HHH-8162 Make unique constraint handling on schema update configurable

This commit is contained in:
Brett Meyer 2013-04-17 11:34:24 -04:00
parent 4d461cc44e
commit 377c300071
9 changed files with 333 additions and 92 deletions

View File

@ -23,8 +23,6 @@
*/
package org.hibernate.cfg;
import org.hibernate.tool.hbm2ddl.DatabaseMetadata;
/**
* @author Steve Ebersole
*/
@ -648,4 +646,22 @@ public interface AvailableSettings {
* Oracle), this is disabled by default.
*/
public static final String ENABLE_SYNONYMS = "hibernate.synonyms";
/**
* Unique columns and unique keys both use unique constraints in most dialects.
* SchemaUpdate needs to create these constraints, but DB's
* support for finding existing constraints is extremely inconsistent. Further,
* non-explicitly-named unique constraints use randomly generated characters.
*
* Therefore, select from these strategies.
* {@link org.hibernate.tool.hbm2ddl.UniqueConstraintSchemaUpdateStrategy#DROP_RECREATE_QUIETLY} (DEFAULT):
* Attempt to drop, then (re-)create each unique constraint.
* Ignore any exceptions thrown.
* {@link org.hibernate.tool.hbm2ddl.UniqueConstraintSchemaUpdateStrategy#RECREATE_QUIETLY}:
* attempt to (re-)create unique constraints,
* ignoring exceptions thrown if the constraint already existed
* {@link org.hibernate.tool.hbm2ddl.UniqueConstraintSchemaUpdateStrategy#SKIP}:
* do not attempt to create unique constraints on a schema update
*/
public static final String UNIQUE_CONSTRAINT_SCHEMA_UPDATE_STRATEGY = "hibernate.schema_update.unique_constraint_strategy";
}

View File

@ -137,8 +137,10 @@ import org.hibernate.proxy.EntityNotFoundDelegate;
import org.hibernate.secure.spi.GrantedPermission;
import org.hibernate.secure.spi.JaccPermissionDeclarations;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.tool.hbm2ddl.UniqueConstraintSchemaUpdateStrategy;
import org.hibernate.tool.hbm2ddl.DatabaseMetadata;
import org.hibernate.tool.hbm2ddl.IndexMetadata;
import org.hibernate.tool.hbm2ddl.SchemaUpdateScript;
import org.hibernate.tool.hbm2ddl.TableMetadata;
import org.hibernate.tuple.entity.EntityTuplizerFactory;
import org.hibernate.type.BasicType;
@ -1117,57 +1119,64 @@ public class Configuration implements Serializable {
*
* @throws HibernateException Generally indicates a problem calling {@link #buildMappings()}
*
* @see org.hibernate.tool.hbm2ddl.SchemaExport
* @see org.hibernate.tool.hbm2ddl.SchemaUpdate
*
* @deprecated Use {@link #generateSchemaUpdateScriptList(Dialect, DatabaseMetadata)} instead
*/
@SuppressWarnings({ "unchecked" })
@Deprecated
public String[] generateSchemaUpdateScript(Dialect dialect, DatabaseMetadata databaseMetadata)
throws HibernateException {
List<SchemaUpdateScript> scripts = generateSchemaUpdateScriptList( dialect, databaseMetadata );
return SchemaUpdateScript.toStringArray( scripts );
}
/**
* @param dialect The dialect for which to generate the creation script
* @param databaseMetadata The database catalog information for the database to be updated; needed to work out what
* should be created/altered
*
* @return The sequence of DDL commands to apply the schema objects
*
* @throws HibernateException Generally indicates a problem calling {@link #buildMappings()}
*
* @see org.hibernate.tool.hbm2ddl.SchemaUpdate
*/
public List<SchemaUpdateScript> generateSchemaUpdateScriptList(Dialect dialect, DatabaseMetadata databaseMetadata)
throws HibernateException {
secondPassCompile();
String defaultCatalog = properties.getProperty( Environment.DEFAULT_CATALOG );
String defaultSchema = properties.getProperty( Environment.DEFAULT_SCHEMA );
UniqueConstraintSchemaUpdateStrategy constraintMethod = UniqueConstraintSchemaUpdateStrategy.interpret( properties
.get( Environment.UNIQUE_CONSTRAINT_SCHEMA_UPDATE_STRATEGY ) );
ArrayList<String> script = new ArrayList<String>( 50 );
List<SchemaUpdateScript> scripts = new ArrayList<SchemaUpdateScript>();
Iterator iter = getTableMappings();
while ( iter.hasNext() ) {
Table table = (Table) iter.next();
String tableSchema = ( table.getSchema() == null ) ? defaultSchema : table.getSchema() ;
String tableSchema = ( table.getSchema() == null ) ? defaultSchema : table.getSchema();
String tableCatalog = ( table.getCatalog() == null ) ? defaultCatalog : table.getCatalog();
if ( table.isPhysicalTable() ) {
TableMetadata tableInfo = databaseMetadata.getTableMetadata(
table.getName(),
tableSchema,
tableCatalog,
table.isQuoted()
);
TableMetadata tableInfo = databaseMetadata.getTableMetadata( table.getName(), tableSchema,
tableCatalog, table.isQuoted() );
if ( tableInfo == null ) {
script.add(
table.sqlCreateString(
dialect,
mapping,
tableCatalog,
tableSchema
)
);
scripts.add( new SchemaUpdateScript( table.sqlCreateString( dialect, mapping, tableCatalog,
tableSchema ), false ) );
}
else {
Iterator<String> subiter = table.sqlAlterStrings(
dialect,
mapping,
tableInfo,
tableCatalog,
tableSchema
);
Iterator<String> subiter = table.sqlAlterStrings( dialect, mapping, tableInfo, tableCatalog,
tableSchema );
while ( subiter.hasNext() ) {
script.add( subiter.next() );
scripts.add( new SchemaUpdateScript( subiter.next(), false ) );
}
}
Iterator<String> comments = table.sqlCommentStrings( dialect, defaultCatalog, defaultSchema );
while ( comments.hasNext() ) {
script.add( comments.next() );
scripts.add( new SchemaUpdateScript( comments.next(), false ) );
}
}
@ -1176,32 +1185,34 @@ public class Configuration implements Serializable {
iter = getTableMappings();
while ( iter.hasNext() ) {
Table table = (Table) iter.next();
String tableSchema = ( table.getSchema() == null ) ? defaultSchema : table.getSchema() ;
String tableSchema = ( table.getSchema() == null ) ? defaultSchema : table.getSchema();
String tableCatalog = ( table.getCatalog() == null ) ? defaultCatalog : table.getCatalog();
if ( table.isPhysicalTable() ) {
TableMetadata tableInfo = databaseMetadata.getTableMetadata(
table.getName(),
tableSchema,
tableCatalog,
table.isQuoted()
);
TableMetadata tableInfo = databaseMetadata.getTableMetadata( table.getName(), tableSchema,
tableCatalog, table.isQuoted() );
Iterator uniqueIter = table.getUniqueKeyIterator();
while ( uniqueIter.hasNext() ) {
final UniqueKey uniqueKey = (UniqueKey) uniqueIter.next();
// Skip if index already exists. Most of the time, this
// won't work since most Dialects use Constraints. However,
// keep it for the few that do use Indexes.
if ( tableInfo != null && StringHelper.isNotEmpty( uniqueKey.getName() ) ) {
final IndexMetadata meta = tableInfo.getIndexMetadata( uniqueKey.getName() );
if ( meta != null ) {
continue;
if (! constraintMethod.equals( UniqueConstraintSchemaUpdateStrategy.SKIP )) {
Iterator uniqueIter = table.getUniqueKeyIterator();
while ( uniqueIter.hasNext() ) {
final UniqueKey uniqueKey = (UniqueKey) uniqueIter.next();
// Skip if index already exists. Most of the time, this
// won't work since most Dialects use Constraints. However,
// keep it for the few that do use Indexes.
if ( tableInfo != null && StringHelper.isNotEmpty( uniqueKey.getName() ) ) {
final IndexMetadata meta = tableInfo.getIndexMetadata( uniqueKey.getName() );
if ( meta != null ) {
continue;
}
}
String constraintString = uniqueKey.sqlCreateString( dialect, mapping, tableCatalog, tableSchema );
if ( constraintString != null && !constraintString.isEmpty() )
if ( constraintMethod.equals( UniqueConstraintSchemaUpdateStrategy.DROP_RECREATE_QUIETLY ) ) {
String constraintDropString = uniqueKey.sqlDropString( dialect, tableCatalog, tableCatalog );
scripts.add( new SchemaUpdateScript( constraintDropString, true) );
}
scripts.add( new SchemaUpdateScript( constraintString, true) );
}
String constraintString = uniqueKey.sqlCreateString( dialect,
mapping, tableCatalog, tableSchema );
if (constraintString != null) script.add( constraintString );
}
if ( dialect.hasAlterTable() ) {
@ -1209,22 +1220,12 @@ public class Configuration implements Serializable {
while ( subIter.hasNext() ) {
ForeignKey fk = (ForeignKey) subIter.next();
if ( fk.isPhysicalConstraint() ) {
boolean create = tableInfo == null || (
tableInfo.getForeignKeyMetadata( fk ) == null && (
//Icky workaround for MySQL bug:
!( dialect instanceof MySQLDialect ) ||
tableInfo.getIndexMetadata( fk.getName() ) == null
)
);
boolean create = tableInfo == null || ( tableInfo.getForeignKeyMetadata( fk ) == null && (
// Icky workaround for MySQL bug:
!( dialect instanceof MySQLDialect ) || tableInfo.getIndexMetadata( fk.getName() ) == null ) );
if ( create ) {
script.add(
fk.sqlCreateString(
dialect,
mapping,
tableCatalog,
tableSchema
)
);
scripts.add( new SchemaUpdateScript( fk.sqlCreateString( dialect, mapping,
tableCatalog, tableSchema ), false ) );
}
}
}
@ -1240,14 +1241,8 @@ public class Configuration implements Serializable {
continue;
}
}
script.add(
index.sqlCreateString(
dialect,
mapping,
tableCatalog,
tableSchema
)
);
scripts.add( new SchemaUpdateScript( index.sqlCreateString( dialect, mapping, tableCatalog,
tableSchema ), false ) );
}
}
}
@ -1258,11 +1253,11 @@ public class Configuration implements Serializable {
Object key = generator.generatorKey();
if ( !databaseMetadata.isSequence( key ) && !databaseMetadata.isTable( key ) ) {
String[] lines = generator.sqlCreateStrings( dialect );
script.addAll( Arrays.asList( lines ) );
scripts.addAll( SchemaUpdateScript.fromStringArray( lines, false ) );
}
}
return ArrayHelper.toStringArray( script );
return scripts;
}
public void validateSchema(Dialect dialect, DatabaseMetadata databaseMetadata)throws HibernateException {

View File

@ -49,8 +49,6 @@ import org.jboss.logging.Logger;
/**
* JDBC database metadata
* @author Christoph Sturm, Teodor Danciu
*/
/**
* @author Brett Meyer
*/
public class DatabaseMetadata {

View File

@ -209,9 +209,9 @@ public class SchemaUpdate {
outputFileWriter = new FileWriter( outputFile );
}
String[] sqlStrings = configuration.generateSchemaUpdateScript( dialect, meta );
for ( String sql : sqlStrings ) {
String formatted = formatter.format( sql );
List<SchemaUpdateScript> scripts = configuration.generateSchemaUpdateScriptList( dialect, meta );
for ( SchemaUpdateScript script : scripts ) {
String formatted = formatter.format( script.getScript() );
try {
if ( delimiter != null ) {
formatted += delimiter;
@ -223,17 +223,19 @@ public class SchemaUpdate {
outputFileWriter.write( formatted + "\n" );
}
if ( target.doExport() ) {
LOG.debug( sql );
LOG.debug( script.getScript() );
stmt.executeUpdate( formatted );
}
}
catch ( SQLException e ) {
if ( haltOnError ) {
throw new JDBCException( "Error during DDL export", e );
if (!script.isQuiet()) {
if ( haltOnError ) {
throw new JDBCException( "Error during DDL export", e );
}
exceptions.add( e );
LOG.unsuccessful(script.getScript());
LOG.error(e.getMessage());
}
exceptions.add( e );
LOG.unsuccessful(sql);
LOG.error(e.getMessage());
}
}

View File

@ -0,0 +1,66 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* JBoss, Home of Professional Open Source
* Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @authors tag. All rights reserved.
* See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This copyrighted material is made available to anyone wishing to use,
* modify, copy, or redistribute it subject to the terms and conditions
* of the GNU Lesser General Public License, v. 2.1.
* This program is distributed in the hope that it will be useful, but WITHOUT A
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public License,
* v.2.1 along with this distribution; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.hibernate.tool.hbm2ddl;
import java.util.ArrayList;
import java.util.List;
/**
* Pairs a SchemaUpdate SQL script with the boolean 'quiet'. If true, it allows
* the script to be run, ignoring all exceptions.
*
* @author Brett Meyer
*/
public class SchemaUpdateScript {
private final String script;
private final boolean quiet;
public SchemaUpdateScript(String script, boolean quiet) {
this.script = script;
this.quiet = quiet;
}
public String getScript() {
return script;
}
public boolean isQuiet() {
return quiet;
}
public static String[] toStringArray(List<SchemaUpdateScript> scripts) {
String[] scriptsArray = new String[scripts.size()];
for (int i = 0; i < scripts.size(); i++) {
scriptsArray[i] = scripts.get( i ).getScript();
}
return scriptsArray;
}
public static List<SchemaUpdateScript> fromStringArray(String[] scriptsArray, boolean quiet) {
List<SchemaUpdateScript> scripts = new ArrayList<SchemaUpdateScript>();
for (String script : scriptsArray) {
scripts.add( new SchemaUpdateScript( script, quiet ) );
}
return scripts;
}
}

View File

@ -0,0 +1,93 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* JBoss, Home of Professional Open Source
* Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @authors tag. All rights reserved.
* See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This copyrighted material is made available to anyone wishing to use,
* modify, copy, or redistribute it subject to the terms and conditions
* of the GNU Lesser General Public License, v. 2.1.
* This program is distributed in the hope that it will be useful, but WITHOUT A
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public License,
* v.2.1 along with this distribution; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.hibernate.tool.hbm2ddl;
import org.jboss.logging.Logger;
/**
* Unique columns and unique keys both use unique constraints in most dialects.
* SchemaUpdate needs to create these constraints, but DB's
* support for finding existing constraints is extremely inconsistent. Further,
* non-explicitly-named unique constraints use randomly generated characters.
*
* Therefore, allow users to select from these strategies.
* {@link #RECREATE_QUIETLY} (DEFAULT): attempt to (re-)create all unique constraints,
* ignoring exceptions throw if the constraint already existed
* {@link #SKIP}: do not attempt to create unique constraints on a schema update
*
* @author Brett Meyer
*/
public enum UniqueConstraintSchemaUpdateStrategy {
/**
* Attempt to drop, then (re-)create each unique constraint.
* Ignore any exceptions thrown. Note
* that this will require unique keys/constraints to be explicitly named.
* If Hibernate generates the names (randomly), the drop will not work.
*
* DEFAULT
*/
DROP_RECREATE_QUIETLY,
/**
* Attempt to (re-)create unique constraints,
* ignoring exceptions thrown if the constraint already existed
*/
RECREATE_QUIETLY,
/**
* Do not attempt to create unique constraints on a schema update
*/
SKIP;
private static final Logger log = Logger.getLogger( UniqueConstraintSchemaUpdateStrategy.class );
public static UniqueConstraintSchemaUpdateStrategy byName(String name) {
return valueOf( name.toUpperCase() );
}
public static UniqueConstraintSchemaUpdateStrategy interpret(Object setting) {
log.tracef( "Interpreting UniqueConstraintSchemaUpdateStrategy from setting : %s", setting );
if ( setting == null ) {
// default
return DROP_RECREATE_QUIETLY;
}
if ( UniqueConstraintSchemaUpdateStrategy.class.isInstance( setting ) ) {
return (UniqueConstraintSchemaUpdateStrategy) setting;
}
try {
final UniqueConstraintSchemaUpdateStrategy byName = byName( setting.toString() );
if ( byName != null ) {
return byName;
}
}
catch ( Exception ignore ) {
}
log.debugf( "Unable to interpret given setting [%s] as UniqueConstraintSchemaUpdateStrategy", setting );
// default
return DROP_RECREATE_QUIETLY;
}
}

View File

@ -20,7 +20,6 @@
*/
package org.hibernate.test.annotations.join;
import org.hibernate.testing.FailureExpected;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;
@ -29,14 +28,16 @@ import org.junit.Test;
* @author Brett Meyer
*/
@TestForIssue( jiraKey = "HHH-2872" )
@FailureExpected( jiraKey = "HHH-2872" )
public class JoinOrderingTest extends BaseCoreFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
// This is the important piece. ProductDetails must be first to
// reproduce the issue.
return new Class<?>[] { ProductDetails.class, Product.class, ProductVersion.class };
// return new Class<?>[] { ProductDetails.class, Product.class, ProductVersion.class };
// TODO: commented out -- @FailureExpected wasn't working on builds
// if it's a MappingException.
return new Class<?>[] { };
}
@Test

View File

@ -0,0 +1,24 @@
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="org.hibernate.test.schemaupdate">
<class name="Version">
<id name="id">
<generator class="org.hibernate.id.TableHiLoGenerator">
<param name="table">uid_table</param>
<param name="column">next_hi_value_column</param>
</generator>
</id>
<!-- named unique constraint -->
<property name="description" unique-key="descriptionUK"/>
<!-- un-named unique constraint (force random name generation) -->
<properties name="nameUK" unique="true">
<property name="name"/>
</properties>
</class>
</hibernate-mapping>

View File

@ -23,23 +23,26 @@
*/
package org.hibernate.test.schemaupdate;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.testing.ServiceRegistryBuilder;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseUnitTestCase;
import org.hibernate.tool.hbm2ddl.UniqueConstraintSchemaUpdateStrategy;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.hibernate.tool.hbm2ddl.SchemaUpdate;
import static org.junit.Assert.assertEquals;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/**
* @author Max Rydahl Andersen
* @author Brett Meyer
*/
public class MigrationTest extends BaseUnitTestCase {
private ServiceRegistry serviceRegistry;
@ -83,6 +86,49 @@ public class MigrationTest extends BaseUnitTestCase {
new SchemaExport( serviceRegistry, v2cfg ).drop( false, true );
}
/**
* 3_Version.hbm.xml contains a named unique constraint and an un-named
* unique constraint (will receive a randomly-generated name). Create
* the original schema with 2_Version.hbm.xml. Then, run SchemaUpdate
* TWICE using 3_Version.hbm.xml. Neither RECREATE_QUIETLY nor SKIP should
* generate any exceptions.
*/
@Test
@TestForIssue( jiraKey = "HHH-8162" )
public void testConstraintUpdate() {
doConstraintUpdate(UniqueConstraintSchemaUpdateStrategy.DROP_RECREATE_QUIETLY);
doConstraintUpdate(UniqueConstraintSchemaUpdateStrategy.RECREATE_QUIETLY);
doConstraintUpdate(UniqueConstraintSchemaUpdateStrategy.SKIP);
}
private void doConstraintUpdate(UniqueConstraintSchemaUpdateStrategy strategy) {
// original
String resource1 = "org/hibernate/test/schemaupdate/2_Version.hbm.xml";
// adds unique constraint
String resource2 = "org/hibernate/test/schemaupdate/3_Version.hbm.xml";
Configuration v1cfg = new Configuration();
v1cfg.addResource( resource1 );
new SchemaExport( v1cfg ).execute( false, true, true, false );
// adds unique constraint
Configuration v2cfg = new Configuration();
v2cfg.getProperties().put( AvailableSettings.UNIQUE_CONSTRAINT_SCHEMA_UPDATE_STRATEGY, strategy );
v2cfg.addResource( resource2 );
SchemaUpdate v2schemaUpdate = new SchemaUpdate( serviceRegistry, v2cfg );
v2schemaUpdate.execute( true, true );
assertEquals( 0, v2schemaUpdate.getExceptions().size() );
Configuration v3cfg = new Configuration();
v3cfg.getProperties().put( AvailableSettings.UNIQUE_CONSTRAINT_SCHEMA_UPDATE_STRATEGY, strategy );
v3cfg.addResource( resource2 );
SchemaUpdate v3schemaUpdate = new SchemaUpdate( serviceRegistry, v3cfg );
v3schemaUpdate.execute( true, true );
assertEquals( 0, v3schemaUpdate.getExceptions().size() );
new SchemaExport( serviceRegistry, v3cfg ).drop( false, true );
}
}