add some comments for the next poor soul who wrestles with unique constraints

This commit is contained in:
Gavin 2022-11-27 11:53:11 +01:00 committed by Gavin King
parent 5172d8798f
commit 12aa8bd431
9 changed files with 40 additions and 13 deletions

View File

@ -212,7 +212,9 @@ public class DB2LegacyDialect extends Dialect {
protected UniqueDelegate createUniqueDelegate() { protected UniqueDelegate createUniqueDelegate() {
return getDB2Version().isSameOrAfter(10,5) return getDB2Version().isSameOrAfter(10,5)
//use 'create unique index ... exclude null keys'
? new AlterTableUniqueIndexDelegate( this ) ? new AlterTableUniqueIndexDelegate( this )
//ignore unique keys on nullable columns in earlier versions
: new SkipNullableUniqueDelegate( this ); : new SkipNullableUniqueDelegate( this );
} }
@ -495,6 +497,8 @@ public class DB2LegacyDialect extends Dialect {
@Override @Override
public String getCreateIndexTail(boolean unique, List<Column> columns) { public String getCreateIndexTail(boolean unique, List<Column> columns) {
// we only create unique indexes, as opposed to unique constraints,
// when the column is nullable, so safe to infer unique => nullable
return unique ? " exclude null keys" : ""; return unique ? " exclude null keys" : "";
} }

View File

@ -73,8 +73,11 @@ public class DB2iLegacyDialect extends DB2LegacyDialect {
@Override @Override
protected UniqueDelegate createUniqueDelegate() { protected UniqueDelegate createUniqueDelegate() {
//TODO: when was 'create unique where not null index' really first introduced?
return getVersion().isSameOrAfter(7, 1) return getVersion().isSameOrAfter(7, 1)
//use 'create unique where not null index'
? new AlterTableUniqueIndexDelegate(this) ? new AlterTableUniqueIndexDelegate(this)
//ignore unique keys on nullable columns in earlier versions
: new SkipNullableUniqueDelegate(this); : new SkipNullableUniqueDelegate(this);
} }

View File

@ -88,8 +88,11 @@ public class DB2zLegacyDialect extends DB2LegacyDialect {
@Override @Override
protected UniqueDelegate createUniqueDelegate() { protected UniqueDelegate createUniqueDelegate() {
//TODO: when was 'create unique where not null index' really first introduced?
return getVersion().isSameOrAfter(11) return getVersion().isSameOrAfter(11)
//use 'create unique where not null index'
? new AlterTableUniqueIndexDelegate(this) ? new AlterTableUniqueIndexDelegate(this)
//ignore unique keys on nullable columns in earlier versions
: new SkipNullableUniqueDelegate(this); : new SkipNullableUniqueDelegate(this);
} }

View File

@ -120,7 +120,9 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect {
private UniqueDelegate createUniqueDelgate(DatabaseVersion version) { private UniqueDelegate createUniqueDelgate(DatabaseVersion version) {
return version.isSameOrAfter(10) return version.isSameOrAfter(10)
//use 'create unique nonclustered index ... where ...'
? new AlterTableUniqueIndexDelegate(this) ? new AlterTableUniqueIndexDelegate(this)
//ignore unique keys on nullable columns in versions before 2008
: new SkipNullableUniqueDelegate(this); : new SkipNullableUniqueDelegate(this);
} }

View File

@ -75,17 +75,16 @@ public class AlterTableUniqueDelegate implements UniqueDelegate {
public String getAlterTableToDropUniqueKeyCommand(UniqueKey uniqueKey, Metadata metadata, public String getAlterTableToDropUniqueKeyCommand(UniqueKey uniqueKey, Metadata metadata,
SqlStringGenerationContext context) { SqlStringGenerationContext context) {
final String tableName = context.format( uniqueKey.getTable().getQualifiedTableName() ); final String tableName = context.format( uniqueKey.getTable().getQualifiedTableName() );
final StringBuilder command = new StringBuilder( dialect.getAlterTableString(tableName) );
final StringBuilder buf = new StringBuilder( dialect.getAlterTableString(tableName) ); command.append( dialect.getDropUniqueKeyString() );
buf.append( dialect.getDropUniqueKeyString() );
if ( dialect.supportsIfExistsBeforeConstraintName() ) { if ( dialect.supportsIfExistsBeforeConstraintName() ) {
buf.append( "if exists " ); command.append( "if exists " );
} }
buf.append( dialect.quote( uniqueKey.getName() ) ); command.append( dialect.quote( uniqueKey.getName() ) );
if ( dialect.supportsIfExistsAfterConstraintName() ) { if ( dialect.supportsIfExistsAfterConstraintName() ) {
buf.append( " if exists" ); command.append( " if exists" );
} }
return buf.toString(); return command.toString();
} }
} }

View File

@ -12,15 +12,17 @@ import org.hibernate.dialect.Dialect;
import org.hibernate.mapping.UniqueKey; import org.hibernate.mapping.UniqueKey;
import static org.hibernate.mapping.Index.buildSqlCreateIndexString; import static org.hibernate.mapping.Index.buildSqlCreateIndexString;
import static org.hibernate.mapping.Index.buildSqlDropIndexString;
/** /**
* A {@link UniqueDelegate} which uses {@code create unique index} commands when necessary. * A {@link UniqueDelegate} which uses {@code create unique index} commands when necessary.
* <ul> * <ul>
* <li>DB2 does not allow unique constraints on nullable columns, but it does allow the creation * <li>DB2 does not allow unique constraints on nullable columns, but it does allow the creation
* of unique indexes instead, using {@code create unique index ... exclude null keys}. * of unique indexes instead, using {@code create unique index ... exclude null keys} or
* {@code create unique where not null index}, depending on flavor.
* <li>SQL Server <em>does</em> allow unique constraints on nullable columns, but the semantics * <li>SQL Server <em>does</em> allow unique constraints on nullable columns, but the semantics
* are that two null values are non-unique. So here we need to jump through hoops with the * are that two null values are non-unique. So here we need to jump through hoops with the
* {@code create unique nonclustered index} command. * {@code create unique nonclustered index ... where ...} command.
* </ul> * </ul>
* *
* @author Brett Meyer * @author Brett Meyer
@ -53,7 +55,7 @@ public class AlterTableUniqueIndexDelegate extends AlterTableUniqueDelegate {
public String getAlterTableToDropUniqueKeyCommand(UniqueKey uniqueKey, Metadata metadata, public String getAlterTableToDropUniqueKeyCommand(UniqueKey uniqueKey, Metadata metadata,
SqlStringGenerationContext context) { SqlStringGenerationContext context) {
if ( uniqueKey.hasNullableColumn() ) { if ( uniqueKey.hasNullableColumn() ) {
return org.hibernate.mapping.Index.buildSqlDropIndexString( return buildSqlDropIndexString(
uniqueKey.getName(), uniqueKey.getName(),
context.format( uniqueKey.getTable().getQualifiedTableName() ) context.format( uniqueKey.getTable().getQualifiedTableName() )
); );

View File

@ -24,6 +24,9 @@ import org.hibernate.mapping.UniqueKey;
* <li>For unique keys with no explicit name, it results in {@code unique(x, y)} after the * <li>For unique keys with no explicit name, it results in {@code unique(x, y)} after the
* column list. * column list.
* </ul> * </ul>
* Counterintuitively, this class extends {@link AlterTableUniqueDelegate}, since it falls back
* to using {@code alter table} for {@linkplain org.hibernate.tool.schema.spi.SchemaMigrator
* schema migration}.
* *
* @author Gavin King * @author Gavin King
*/ */

View File

@ -19,7 +19,7 @@ import org.hibernate.mapping.UniqueKey;
* Needed because unique constraints on nullable columns in Sybase always consider null values to be non-unique. * Needed because unique constraints on nullable columns in Sybase always consider null values to be non-unique.
* There is simply no way to create a unique constraint with the semantics we want on a nullable column in Sybase >:-( * There is simply no way to create a unique constraint with the semantics we want on a nullable column in Sybase >:-(
* <p> * <p>
* You might argue that this was a bad decision because if the programmer explicitly specifies an {@code @UniqueKey}, * You might argue that this behavior is bad because if the programmer explicitly specifies an {@code @UniqueKey},
* then we should damn well respect their wishes. But the simple answer is that the user should have also specified * then we should damn well respect their wishes. But the simple answer is that the user should have also specified
* {@code @Column(nullable=false)} if that is what they wanted. A unique key on a nullable column just really doesn't * {@code @Column(nullable=false)} if that is what they wanted. A unique key on a nullable column just really doesn't
* make sense in Sybase, except, perhaps, in some incredibly corner cases. * make sense in Sybase, except, perhaps, in some incredibly corner cases.

View File

@ -13,7 +13,7 @@ import org.hibernate.mapping.Table;
import org.hibernate.mapping.UniqueKey; import org.hibernate.mapping.UniqueKey;
/** /**
* Dialect-level delegate in charge of applying unique constraints in DDL. Uniqueness can * Dialect-level delegate responsible for applying unique constraints in DDL. Uniqueness can
* be specified in any of three ways: * be specified in any of three ways:
* <ol> * <ol>
* <li> * <li>
@ -30,7 +30,18 @@ import org.hibernate.mapping.UniqueKey;
* Also, see {@link #getAlterTableToDropUniqueKeyCommand}. * Also, see {@link #getAlterTableToDropUniqueKeyCommand}.
* </li> * </li>
* </ol> * </ol>
* The first two options are generally preferred. * The first two options are generally preferred, and so we use {@link CreateTableUniqueDelegate}
* where possible. However, for databases where unique constraints may not contain a nullable
* column, and unique indexes must be used instead, we use {@link AlterTableUniqueIndexDelegate}.
* <p>
* Hibernate specifies that a unique constraint on a nullable column considers null values to be
* distinct. Some databases default to the opposite semantic, where null values are considered
* equal for the purpose of determining uniqueness. This is almost never useful, and is the
* opposite of what we want when we use a unique constraint on a foreign key to map an optional
* {@link org.hibernate.mapping.OneToOne} association. Therefore, our {@code UniqueDelegate}s must
* jump through hoops to emulate the sensible semantics specified by ANSI, Hibernate, and common
* sense, namely, that two null values are distinct. A particularly egregious offender is Sybase,
* where we must simply {@linkplain SkipNullableUniqueDelegate skip creating the unique constraint}.
* *
* @author Brett Meyer * @author Brett Meyer
*/ */