From c0d5fe91532dd65960e2ae348fc1ebefa03549f4 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Sat, 14 Jan 2017 17:00:48 +0000 Subject: [PATCH] HHH-11236 - JPA hbm2ddl auto-generation creates ddl with invalid syntax for Unique Key with any MySQLDialect --- .../org/hibernate/dialect/MySQLDialect.java | 11 ++ .../dialect/unique/DefaultUniqueDelegate.java | 6 +- .../dialect/unique/MySQLUniqueDelegate.java | 32 ++++ ...ySQLDropConstraintThrowsExceptionTest.java | 121 ++++++++++++ .../UniqueConstraintDropTest.java | 180 ++++++++++++++++++ ...reparedStatementSpyConnectionProvider.java | 36 ++++ 6 files changed, 385 insertions(+), 1 deletion(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/unique/MySQLUniqueDelegate.java create mode 100644 hibernate-core/src/test/java/org/hibernate/test/annotations/uniqueconstraint/MySQLDropConstraintThrowsExceptionTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/test/schemaupdate/uniqueconstraint/UniqueConstraintDropTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index 43c0bee1d4..3bcc894035 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -22,6 +22,8 @@ import org.hibernate.dialect.identity.MySQLIdentityColumnSupport; import org.hibernate.dialect.pagination.AbstractLimitHandler; import org.hibernate.dialect.pagination.LimitHandler; import org.hibernate.dialect.pagination.LimitHelper; +import org.hibernate.dialect.unique.MySQLUniqueDelegate; +import org.hibernate.dialect.unique.UniqueDelegate; import org.hibernate.engine.spi.RowSelection; import org.hibernate.exception.LockAcquisitionException; import org.hibernate.exception.LockTimeoutException; @@ -43,6 +45,8 @@ import org.hibernate.type.StandardBasicTypes; @SuppressWarnings("deprecation") public class MySQLDialect extends Dialect { + private final UniqueDelegate uniqueDelegate; + private static final LimitHandler LIMIT_HANDLER = new AbstractLimitHandler() { @Override public String processSql(String sql, RowSelection selection) { @@ -196,6 +200,8 @@ public class MySQLDialect extends Dialect { getDefaultProperties().setProperty( Environment.MAX_FETCH_DEPTH, "2" ); getDefaultProperties().setProperty( Environment.STATEMENT_BATCH_SIZE, DEFAULT_BATCH_SIZE ); + + uniqueDelegate = new MySQLUniqueDelegate( this ); } protected void registerVarcharTypes() { @@ -422,6 +428,11 @@ public class MySQLDialect extends Dialect { return ps.getResultSet(); } + @Override + public UniqueDelegate getUniqueDelegate() { + return uniqueDelegate; + } + @Override public boolean supportsRowValueConstructorSyntax() { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/unique/DefaultUniqueDelegate.java b/hibernate-core/src/main/java/org/hibernate/dialect/unique/DefaultUniqueDelegate.java index 19fc74ac0c..585a74482b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/unique/DefaultUniqueDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/unique/DefaultUniqueDelegate.java @@ -85,7 +85,7 @@ public class DefaultUniqueDelegate implements UniqueDelegate { final StringBuilder buf = new StringBuilder( "alter table " ); buf.append( tableName ); - buf.append(" drop constraint " ); + buf.append( getDropUnique() ); if ( dialect.supportsIfExistsBeforeConstraintName() ) { buf.append( "if exists " ); } @@ -96,4 +96,8 @@ public class DefaultUniqueDelegate implements UniqueDelegate { return buf.toString(); } + protected String getDropUnique(){ + return " drop constraint "; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/unique/MySQLUniqueDelegate.java b/hibernate-core/src/main/java/org/hibernate/dialect/unique/MySQLUniqueDelegate.java new file mode 100644 index 0000000000..751b54e122 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/unique/MySQLUniqueDelegate.java @@ -0,0 +1,32 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.dialect.unique; + +import org.hibernate.boot.Metadata; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.mapping.UniqueKey; + +/** + * @author Andrea Boriero + */ +public class MySQLUniqueDelegate extends DefaultUniqueDelegate { + + /** + * Constructs MySQLUniqueDelegate + * + * @param dialect The dialect for which we are handling unique constraints + */ + public MySQLUniqueDelegate(Dialect dialect) { + super( dialect ); + } + + @Override + protected String getDropUnique() { + return " drop index "; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/annotations/uniqueconstraint/MySQLDropConstraintThrowsExceptionTest.java b/hibernate-core/src/test/java/org/hibernate/test/annotations/uniqueconstraint/MySQLDropConstraintThrowsExceptionTest.java new file mode 100644 index 0000000000..f39328e4ef --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/annotations/uniqueconstraint/MySQLDropConstraintThrowsExceptionTest.java @@ -0,0 +1,121 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.annotations.uniqueconstraint; + +import java.util.List; +import java.util.stream.Collectors; +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.hibernate.boot.Metadata; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.MySQL5InnoDBDialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; + +import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseUnitTestCase; +import org.hibernate.test.util.jdbc.PreparedStatementSpyConnectionProvider; +import org.junit.After; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +@TestForIssue(jiraKey = "HHH-11236") +@RequiresDialect(MySQL5InnoDBDialect.class) +public class MySQLDropConstraintThrowsExceptionTest extends BaseUnitTestCase { + + @After + public void releaseResources() { + + } + + @Test + public void testEnumTypeInterpretation() { + StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder() + .enableAutoClose() + .applySetting( AvailableSettings.HBM2DDL_AUTO, "drop" ) + .build(); + + SessionFactoryImplementor sessionFactory = null; + + try { + final Metadata metadata = new MetadataSources( serviceRegistry ) + .addAnnotatedClass( Customer.class ) + .buildMetadata(); + sessionFactory = (SessionFactoryImplementor) metadata.buildSessionFactory(); + } + finally { + if ( sessionFactory != null ) { + sessionFactory.close(); + } + StandardServiceRegistryBuilder.destroy( serviceRegistry ); + } + + PreparedStatementSpyConnectionProvider connectionProvider = new PreparedStatementSpyConnectionProvider(); + + serviceRegistry = new StandardServiceRegistryBuilder() + .enableAutoClose() + .applySetting( AvailableSettings.HBM2DDL_AUTO, "update" ) + .applySetting( + AvailableSettings.CONNECTION_PROVIDER, + connectionProvider + ) + .build(); + + try { + final Metadata metadata = new MetadataSources( serviceRegistry ) + .addAnnotatedClass( Customer.class ) + .buildMetadata(); + sessionFactory = (SessionFactoryImplementor) metadata.buildSessionFactory(); + List alterStatements = connectionProvider.getExecuteStatements().stream() + .filter( + sql -> sql.toLowerCase().contains( "alter " ) + ).map( String::trim ).collect( Collectors.toList() ); + assertTrue(alterStatements.get(0).matches( "alter table CUSTOMER\\s+drop index .*?" )); + assertTrue(alterStatements.get(1).matches( "alter table CUSTOMER\\s+add constraint .*? unique \\(CUSTOMER_ID\\)" )); + } + finally { + if ( sessionFactory != null ) { + sessionFactory.close(); + } + StandardServiceRegistryBuilder.destroy( serviceRegistry ); + } + } + + @Entity + @Table(name = "CUSTOMER") + public static class Customer { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "CUSTOMER_ACCOUNT_NUMBER") + public Long customerAccountNumber; + + @Basic + @Column(name = "CUSTOMER_ID", unique = true) + public String customerId; + + @Basic + @Column(name = "BILLING_ADDRESS") + public String billingAddress; + + public Customer() { + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/schemaupdate/uniqueconstraint/UniqueConstraintDropTest.java b/hibernate-core/src/test/java/org/hibernate/test/schemaupdate/uniqueconstraint/UniqueConstraintDropTest.java new file mode 100644 index 0000000000..bb28c44ae7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/schemaupdate/uniqueconstraint/UniqueConstraintDropTest.java @@ -0,0 +1,180 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.schemaupdate.uniqueconstraint; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.EnumSet; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.cfg.Environment; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.engine.config.spi.ConfigurationService; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.tool.schema.TargetType; +import org.hibernate.tool.schema.internal.DefaultSchemaFilter; +import org.hibernate.tool.schema.internal.ExceptionHandlerLoggedImpl; +import org.hibernate.tool.schema.internal.HibernateSchemaManagementTool; +import org.hibernate.tool.schema.internal.IndividuallySchemaMigratorImpl; +import org.hibernate.tool.schema.internal.exec.ScriptTargetOutputToFile; +import org.hibernate.tool.schema.spi.ExceptionHandler; +import org.hibernate.tool.schema.spi.ExecutionOptions; +import org.hibernate.tool.schema.spi.SchemaManagementTool; +import org.hibernate.tool.schema.spi.ScriptTargetOutput; +import org.hibernate.tool.schema.spi.TargetDescriptor; + +import org.hibernate.testing.TestForIssue; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +/** + * @author Andrea Boriero + */ +public class UniqueConstraintDropTest { + private File output; + private MetadataImplementor metadata; + private StandardServiceRegistry ssr; + private HibernateSchemaManagementTool tool; + private ExecutionOptions options; + + @Before + public void setUp() throws Exception { + output = File.createTempFile( "update_script", ".sql" ); + output.deleteOnExit(); + ssr = new StandardServiceRegistryBuilder() + .applySetting( Environment.HBM2DDL_AUTO, "none" ) + .applySetting( Environment.FORMAT_SQL, "false" ) + .applySetting( Environment.SHOW_SQL, "true" ) + .build(); + metadata = (MetadataImplementor) new MetadataSources( ssr ) + .addResource( "org/hibernate/test/schemaupdate/uniqueconstraint/TestEntity.hbm.xml" ) + .buildMetadata(); + metadata.validate(); + tool = (HibernateSchemaManagementTool) ssr.getService( SchemaManagementTool.class ); + + final Map configurationValues = ssr.getService( ConfigurationService.class ).getSettings(); + options = new ExecutionOptions() { + @Override + public boolean shouldManageNamespaces() { + return true; + } + + @Override + public Map getConfigurationValues() { + return configurationValues; + } + + @Override + public ExceptionHandler getExceptionHandler() { + return ExceptionHandlerLoggedImpl.INSTANCE; + } + }; + } + + @After + public void tearDown() { + StandardServiceRegistryBuilder.destroy( ssr ); + } + + @Test + @TestForIssue(jiraKey = "HHH-11236") + public void testUniqueConstraintIsGenerated() throws Exception { + + new IndividuallySchemaMigratorImpl( tool, DefaultSchemaFilter.INSTANCE ) + .doMigration( + metadata, + options, + new TargetDescriptorImpl() + ); + + if ( getDialect() instanceof MySQLDialect ) { + assertThat( + "The test_entity_item table unique constraint has not been dropped", + checkDropIndex( "test_entity_item", "item" ), + is( true ) + ); + } + else { + assertThat( + "The test_entity_item table unique constraint has not been dropped", + checkDropConstraint( "test_entity_item", "item" ), + is( true ) + ); + } + } + + protected Dialect getDialect() { + return ssr.getService( JdbcEnvironment.class ).getDialect(); + } + + private boolean checkDropConstraint(String tableName, String columnName) throws IOException { + boolean matches = false; + String regex = "alter table " + tableName + " drop constraint"; + + if ( getDialect().supportsIfExistsBeforeConstraintName() ) { + regex += " if exists"; + } + regex += " uk_(.)*"; + if ( getDialect().supportsIfExistsAfterConstraintName() ) { + regex += " if exists"; + } + + return isMatching( matches, regex ); + } + + private boolean checkDropIndex(String tableName, String columnName) throws IOException { + boolean matches = false; + String regex = "alter table " + tableName + " drop index"; + + if ( getDialect().supportsIfExistsBeforeConstraintName() ) { + regex += " if exists"; + } + regex += " uk_(.)*"; + if ( getDialect().supportsIfExistsAfterConstraintName() ) { + regex += " if exists"; + } + + return isMatching( matches, regex ); + } + + private boolean isMatching(boolean matches, String regex) throws IOException { + final String fileContent = new String( Files.readAllBytes( output.toPath() ) ).toLowerCase(); + final String[] split = fileContent.split( System.lineSeparator() ); + Pattern p = Pattern.compile( regex ); + for ( String line : split ) { + final Matcher matcher = p.matcher( line ); + if ( matcher.matches() ) { + matches = true; + } + } + return matches; + } + + private class TargetDescriptorImpl implements TargetDescriptor { + public EnumSet getTargetTypes() { + return EnumSet.of( TargetType.SCRIPT ); + } + + @Override + public ScriptTargetOutput getScriptTargetOutput() { + return new ScriptTargetOutputToFile( output, Charset.defaultCharset().name() ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/util/jdbc/PreparedStatementSpyConnectionProvider.java b/hibernate-core/src/test/java/org/hibernate/test/util/jdbc/PreparedStatementSpyConnectionProvider.java index f6d32bd5c3..42a8dc690f 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/util/jdbc/PreparedStatementSpyConnectionProvider.java +++ b/hibernate-core/src/test/java/org/hibernate/test/util/jdbc/PreparedStatementSpyConnectionProvider.java @@ -9,6 +9,7 @@ package org.hibernate.test.util.jdbc; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.sql.Statement; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -34,6 +35,9 @@ public class PreparedStatementSpyConnectionProvider private final Map preparedStatementMap = new LinkedHashMap<>(); + private final List executeStatements = new ArrayList<>(); + private final List executeUpdateStatements = new ArrayList<>(); + private final List acquiredConnections = new ArrayList<>( ); @Override @@ -62,6 +66,22 @@ public class PreparedStatementSpyConnectionProvider preparedStatementMap.put( statementSpy, sql ); return statementSpy; } ).when( connectionSpy ).prepareStatement( anyString() ); + + doAnswer( invocation -> { + Statement statement = (Statement) invocation.callRealMethod(); + Statement statementSpy = Mockito.spy( statement ); + doAnswer( statementInvocation -> { + String sql = (String) statementInvocation.getArguments()[0]; + executeStatements.add( sql ); + return statementInvocation.callRealMethod(); + }).when( statementSpy ).execute( anyString() ); + doAnswer( statementInvocation -> { + String sql = (String) statementInvocation.getArguments()[0]; + executeUpdateStatements.add( sql ); + return statementInvocation.callRealMethod(); + }).when( statementSpy ).executeUpdate( anyString() ); + return statementSpy; + } ).when( connectionSpy ).createStatement(); } catch ( SQLException e ) { throw new IllegalArgumentException( e ); @@ -124,6 +144,22 @@ public class PreparedStatementSpyConnectionProvider return new ArrayList<>( preparedStatementMap.keySet() ); } + /** + * Get the SQL statements that were executed since the last clear operation. + * @return list of recorded update statements. + */ + public List getExecuteStatements() { + return executeStatements; + } + + /** + * Get the SQL update statements that were executed since the last clear operation. + * @return list of recorded update statements. + */ + public List getExecuteUpdateStatements() { + return executeUpdateStatements; + } + /** * Get a list of current acquired Connections. * @return list of current acquired Connections