consistently allow multiple @Check constraints

+ improvements to jdoc of @Formula and @Check
This commit is contained in:
Gavin 2023-01-02 15:59:01 +01:00 committed by Gavin King
parent aeabc0e48e
commit f385fa063a
16 changed files with 157 additions and 66 deletions

View File

@ -32,8 +32,16 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* on which columns are involved in the constraint expression specified by
* {@link #constraints()}.
* </ul>
* <p>
* For an entity with {@linkplain jakarta.persistence.SecondaryTable secondary tables},
* a check constraint may involve columns of the primary table, or columns of any one
* of the secondary tables. But it may not involve columns of more than one table.
* <p>
* An entity may have multiple {@code @Check} annotations, each defining a different
* constraint.
*
* @author Emmanuel Bernard
* @author Gavin King
*
* @see DialectOverride.Check
*/

View File

@ -9,13 +9,17 @@ package org.hibernate.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* A list of {@link Check}s.
*
* @author Gavin King
*/
@Target(TYPE)
@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface Checks {
Check[] value();

View File

@ -45,6 +45,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* secondary table. Therefore, {@link #on} may be ambiguous.
*
* @author Yanming Zhou
* @author Gavin King
*/
@TypeBinderType(binder = CommentBinder.class)
@AttributeBinderType(binder = CommentBinder.class)

View File

@ -18,22 +18,26 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* an attribute instead of storing the value in a {@link jakarta.persistence.Column}.
* A {@code Formula} mapping defines a "derived" attribute, whose state is determined
* from other columns and functions when an entity is read from the database.
*
* <p>
* A formula may involve multiple columns and SQL operators:
* <pre>
* // perform calculations using SQL operators
* &#64;Formula("sub_total + (sub_total * tax)")
* long getTotalCost() { ... }
* </pre>
*
* It may even call SQL functions:
* <pre>
* // call native SQL functions
* &#64;Formula("upper(substring(middle_name, 1))")
* &#64;Formula("upper(substring(middle_name from 0 for 1))")
* Character getMiddleInitial() { ... }
* </pre>
* <p>
* {@link ColumnTransformer} is an alternative, allowing the use of native SQL to
* read and write values.
*
* For an entity with {@linkplain jakarta.persistence.SecondaryTable secondary tables},
* a formula may involve columns of the primary table, or columns of any one of the
* secondary tables. But it may not involve columns of more than one table.
* <p>
* The {@link ColumnTransformer} annotation is an alternative in certain cases, allowing
* the use of native SQL to read <em>and write</em> values to a column.
* <pre>
* // it might be better to use &#64;ColumnTransformer in this case
* &#064;Formula("decrypt(credit_card_num)")

View File

@ -31,7 +31,7 @@ import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.mapping.CheckConstraint;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.PersistentClass;
@ -309,13 +309,14 @@ class TypeSafeActivator {
}
private static void applySQLCheck(Column col, String checkConstraint) {
String existingCheck = col.getCheckConstraint();
// need to check whether the new check is already part of the existing check, because applyDDL can be called
// multiple times
if ( StringHelper.isNotEmpty( existingCheck ) && !existingCheck.contains( checkConstraint ) ) {
checkConstraint = col.getCheckConstraint() + " AND " + checkConstraint;
// need to check whether the new check is already part of the existing check,
// because applyDDL can be called multiple times
for ( CheckConstraint constraint : col.getCheckConstraints() ) {
if ( constraint.getConstraint().equalsIgnoreCase( checkConstraint ) ) {
return; //EARLY EXIT
}
}
col.setCheckConstraint( checkConstraint );
col.addCheckConstraint( new CheckConstraint( checkConstraint ) );
}
private static boolean applyNotNull(Property property, ConstraintDescriptor<?> descriptor) {

View File

@ -6,10 +6,13 @@
*/
package org.hibernate.boot.model.internal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.hibernate.AnnotationException;
import org.hibernate.annotations.Check;
import org.hibernate.annotations.Checks;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.ColumnTransformer;
import org.hibernate.annotations.ColumnTransformers;
@ -84,8 +87,7 @@ public class AnnotatedColumn {
private String generatedAs;
// private String comment;
private String checkConstraintName;
private String checkConstraint;
private final List<CheckConstraint> checkConstraints = new ArrayList<>();
private AnnotatedColumns parent;
@ -190,9 +192,8 @@ public class AnnotatedColumn {
this.defaultValue = defaultValue;
}
public void setCheckConstraint(String name, String constraint) {
this.checkConstraintName = name;
this.checkConstraint = constraint;
public void addCheckConstraint(String name, String constraint) {
checkConstraints.add( new CheckConstraint( name, constraint ) );
}
// public String getComment() {
@ -235,8 +236,8 @@ public class AnnotatedColumn {
if ( defaultValue != null ) {
mappingColumn.setDefaultValue( defaultValue );
}
if ( checkConstraint != null ) {
mappingColumn.setCheck( new CheckConstraint( checkConstraintName, checkConstraint ) );
for ( CheckConstraint constraint : checkConstraints ) {
mappingColumn.addCheckConstraint( constraint );
}
// if ( isNotEmpty( comment ) ) {
// mappingColumn.setComment( comment );
@ -275,8 +276,8 @@ public class AnnotatedColumn {
mappingColumn.setNullable( nullable );
mappingColumn.setSqlType( sqlType );
mappingColumn.setUnique( unique );
if ( checkConstraint != null ) {
mappingColumn.setCheck( new CheckConstraint( checkConstraintName, checkConstraint ) );
for ( CheckConstraint constraint : checkConstraints ) {
mappingColumn.addCheckConstraint( constraint );
}
mappingColumn.setDefaultValue( defaultValue );
@ -808,13 +809,21 @@ public class AnnotatedColumn {
private void applyCheckConstraint(PropertyData inferredData, int length) {
final XProperty property = inferredData.getProperty();
if ( property != null ) {
final Check check = getOverridableAnnotation( property, Check.class, getBuildingContext() );
if ( check != null ) {
if ( length != 1 ) {
throw new AnnotationException("'@Check' may only be applied to single-column mappings but '"
+ property.getName() + "' maps to " + length + " columns (use a table-level '@Check')" );
if ( property.isAnnotationPresent( Checks.class ) ) {
// if there are multiple annotations, they're not overrideable
for ( Check check : property.getAnnotation( Checks.class ).value() ) {
addCheckConstraint( check.name().isEmpty() ? null : check.name(), check.constraints() );
}
}
else {
final Check check = getOverridableAnnotation( property, Check.class, getBuildingContext() );
if ( check != null ) {
if ( length != 1 ) {
throw new AnnotationException("'@Check' may only be applied to single-column mappings but '"
+ property.getName() + "' maps to " + length + " columns (use a table-level '@Check')" );
}
addCheckConstraint( check.name().isEmpty() ? null : check.name(), check.constraints() );
}
setCheckConstraint( check.name().isEmpty() ? null : check.name(), check.constraints() );
}
}
else {

View File

@ -24,6 +24,7 @@ import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.Check;
import org.hibernate.annotations.Checks;
import org.hibernate.annotations.CollectionId;
import org.hibernate.annotations.CollectionIdJavaType;
import org.hibernate.annotations.CollectionIdJdbcType;
@ -2381,18 +2382,30 @@ public abstract class CollectionBinder {
);
Table collectionTable = tableBinder.bind();
collection.setCollectionTable( collectionTable );
handleCheck( collectionTable );
handleCheckConstraints( collectionTable );
}
private void handleCheck(Table collectionTable) {
final Check check = getOverridableAnnotation( property, Check.class, buildingContext );
if ( check != null ) {
final String name = check.name();
final String constraint = check.constraints();
collectionTable.addCheck( name.isEmpty()
? new CheckConstraint( constraint )
: new CheckConstraint( name, constraint ) );
private void handleCheckConstraints(Table collectionTable) {
if ( property.isAnnotationPresent( Checks.class ) ) {
// if there are multiple annotations, they're not overrideable
for ( Check check : property.getAnnotation( Checks.class ).value() ) {
addCheckToCollection( collectionTable, check );
}
}
else {
final Check check = getOverridableAnnotation( property, Check.class, buildingContext );
if ( check != null ) {
addCheckToCollection( collectionTable, check );
}
}
}
private static void addCheckToCollection(Table collectionTable, Check check) {
final String name = check.name();
final String constraint = check.constraints();
collectionTable.addCheck( name.isEmpty()
? new CheckConstraint( constraint )
: new CheckConstraint( name, constraint ) );
}
private void handleUnownedManyToMany(
@ -2412,8 +2425,8 @@ public abstract class CollectionBinder {
otherSideProperty = collectionEntity.getRecursiveProperty( mappedBy );
}
catch ( MappingException e ) {
throw new AnnotationException( "Association '" + safeCollectionRole() +
"is 'mappedBy' a property named '" + mappedBy
throw new AnnotationException( "Association '" + safeCollectionRole()
+ "is 'mappedBy' a property named '" + mappedBy
+ "' which does not exist in the target entity '" + elementType.getName() + "'" );
}
final Value otherSidePropertyValue = otherSideProperty.getValue();
@ -2423,6 +2436,12 @@ public abstract class CollectionBinder {
// this is a ToOne with a @JoinTable or a regular property
: otherSidePropertyValue.getTable();
collection.setCollectionTable( table );
if ( property.isAnnotationPresent( Checks.class )
|| property.isAnnotationPresent( Check.class ) ) {
throw new AnnotationException( "Association '" + safeCollectionRole()
+ " is an unowned collection and may not be annotated '@Check'" );
}
}
private void detectManyToManyProblems(

View File

@ -53,7 +53,6 @@ import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.Filter;
import org.hibernate.annotations.Filters;
import org.hibernate.annotations.ForeignKey;
import org.hibernate.annotations.ForeignKey;
import org.hibernate.annotations.Immutable;
import org.hibernate.annotations.Loader;
import org.hibernate.annotations.NaturalIdCache;
@ -213,7 +212,7 @@ public class EntityBinder {
entityBinder.bindEntity();
entityBinder.handleClassTable( inheritanceState, superEntity );
entityBinder.handleSecondaryTables();
entityBinder.handleCheck();
entityBinder.handleCheckConstraints();
final PropertyHolder holder = buildPropertyHolder(
clazzToProcess,
persistentClass,
@ -242,7 +241,7 @@ public class EntityBinder {
entityBinder.callTypeBinders( persistentClass );
}
private void handleCheck() {
private void handleCheckConstraints() {
if ( annotatedClass.isAnnotationPresent( Checks.class ) ) {
// if we have more than one of them they are not overrideable :-/
for ( Check check : annotatedClass.getAnnotation( Checks.class ).value() ) {

View File

@ -19,6 +19,7 @@ import org.hibernate.boot.model.source.spi.LocalMetadataBuildingContext;
import org.hibernate.boot.model.source.spi.RelationalValueSource;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.mapping.CheckConstraint;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Formula;
import org.hibernate.mapping.OneToOne;
@ -155,7 +156,10 @@ public class RelationalObjectBinder {
column.setUnique( columnSource.isUnique() );
column.setCheckConstraint( columnSource.getCheckCondition() );
String checkCondition = columnSource.getCheckCondition();
if ( checkCondition != null ) {
column.addCheckConstraint( new CheckConstraint(checkCondition) );
}
column.setDefaultValue( columnSource.getDefaultValue() );
column.setSqlType( columnSource.getSqlType() );

View File

@ -31,7 +31,9 @@ public class AggregateColumn extends Column {
setSqlType( column.getSqlType() );
setSqlTypeCode( column.getSqlTypeCode() );
uniqueInteger = column.uniqueInteger; //usually useless
checkConstraint = column.checkConstraint;
for ( CheckConstraint constraint : column.getCheckConstraints() ) {
addCheckConstraint( constraint );
}
setComment( column.getComment() );
setDefaultValue( column.getDefaultValue() );
setGeneratedAs( column.getGeneratedAs() );

View File

@ -340,11 +340,11 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
}
}
if ( dialect.supportsColumnCheck() && !column.hasCheckConstraint() ) {
if ( dialect.supportsColumnCheck() ) {
final String checkCondition = resolution.getLegacyResolvedBasicType()
.getCheckCondition( column.getQuotedName( dialect ), dialect );
if ( checkCondition != null ) {
column.setCheck( new CheckConstraint( checkCondition ) );
column.addCheckConstraint( new CheckConstraint( checkCondition ) );
}
}
}

View File

@ -8,6 +8,7 @@ package org.hibernate.mapping;
import java.io.Serializable;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Objects;
@ -33,6 +34,7 @@ import org.hibernate.type.descriptor.jdbc.ArrayJdbcType;
import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry;
import org.hibernate.type.spi.TypeConfiguration;
import static java.util.Collections.unmodifiableList;
import static org.hibernate.internal.util.StringHelper.isEmpty;
import static org.hibernate.internal.util.StringHelper.lastIndexOfLetter;
import static org.hibernate.internal.util.StringHelper.nullIfEmpty;
@ -58,7 +60,6 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn
private Integer sqlTypeCode;
private boolean quoted;
int uniqueInteger;
CheckConstraint checkConstraint;
private String comment;
private String defaultValue;
private String generatedAs;
@ -67,6 +68,7 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn
private String customRead;
private Size columnSize;
private String specializedTypeDeclaration;
private java.util.List<CheckConstraint> checkConstraints = new ArrayList<>();
public Column() {
}
@ -490,31 +492,52 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn
return specializedTypeDeclaration != null;
}
public void addCheckConstraint(CheckConstraint checkConstraint) {
this.checkConstraints.add( checkConstraint );
}
public java.util.List<CheckConstraint> getCheckConstraints() {
return unmodifiableList( checkConstraints );
}
@Deprecated(since = "6.2")
public String getCheckConstraint() {
return checkConstraint == null ? null : checkConstraint.getConstraint();
if ( checkConstraints.isEmpty() ) {
return null;
}
else if ( checkConstraints.size() > 1 ) {
throw new IllegalStateException( "column has multiple check constraints" );
}
else {
return checkConstraints.get(0).getConstraint();
}
}
@Deprecated(since = "6.2")
public void setCheckConstraint(String constraint) {
checkConstraint = constraint == null ? null : new CheckConstraint( constraint );
}
public void setCheck(CheckConstraint check) {
checkConstraint = check;
if ( constraint != null ) {
if ( !checkConstraints.isEmpty() ) {
throw new IllegalStateException( "column already has a check constraint" );
}
checkConstraints.add( new CheckConstraint( constraint ) );
}
}
public boolean hasCheckConstraint() {
return checkConstraint != null;
return !checkConstraints.isEmpty();
}
@Deprecated(since = "6.2")
public String checkConstraint() {
return checkConstraint == null ? null : checkConstraint.constraintString();
}
public CheckConstraint getCheck() {
return checkConstraint;
if ( checkConstraints.isEmpty() ) {
return null;
}
else if ( checkConstraints.size() > 1 ) {
throw new IllegalStateException( "column has multiple check constraints" );
}
else {
return checkConstraints.get(0).constraintString();
}
}
@Override
@ -655,7 +678,7 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn
copy.sqlTypeName = sqlTypeName;
copy.sqlTypeCode = sqlTypeCode;
copy.uniqueInteger = uniqueInteger; //usually useless
copy.checkConstraint = checkConstraint;
copy.checkConstraints = checkConstraints;
copy.comment = comment;
copy.defaultValue = defaultValue;
copy.generatedAs = generatedAs;

View File

@ -471,7 +471,7 @@ public class ManyToManyCollectionPart extends AbstractEntityCollectionPart imple
true,
false,
false,
creationProcess.getCreationContext().getJdbcServices().getDialect(),
creationProcess.getCreationContext().getDialect(),
creationProcess.getSqmFunctionRegistry()
);

View File

@ -207,6 +207,7 @@ public class NamedObjectRepositoryImpl implements NamedObjectRepository {
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Named query checking
@Override
public void validateNamedQueries(QueryEngine queryEngine) {
final Map<String, HibernateException> errors = checkNamedQueries( queryEngine );
if ( !errors.isEmpty() ) {
@ -223,6 +224,7 @@ public class NamedObjectRepositoryImpl implements NamedObjectRepository {
}
}
@Override
public Map<String, HibernateException> checkNamedQueries(QueryEngine queryEngine) {
Map<String,HibernateException> errors = new HashMap<>();

View File

@ -11,6 +11,7 @@ import org.hibernate.boot.model.relational.SqlStringGenerationContext;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.jdbc.Size;
import org.hibernate.mapping.CheckConstraint;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Constraint;
import org.hibernate.mapping.Table;
@ -100,8 +101,10 @@ class ColumnDefinitions {
definition.append( dialect.getUniqueDelegate().getColumnDefinitionUniquenessFragment( column, context ) );
}
if ( dialect.supportsColumnCheck() && column.hasCheckConstraint() ) {
definition.append( column.getCheck().constraintString() );
if ( dialect.supportsColumnCheck() ) {
for ( CheckConstraint checkConstraint : column.getCheckConstraints() ) {
definition.append( checkConstraint.constraintString() );
}
}
}

View File

@ -232,8 +232,7 @@ public class StandardTableExporter implements Exporter<Table> {
}
else {
for ( Column subColumn : value.getColumns() ) {
final CheckConstraint check = subColumn.getCheck();
final String checkConstraint = check == null ? null : check.getConstraint();
final String checkConstraint = getCheckConstraint( subColumn );
if ( !subColumn.isNullable() || checkConstraint != null ) {
final String subColumnName = subColumn.getQuotedName( dialect );
final String columnExpression = aggregateSupport.aggregateComponentCustomReadExpression(
@ -275,6 +274,19 @@ public class StandardTableExporter implements Exporter<Table> {
return separator;
}
private static String getCheckConstraint(Column subColumn) {
final List<CheckConstraint> checkConstraints = subColumn.getCheckConstraints();
if ( checkConstraints.isEmpty() ) {
return null;
}
else if ( checkConstraints.size() > 1 ) {
throw new MappingException( "Multiple check constraints not supported for aggregate columns" );
}
else {
return checkConstraints.get(0).getConstraint();
}
}
protected String tableCreateString(boolean hasPrimaryKey) {
return hasPrimaryKey ? dialect.getCreateTableString() : dialect.getCreateMultisetTableString();