reenable check constraings on enum columns, and use MySQL enum column types

MySQL doesn't have real check constraints, but it does have something just as good for this special case
This commit is contained in:
Gavin 2022-12-09 22:33:56 +01:00 committed by Gavin King
parent 7208bcea41
commit baffbc0aae
11 changed files with 277 additions and 64 deletions

View File

@ -668,6 +668,10 @@ public abstract class Dialect implements ConversionContext {
}
}
public String getEnumTypeDeclaration(int sqlType, Class<? extends Enum<?>> enumClass) {
return null;
}
/**
* Render a SQL check condition for a column that represents an enumerated value.
*/
@ -2268,8 +2272,8 @@ public abstract class Dialect implements ConversionContext {
*
* @return The appropriate empty values clause.
*
* @deprecated Override {@link org.hibernate.sql.ast.spi.AbstractSqlAstTranslator#renderInsertIntoNoColumns}
* on the {@link #getSqlAstTranslatorFactory() translator} returned by this dialect
* @deprecated Override the method {@code renderInsertIntoNoColumns()} on the
* {@link #getSqlAstTranslatorFactory() translator} returned by this dialect
*/
@Deprecated( since = "6" )
public String getNoColumnsInsertString() {

View File

@ -368,11 +368,6 @@ public class HSQLDialect extends Dialect {
return true;
}
@Override
public boolean supportsColumnCheck() {
return true;
}
@Override
public SequenceSupport getSequenceSupport() {
return HSQLSequenceSupport.INSTANCE;

View File

@ -77,6 +77,8 @@ import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry;
import jakarta.persistence.TemporalType;
import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate;
import static org.hibernate.type.SqlTypes.isCharacterType;
import static org.hibernate.type.SqlTypes.isIntegral;
import static org.hibernate.type.SqlTypes.BIGINT;
import static org.hibernate.type.SqlTypes.BINARY;
import static org.hibernate.type.SqlTypes.BIT;
@ -752,6 +754,23 @@ public class MySQLDialect extends Dialect {
return false;
}
@Override
public String getEnumTypeDeclaration(int sqlType, Class<? extends Enum<?>> enumClass) {
if ( isCharacterType(sqlType) || isIntegral(sqlType) ) {
StringBuilder type = new StringBuilder();
type.append( "enum (" );
String separator = "";
for ( Enum<?> value : enumClass.getEnumConstants() ) {
type.append( separator ).append('\'').append( value.name() ).append('\'');
separator = ",";
}
return type.append( ')' ).toString();
}
else {
return null;
}
}
@Override
public String getQueryHintString(String query, String hints) {
return IndexQueryHintHandler.INSTANCE.addQueryHints( query, hints );

View File

@ -317,21 +317,26 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
throw new IllegalStateException( "Unable to resolve BasicValue : " + this );
}
final Selectable column = getColumn();
if ( column instanceof Column && resolution.getValueConverter() == null ) {
final Column physicalColumn = (Column) column;
if ( physicalColumn.getSqlTypeCode() == null ) {
physicalColumn.setSqlTypeCode( resolution.getJdbcType().getDefaultSqlTypeCode() );
final Selectable selectable = getColumn();
if ( selectable instanceof Column && resolution.getValueConverter() == null ) {
final Column column = (Column) selectable;
if ( column.getSqlTypeCode() == null ) {
column.setSqlTypeCode( resolution.getJdbcType().getDefaultSqlTypeCode() );
}
final BasicType<?> basicType = resolution.getLegacyResolvedBasicType();
final Dialect dialect = getServiceRegistry().getService( JdbcServices.class ).getDialect();
final String checkConstraint = physicalColumn.getCheckConstraint();
if ( checkConstraint == null && dialect.supportsColumnCheck() ) {
physicalColumn.setCheckConstraint(
basicType.getJavaTypeDescriptor().getCheckCondition(
physicalColumn.getQuotedName( dialect ),
basicType.getJdbcType(),
column.setSpecializedTypeDeclaration(
resolution.getDomainJavaType().getSpecializedTypeDeclaration(
resolution.getJdbcType(),
dialect
)
);
if ( dialect.supportsColumnCheck() && !column.hasCheckConstraint() ) {
column.setCheckConstraint(
resolution.getDomainJavaType().getCheckCondition(
column.getQuotedName(dialect),
resolution.getJdbcType(),
dialect
)
);

View File

@ -61,6 +61,7 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn
private String customWrite;
private String customRead;
private Size columnSize;
private String specializedTypeDeclaration;
public Column() {
}
@ -431,6 +432,18 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn
return getClass().getSimpleName() + '(' + getName() + ')';
}
public void setSpecializedTypeDeclaration(String specializedTypeDeclaration) {
this.specializedTypeDeclaration = specializedTypeDeclaration;
}
public String getSpecializedTypeDeclaration() {
return specializedTypeDeclaration;
}
public boolean hasSpecializedTypeDeclaration() {
return specializedTypeDeclaration != null;
}
public String getCheckConstraint() {
return checkConstraint;
}
@ -444,7 +457,7 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn
}
public String checkConstraint() {
if (checkConstraint==null) {
if ( checkConstraint == null ) {
return null;
}
return " check (" + checkConstraint + ")";
@ -540,7 +553,6 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn
this.generatedAs = generatedAs;
}
public String getCustomWrite() {
return customWrite;
}

View File

@ -461,8 +461,11 @@ public class Table implements Serializable, ContributableDatabaseObject {
dialect,
metadata
);
if ( column.getGeneratedAs()==null || dialect.hasDataTypeBeforeGeneratedAs() ) {
alter.append( ' ' ).append( columnType );
if ( column.hasSpecializedTypeDeclaration() ) {
alter.append( ' ' ).append( column.getSpecializedTypeDeclaration() );
}
else if ( column.getGeneratedAs() == null || dialect.hasDataTypeBeforeGeneratedAs() ) {
alter.append(' ').append(columnType);
}
final String defaultValue = column.getDefaultValue();
@ -490,9 +493,8 @@ public class Table implements Serializable, ContributableDatabaseObject {
.getColumnDefinitionUniquenessFragment( column, sqlStringGenerationContext ) );
}
final String checkConstraint = column.checkConstraint();
if ( checkConstraint !=null && dialect.supportsColumnCheck() ) {
alter.append( checkConstraint );
if ( column.hasCheckConstraint() && dialect.supportsColumnCheck() ) {
alter.append( column.checkConstraint() );
}
final String columnComment = column.getComment();

View File

@ -52,7 +52,7 @@ public class StandardTableExporter implements Exporter<Table> {
try {
String formattedTableName = context.format( tableName );
StringBuilder buf =
StringBuilder createTable =
new StringBuilder( tableCreateString( table.hasPrimaryKey() ) )
.append( ' ' )
.append( formattedTableName )
@ -78,96 +78,98 @@ public class StandardTableExporter implements Exporter<Table> {
}
boolean isFirst = true;
for ( Column col : table.getColumns() ) {
for ( Column column : table.getColumns() ) {
if ( isFirst ) {
isFirst = false;
}
else {
buf.append( ", " );
createTable.append( ", " );
}
String colName = col.getQuotedName( dialect );
buf.append( colName );
String colName = column.getQuotedName( dialect );
createTable.append( colName );
if ( isPrimaryKeyIdentity && colName.equals( pkColName ) ) {
// to support dialects that have their own identity data type
if ( dialect.getIdentityColumnSupport().hasDataTypeInIdentityColumn() ) {
buf.append( ' ' ).append(
col.getSqlType( metadata.getDatabase().getTypeConfiguration(), dialect, metadata )
createTable.append( ' ' ).append(
column.getSqlType( metadata.getDatabase().getTypeConfiguration(), dialect, metadata )
);
}
String identityColumnString = dialect.getIdentityColumnSupport()
.getIdentityColumnString( col.getSqlTypeCode(metadata) );
buf.append( ' ' ).append( identityColumnString );
.getIdentityColumnString( column.getSqlTypeCode(metadata) );
createTable.append( ' ' ).append( identityColumnString );
}
else {
final String columnType = col.getSqlType(
final String columnType = column.getSqlType(
metadata.getDatabase().getTypeConfiguration(),
dialect,
metadata
);
if ( col.getGeneratedAs()==null || dialect.hasDataTypeBeforeGeneratedAs() ) {
buf.append( ' ' ).append( columnType );
if ( column.hasSpecializedTypeDeclaration() ) {
createTable.append( ' ' ).append( column.getSpecializedTypeDeclaration() );
}
else if ( column.getGeneratedAs() == null || dialect.hasDataTypeBeforeGeneratedAs() ) {
createTable.append( ' ' ).append( columnType );
}
String defaultValue = col.getDefaultValue();
String defaultValue = column.getDefaultValue();
if ( defaultValue != null ) {
buf.append( " default " ).append( defaultValue );
createTable.append( " default " ).append( defaultValue );
}
String generatedAs = col.getGeneratedAs();
String generatedAs = column.getGeneratedAs();
if ( generatedAs != null) {
buf.append( dialect.generatedAs( generatedAs ) );
createTable.append( dialect.generatedAs( generatedAs ) );
}
if ( col.isNullable() ) {
buf.append( dialect.getNullColumnString( columnType ) );
if ( column.isNullable() ) {
createTable.append( dialect.getNullColumnString( columnType ) );
}
else {
buf.append( " not null" );
createTable.append( " not null" );
}
}
if ( col.isUnique() && !table.isPrimaryKey( col ) ) {
String keyName = Constraint.generateName( "UK_", table, col );
if ( column.isUnique() && !table.isPrimaryKey( column ) ) {
String keyName = Constraint.generateName( "UK_", table, column );
UniqueKey uk = table.getOrCreateUniqueKey( keyName );
uk.addColumn( col );
buf.append(
uk.addColumn( column );
createTable.append(
dialect.getUniqueDelegate()
.getColumnDefinitionUniquenessFragment( col, context )
.getColumnDefinitionUniquenessFragment( column, context )
);
}
String checkConstraint = col.checkConstraint();
if ( checkConstraint != null && dialect.supportsColumnCheck() ) {
buf.append( checkConstraint );
if ( dialect.supportsColumnCheck() && column.hasCheckConstraint() ) {
createTable.append( column.checkConstraint() );
}
String columnComment = col.getComment();
String columnComment = column.getComment();
if ( columnComment != null ) {
buf.append( dialect.getColumnComment( columnComment ) );
createTable.append( dialect.getColumnComment( columnComment ) );
}
}
if ( table.hasPrimaryKey() ) {
buf.append( ", " )
createTable.append( ", " )
.append( table.getPrimaryKey().sqlConstraintString( dialect ) );
}
buf.append( dialect.getUniqueDelegate().getTableCreationUniqueConstraintsFragment( table, context ) );
createTable.append( dialect.getUniqueDelegate().getTableCreationUniqueConstraintsFragment( table, context ) );
applyTableCheck( table, buf );
applyTableCheck( table, createTable );
buf.append( ')' );
createTable.append( ')' );
if ( table.getComment() != null ) {
buf.append( dialect.getTableComment( table.getComment() ) );
createTable.append( dialect.getTableComment( table.getComment() ) );
}
applyTableTypeString( buf );
applyTableTypeString( createTable );
List<String> sqlStrings = new ArrayList<>();
sqlStrings.add( buf.toString() );
sqlStrings.add( createTable.toString() );
applyComments( table, formattedTableName, sqlStrings );

View File

@ -220,4 +220,8 @@ public class EnumJavaType<T extends Enum<T>> extends AbstractClassJavaType<T> {
return dialect.getEnumCheckCondition( columnName, jdbcType.getJdbcTypeCode(), getJavaTypeClass() );
}
@Override
public String getSpecializedTypeDeclaration(JdbcType jdbcType, Dialect dialect) {
return dialect.getEnumTypeDeclaration( jdbcType.getJdbcTypeCode(), getJavaTypeClass() );
}
}

View File

@ -289,6 +289,10 @@ public interface JavaType<T> extends Serializable {
return null;
}
default String getSpecializedTypeDeclaration(JdbcType jdbcType, Dialect dialect) {
return null;
}
/**
* Creates the {@link JavaType} for the given {@link ParameterizedType}
* based on this {@link JavaType} registered for the raw type.

View File

@ -14,8 +14,6 @@ import org.hibernate.Session;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.type.descriptor.JdbcBindingLogging;
import org.hibernate.type.descriptor.JdbcExtractingLogging;
import org.hibernate.type.descriptor.jdbc.BasicBinder;
import org.hibernate.type.descriptor.jdbc.BasicExtractor;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;

View File

@ -0,0 +1,168 @@
/*
* 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 http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.orm.test.schematools;
import jakarta.persistence.Basic;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.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.Dialect;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.orm.test.tool.schema.ExecutionOptionsTestImpl;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import org.hibernate.testing.orm.junit.ServiceRegistryScope;
import org.hibernate.tool.schema.SourceType;
import org.hibernate.tool.schema.TargetType;
import org.hibernate.tool.schema.internal.HibernateSchemaManagementTool;
import org.hibernate.tool.schema.internal.exec.GenerationTarget;
import org.hibernate.tool.schema.internal.exec.JdbcContext;
import org.hibernate.tool.schema.spi.SchemaCreator;
import org.hibernate.tool.schema.spi.SchemaManagementTool;
import org.hibernate.tool.schema.spi.ScriptSourceInput;
import org.hibernate.tool.schema.spi.ScriptTargetOutput;
import org.hibernate.tool.schema.spi.SourceDescriptor;
import org.hibernate.tool.schema.spi.TargetDescriptor;
import org.junit.jupiter.api.Test;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Dialect#getFallbackSchemaManagementTool}
*
* @author Steve Ebersole
*/
public class EnumCheckTests {
@Test
public void testFallbackToolIsPickedUp() {
ServiceRegistryScope.using(
() -> {
return new StandardServiceRegistryBuilder()
.applySetting( AvailableSettings.DIALECT, CustomDialect.class.getName() )
.build();
},
(registryScope) -> {
final StandardServiceRegistry registry = registryScope.getRegistry();
final Metadata metadata = new MetadataSources( registry )
.addAnnotatedClass( SimpleEntity.class )
.buildMetadata();
final HibernateSchemaManagementTool tool = (HibernateSchemaManagementTool) registry.getService( SchemaManagementTool.class );
final Map<String, Object> settings = registry.getService( ConfigurationService.class ).getSettings();
final SchemaCreator schemaCreator = tool.getSchemaCreator( settings );
schemaCreator.doCreation(
metadata,
new ExecutionOptionsTestImpl(),
contributed -> true,
new SourceDescriptor() {
@Override
public SourceType getSourceType() {
return SourceType.METADATA;
}
@Override
public ScriptSourceInput getScriptSourceInput() {
return null;
}
},
new TargetDescriptor() {
@Override
public EnumSet<TargetType> getTargetTypes() {
return EnumSet.of( TargetType.DATABASE );
}
@Override
public ScriptTargetOutput getScriptTargetOutput() {
return null;
}
}
);
assertThat( CollectingGenerationTarget.commands.get(0).contains( "check ('SOURCE','CLASS','RUNTIME')") );
}
);
}
private static class CollectingGenerationTarget implements GenerationTarget {
public static final List<String> commands = new ArrayList<>();
public CollectingGenerationTarget(JdbcContext jdbcContext, boolean needsAutoCommit) {
}
@Override
public void prepare() {
commands.clear();
}
@Override
public void accept(String command) {
commands.add( command );
}
@Override
public void release() {
}
}
public static class SchemaManagementToolImpl extends HibernateSchemaManagementTool {
@Override
protected GenerationTarget buildDatabaseTarget(JdbcContext jdbcContext, boolean needsAutoCommit) {
return new CollectingGenerationTarget( jdbcContext, needsAutoCommit );
}
}
public static class CustomDialect extends Dialect {
@Override
public SchemaManagementTool getFallbackSchemaManagementTool(Map<String, Object> configurationValues, ServiceRegistryImplementor registry) {
return new SchemaManagementToolImpl();
}
}
@Entity( name = "SimpleEntity" )
@Table( name = "SimpleEntity" )
public static class SimpleEntity {
@Id
private Integer id;
@Basic
private String name;
@Enumerated(EnumType.STRING)
RetentionPolicy retentionPolicy;
private SimpleEntity() {
// for use by Hibernate
}
public SimpleEntity(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}