automatically detect when a @Check refers to a @SecondaryTable

- also support named check constraints (multiple of them)
- also support check constraints on collection tables
This commit is contained in:
Gavin 2023-01-02 00:07:32 +01:00 committed by Gavin King
parent abb89a32b1
commit 1657c22aca
21 changed files with 413 additions and 250 deletions

View File

@ -6,10 +6,12 @@
*/
package org.hibernate.userguide.schema;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.PersistenceException;
import jakarta.persistence.SecondaryTable;
import org.hibernate.annotations.Check;
import org.hibernate.annotations.NaturalId;
import org.hibernate.dialect.H2Dialect;
@ -20,6 +22,8 @@ import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase;
import org.hibernate.testing.RequiresDialect;
import org.junit.Test;
import java.time.LocalDate;
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@ -120,7 +124,9 @@ public class CheckTest extends BaseEntityManagerFunctionalTestCase {
//tag::schema-generation-database-checks-example[]
@Entity(name = "Book")
@Check(constraints = "CASE WHEN isbn IS NOT NULL THEN LENGTH(isbn) = 13 ELSE true END")
@Check(name = "ValidIsbn", constraints = "CASE WHEN isbn IS NOT NULL THEN LENGTH(isbn) = 13 ELSE true END")
@SecondaryTable(name = "BookEdition")
@Check(name = "PositiveEdition", constraints = "edition > 0")
public static class Book {
@Id
@ -133,6 +139,12 @@ public class CheckTest extends BaseEntityManagerFunctionalTestCase {
private Double price;
@Column(table = "BookEdition")
private int edition = 1;
@Column(table = "BookEdition")
private LocalDate editionDate;
//Getters and setters omitted for brevity
//end::schema-generation-database-checks-example[]

View File

@ -6,6 +6,7 @@
*/
package org.hibernate.annotations;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@ -17,8 +18,19 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Specifies a {@code check} constraint to be included in the generated DDL.
* <ul>
* <li>When a field or property is annotated, the check constraint is added to the column definition.
* <li>When an entity class is annotated, the check constraint is added to the primary table.
* <li>When a basic-typed field or property is annotated, the check constraint is
* added to the {@linkplain jakarta.persistence.Column column} definition.
* <li>When a {@link jakarta.persistence.ManyToOne} association is annotated, the
* check constraint is added to the {@linkplain jakarta.persistence.JoinColumn
* join column} definition.
* <li>When an owned collection is annotated, the check constraint is added to the
* {@linkplain jakarta.persistence.CollectionTable collection table} or
* {@linkplain jakarta.persistence.JoinTable association join table}.
* <li>When an entity class is annotated, the check constraint is added to either
* the {@linkplain jakarta.persistence.Table primary table} or to a
* {@linkplain jakarta.persistence.SecondaryTable secondary table}, depending
* on which columns are involved in the constraint expression specified by
* {@link #constraints()}.
* </ul>
*
* @author Emmanuel Bernard
@ -27,7 +39,12 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
*/
@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
@Repeatable(Checks.class)
public @interface Check {
/**
* The optional name of the check constraint.
*/
String name() default "";
/**
* The check constraint, written in native SQL.
*/

View File

@ -0,0 +1,22 @@
/*
* 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.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* A list of {@link Check}s.
*/
@Target(TYPE)
@Retention(RUNTIME)
public @interface Checks {
Check[] value();
}

View File

@ -22,10 +22,16 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
*
* @see jakarta.persistence.Table
* @see jakarta.persistence.SecondaryTable
*
* @deprecated The options available here are all now offered by other newer and better-designed
* annotations in this package. This annotation will soon be removed, since it's very
* annoying to have two annotations named {@code @Table}.
*
*/
@Target(TYPE)
@Retention(RUNTIME)
@Repeatable(Tables.class)
@Deprecated(since = "6.2", forRemoval = true)
public @interface Table {
/**
* The name of the targeted table.
@ -43,11 +49,10 @@ public @interface Table {
/**
* A check constraint, written in native SQL.
* <p>
* <em>Useful for secondary tables, otherwise use {@link Check}.</em>
*
* @see Check
* @deprecated use {@link Check}.
*/
@Deprecated(since = "6.2")
String checkConstraint() default "";
/**
@ -61,6 +66,7 @@ public @interface Table {
/**
* Specifies a foreign key of a secondary table, which points back to the primary table.
*
* @apiNote Only relevant to secondary tables
* @deprecated use {@link jakarta.persistence.SecondaryTable#foreignKey()}
*/
@Deprecated(since = "6.0", forRemoval = true)
@ -74,9 +80,8 @@ public @interface Table {
/**
* If enabled, Hibernate will never insert or update the columns of the secondary table.
* <p>
* <em>Only applies to secondary tables.</em>
*
* @apiNote Only relevant to secondary tables
* @deprecated use {@link SecondaryRow#owned()}
*/
@Deprecated(since = "6.2")
@ -87,7 +92,7 @@ public @interface Table {
* would not all be null, and will always use an outer join to read the columns. Thus,
* by default, Hibernate avoids creating a row of null values.
*
* @apiNote Only relevant for secondary tables
* @apiNote Only relevant to secondary tables
* @deprecated use {@link SecondaryRow#optional()}
*/
@Deprecated(since = "6.2")
@ -95,9 +100,8 @@ public @interface Table {
/**
* Defines a custom SQL insert statement.
* <p>
* <em>Only applies to secondary tables.</em>
*
* @apiNote Only relevant to secondary tables
* @deprecated use {@link SQLInsert#table()} to specify the secondary table
*/
@Deprecated(since="6.2")
@ -105,9 +109,8 @@ public @interface Table {
/**
* Defines a custom SQL update statement.
* <p>
* <em>Only applies to secondary tables.</em>
*
* @apiNote Only relevant to secondary tables
* @deprecated use {@link SQLInsert#table()} to specify the secondary table
*/
@Deprecated(since="6.2")
@ -115,9 +118,8 @@ public @interface Table {
/**
* Defines a custom SQL delete statement.
* <p>
* <em>Only applies to secondary tables.</em>
*
* @apiNote Only relevant to secondary tables
* @deprecated use {@link SQLInsert#table()} to specify the secondary table
*/
@Deprecated(since="6.2")

View File

@ -16,9 +16,12 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* A grouping of {@link Table}s.
*
* @author Emmanuel Bernard
*
* @deprecated since {@link Table} is deprecated
*/
@Target(TYPE)
@Retention(RUNTIME)
@Deprecated(since = "6.2", forRemoval = true)
public @interface Tables {
/**
* The table grouping.

View File

@ -27,6 +27,7 @@ import org.hibernate.boot.spi.PropertyData;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.mapping.AggregateColumn;
import org.hibernate.mapping.CheckConstraint;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.Formula;
@ -83,6 +84,7 @@ public class AnnotatedColumn {
private String generatedAs;
// private String comment;
private String checkConstraintName;
private String checkConstraint;
private AnnotatedColumns parent;
@ -124,10 +126,6 @@ public class AnnotatedColumn {
return isNotEmpty( formulaString );
}
public String getFormulaString() {
return formulaString;
}
public String getExplicitTableName() {
return explicitTableName;
}
@ -188,16 +186,13 @@ public class AnnotatedColumn {
return defaultValue;
}
public String getCheckConstraint() {
return checkConstraint;
}
public void setDefaultValue(String defaultValue) {
this.defaultValue = defaultValue;
}
public void setCheckConstraint(String checkConstraint) {
this.checkConstraint = checkConstraint;
public void setCheckConstraint(String name, String constraint) {
this.checkConstraintName = name;
this.checkConstraint = constraint;
}
// public String getComment() {
@ -241,7 +236,7 @@ public class AnnotatedColumn {
mappingColumn.setDefaultValue( defaultValue );
}
if ( checkConstraint != null ) {
mappingColumn.setCheckConstraint( checkConstraint );
mappingColumn.setCheck( new CheckConstraint( checkConstraintName, checkConstraint ) );
}
// if ( isNotEmpty( comment ) ) {
// mappingColumn.setComment( comment );
@ -280,7 +275,9 @@ public class AnnotatedColumn {
mappingColumn.setNullable( nullable );
mappingColumn.setSqlType( sqlType );
mappingColumn.setUnique( unique );
mappingColumn.setCheckConstraint( checkConstraint );
if ( checkConstraint != null ) {
mappingColumn.setCheck( new CheckConstraint( checkConstraintName, checkConstraint ) );
}
mappingColumn.setDefaultValue( defaultValue );
if ( writeExpression != null ) {
@ -817,7 +814,7 @@ public class AnnotatedColumn {
throw new AnnotationException("'@Check' may only be applied to single-column mappings but '"
+ property.getName() + "' maps to " + length + " columns (use a table-level '@Check')" );
}
setCheckConstraint( check.constraints() );
setCheckConstraint( check.name().isEmpty() ? null : check.name(), check.constraints() );
}
}
else {

View File

@ -226,7 +226,7 @@ public class ClassPropertyHolder extends AbstractPropertyHolder {
return join;
}
private void addPropertyToPersistentClass(Property prop, XClass declaringClass) {
private void addPropertyToPersistentClass(Property property, XClass declaringClass) {
if ( declaringClass != null ) {
final InheritanceState inheritanceState = inheritanceStatePerClass.get( declaringClass );
if ( inheritanceState == null ) {
@ -235,15 +235,15 @@ public class ClassPropertyHolder extends AbstractPropertyHolder {
);
}
if ( inheritanceState.isEmbeddableSuperclass() ) {
persistentClass.addMappedSuperclassProperty( prop );
addPropertyToMappedSuperclass( prop, declaringClass );
persistentClass.addMappedSuperclassProperty( property );
addPropertyToMappedSuperclass( property, declaringClass );
}
else {
persistentClass.addProperty( prop );
persistentClass.addProperty( property );
}
}
else {
persistentClass.addProperty( prop );
persistentClass.addProperty( property );
}
}
@ -359,7 +359,7 @@ public class ClassPropertyHolder extends AbstractPropertyHolder {
}
}
private void addPropertyToJoin(Property prop, XClass declaringClass, Join join) {
private void addPropertyToJoin(Property property, XClass declaringClass, Join join) {
if ( declaringClass != null ) {
final InheritanceState inheritanceState = inheritanceStatePerClass.get( declaringClass );
if ( inheritanceState == null ) {
@ -368,15 +368,15 @@ public class ClassPropertyHolder extends AbstractPropertyHolder {
);
}
if ( inheritanceState.isEmbeddableSuperclass() ) {
join.addMappedsuperclassProperty(prop);
addPropertyToMappedSuperclass( prop, declaringClass );
join.addMappedsuperclassProperty( property );
addPropertyToMappedSuperclass( property, declaringClass );
}
else {
join.addProperty( prop );
join.addProperty( property );
}
}
else {
join.addProperty( prop );
join.addProperty( property );
}
}

View File

@ -23,6 +23,7 @@ import org.hibernate.annotations.Bag;
import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.Check;
import org.hibernate.annotations.CollectionId;
import org.hibernate.annotations.CollectionIdJavaType;
import org.hibernate.annotations.CollectionIdJdbcType;
@ -84,6 +85,7 @@ import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.mapping.Any;
import org.hibernate.mapping.Backref;
import org.hibernate.mapping.CheckConstraint;
import org.hibernate.mapping.Collection;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Component;
@ -1626,7 +1628,7 @@ public abstract class CollectionBinder {
handleWhere( false );
final PersistentClass targetEntity = persistentClasses.get( getElementType().getName() );
bindCollectionSecondPass( targetEntity, foreignJoinColumns, onDeleteAction);
bindCollectionSecondPass( targetEntity, foreignJoinColumns );
if ( !collection.isInverse() && !collection.getKey().isNullable() ) {
createOneToManyBackref( oneToMany );
@ -2056,84 +2058,35 @@ public abstract class CollectionBinder {
logManyToManySecondPass( oneToMany, isCollectionOfEntities, isManyToAny );
//check for user error
detectManyToManyProblems(
elementType,
property,
propertyHolder,
isCollectionOfEntities,
isManyToAny
);
detectManyToManyProblems( elementType, isCollectionOfEntities, isManyToAny );
if ( isUnownedCollection() ) {
handleUnownedManyToMany(
collection,
joinColumns,
elementType,
targetEntity,
isCollectionOfEntities
);
handleUnownedManyToMany( elementType, targetEntity, isCollectionOfEntities );
}
else {
handleOwnedManyToMany(
collection,
joinColumns,
tableBinder,
property,
buildingContext,
targetEntity,
isCollectionOfEntities
);
handleOwnedManyToMany( targetEntity, isCollectionOfEntities );
}
bindFilters( isCollectionOfEntities );
handleWhere( isCollectionOfEntities );
bindCollectionSecondPass( targetEntity, joinColumns, onDeleteAction);
bindCollectionSecondPass( targetEntity, joinColumns );
if ( isCollectionOfEntities ) {
final ManyToOne element = handleCollectionOfEntities(
collection,
elementType,
notFoundAction,
property,
buildingContext,
targetEntity,
hqlOrderBy
);
final ManyToOne element = handleCollectionOfEntities( elementType, targetEntity, hqlOrderBy );
bindManyToManyInverseForeignKey( targetEntity, inverseJoinColumns, element, oneToMany );
}
else if ( isManyToAny ) {
handleManyToAny(
collection,
inverseJoinColumns,
onDeleteAction,
property,
buildingContext
);
handleManyToAny();
}
else {
handleElementCollection(
collection,
elementColumns,
isEmbedded,
elementType,
property,
propertyHolder,
hqlOrderBy
);
handleElementCollection( elementType, hqlOrderBy );
}
checkFilterConditions( collection );
}
private void handleElementCollection(
Collection collection,
AnnotatedColumns elementColumns,
boolean isEmbedded,
XClass elementType,
XProperty property,
PropertyHolder parentPropertyHolder,
String hqlOrderBy) {
// 'parentPropertyHolder' is the PropertyHolder for the owner of the collection
private void handleElementCollection(XClass elementType, String hqlOrderBy) {
// 'propertyHolder' is the PropertyHolder for the owner of the collection
// 'holder' is the CollectionPropertyHolder.
// 'property' is the collection XProperty
@ -2141,7 +2094,7 @@ public abstract class CollectionBinder {
final AnnotatedClassType classType = annotatedElementType( isEmbedded, property, elementType );
final boolean primitive = classType == NONE;
if ( !primitive ) {
parentPropertyHolder.startingProperty( property );
propertyHolder.startingProperty( property );
}
final CollectionPropertyHolder holder = buildPropertyHolder(
@ -2149,7 +2102,7 @@ public abstract class CollectionBinder {
collection.getRole(),
elementClass,
property,
parentPropertyHolder,
propertyHolder,
buildingContext
);
holder.prepare( property );
@ -2157,18 +2110,15 @@ public abstract class CollectionBinder {
final Class<? extends CompositeUserType<?>> compositeUserType =
resolveCompositeUserType( property, elementClass, buildingContext );
if ( classType == EMBEDDABLE || compositeUserType != null ) {
handleCompositeCollectionElement( collection, property, hqlOrderBy, elementClass, holder, compositeUserType );
handleCompositeCollectionElement( hqlOrderBy, elementClass, holder, compositeUserType );
}
else {
handleCollectionElement( collection, elementColumns, elementType, property, hqlOrderBy, elementClass, holder );
handleCollectionElement( elementType, hqlOrderBy, elementClass, holder );
}
}
private void handleCollectionElement(
Collection collection,
AnnotatedColumns elementColumns,
XClass elementType,
XProperty property,
String hqlOrderBy,
XClass elementClass,
CollectionPropertyHolder holder) {
@ -2199,8 +2149,6 @@ public abstract class CollectionBinder {
}
private void handleCompositeCollectionElement(
Collection collection,
XProperty property,
String hqlOrderBy,
XClass elementClass,
CollectionPropertyHolder holder,
@ -2256,7 +2204,7 @@ public abstract class CollectionBinder {
}
}
AnnotatedClassType annotatedElementType(
private AnnotatedClassType annotatedElementType(
boolean isEmbedded,
XProperty property,
XClass elementType) {
@ -2301,14 +2249,7 @@ public abstract class CollectionBinder {
return elementColumns;
}
private ManyToOne handleCollectionOfEntities(
Collection collection,
XClass elementType,
NotFoundAction notFoundAction,
XProperty property,
MetadataBuildingContext buildingContext,
PersistentClass collectionEntity,
String hqlOrderBy) {
private ManyToOne handleCollectionOfEntities(XClass elementType, PersistentClass collectionEntity, String hqlOrderBy) {
final ManyToOne element = new ManyToOne( buildingContext, collection.getCollectionTable() );
collection.setElement( element );
element.setReferencedEntityName( elementType.getName() );
@ -2339,8 +2280,9 @@ public abstract class CollectionBinder {
foreignKeyDefinition = joinColumnAnn.foreignKey().foreignKeyDefinition();
}
}
if ( joinTableAnn.inverseForeignKey().value() == NO_CONSTRAINT
|| joinTableAnn.inverseForeignKey().value() == PROVIDER_DEFAULT
final ConstraintMode constraintMode = joinTableAnn.inverseForeignKey().value();
if ( constraintMode == NO_CONSTRAINT
|| constraintMode == PROVIDER_DEFAULT
&& buildingContext.getBuildingOptions().isNoConstraintByDefault() ) {
element.disableForeignKey();
}
@ -2353,12 +2295,7 @@ public abstract class CollectionBinder {
return element;
}
private void handleManyToAny(
Collection collection,
AnnotatedJoinColumns inverseJoinColumns,
OnDeleteAction onDeleteAction,
XProperty property,
MetadataBuildingContext buildingContext) {
private void handleManyToAny() {
//@ManyToAny
//Make sure that collTyp is never used during the @ManyToAny branch: it will be set to void.class
final PropertyData inferredData = new PropertyInferredData(
@ -2414,25 +2351,20 @@ public abstract class CollectionBinder {
}
private void handleOwnedManyToMany(
Collection collection,
AnnotatedJoinColumns joinColumns,
TableBinder associationTableBinder,
XProperty property,
MetadataBuildingContext context,
PersistentClass collectionEntity,
boolean isCollectionOfEntities) {
//TODO: only for implicit columns?
//FIXME NamingStrategy
final InFlightMetadataCollector collector = context.getMetadataCollector();
final InFlightMetadataCollector collector = buildingContext.getMetadataCollector();
final PersistentClass owner = collection.getOwner();
joinColumns.setMappedBy(
owner.getEntityName(),
collector.getLogicalTableName( owner.getTable() ),
collector.getFromMappedBy( owner.getEntityName(), joinColumns.getPropertyName() )
);
if ( isEmpty( associationTableBinder.getName() ) ) {
if ( isEmpty( tableBinder.getName() ) ) {
//default value
associationTableBinder.setDefaultName(
tableBinder.setDefaultName(
owner.getClassName(),
owner.getEntityName(),
owner.getJpaEntityName(),
@ -2444,15 +2376,26 @@ public abstract class CollectionBinder {
joinColumns.getPropertyName()
);
}
associationTableBinder.setJPA2ElementCollection(
tableBinder.setJPA2ElementCollection(
!isCollectionOfEntities && property.isAnnotationPresent( ElementCollection.class )
);
collection.setCollectionTable( associationTableBinder.bind() );
Table collectionTable = tableBinder.bind();
collection.setCollectionTable( collectionTable );
handleCheck( 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 handleUnownedManyToMany(
Collection collection,
AnnotatedJoinColumns joinColumns,
XClass elementType,
PersistentClass collectionEntity,
boolean isCollectionOfEntities) {
@ -2484,8 +2427,6 @@ public abstract class CollectionBinder {
private void detectManyToManyProblems(
XClass elementType,
XProperty property,
PropertyHolder parentPropertyHolder,
boolean isCollectionOfEntities,
boolean isManyToAny) {
@ -2495,13 +2436,13 @@ public abstract class CollectionBinder {
+ "' targets the type '" + elementType.getName() + "' which is not an '@Entity' type" );
}
else if (isManyToAny) {
if ( parentPropertyHolder.getJoinTable( property ) == null ) {
if ( propertyHolder.getJoinTable( property ) == null ) {
throw new AnnotationException( "Association '" + safeCollectionRole()
+ "' is a '@ManyToAny' and must specify a '@JoinTable'" );
}
}
else {
final JoinTable joinTableAnn = parentPropertyHolder.getJoinTable( property );
final JoinTable joinTableAnn = propertyHolder.getJoinTable( property );
if ( joinTableAnn != null && joinTableAnn.inverseJoinColumns().length > 0 ) {
throw new AnnotationException( "Association '" + safeCollectionRole()
+ " has a '@JoinTable' with 'inverseJoinColumns' and targets the type '"
@ -2576,10 +2517,7 @@ public abstract class CollectionBinder {
}
}
private void bindCollectionSecondPass(
PersistentClass targetEntity,
AnnotatedJoinColumns joinColumns,
OnDeleteAction onDeleteAction) {
private void bindCollectionSecondPass(PersistentClass targetEntity, AnnotatedJoinColumns joinColumns) {
if ( !isUnownedCollection() ) {
createSyntheticPropertyReference(
@ -2710,8 +2648,8 @@ public abstract class CollectionBinder {
this.foreignJoinColumns = annotatedJoinColumns;
}
public void setExplicitAssociationTable(boolean explicitAssocTable) {
this.isExplicitAssociationTable = explicitAssocTable;
public void setExplicitAssociationTable(boolean isExplicitAssociationTable) {
this.isExplicitAssociationTable = isExplicitAssociationTable;
}
public void setElementColumns(AnnotatedColumns elementColumns) {

View File

@ -45,6 +45,7 @@ import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.Check;
import org.hibernate.annotations.Checks;
import org.hibernate.annotations.DiscriminatorFormula;
import org.hibernate.annotations.DiscriminatorOptions;
import org.hibernate.annotations.DynamicInsert;
@ -52,6 +53,7 @@ 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;
@ -101,6 +103,7 @@ import org.hibernate.engine.spi.FilterDefinition;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.jpa.event.spi.CallbackType;
import org.hibernate.mapping.BasicValue;
import org.hibernate.mapping.CheckConstraint;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.DependantValue;
import org.hibernate.mapping.Join;
@ -210,6 +213,7 @@ public class EntityBinder {
entityBinder.bindEntity();
entityBinder.handleClassTable( inheritanceState, superEntity );
entityBinder.handleSecondaryTables();
entityBinder.handleCheck();
final PropertyHolder holder = buildPropertyHolder(
clazzToProcess,
persistentClass,
@ -238,6 +242,33 @@ public class EntityBinder {
entityBinder.callTypeBinders( persistentClass );
}
private void handleCheck() {
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() ) {
addCheckToEntity( check );
}
}
else {
final Check check = getOverridableAnnotation( annotatedClass, Check.class, context );
if ( check != null ) {
addCheckToEntity( check );
}
}
}
/**
* For now, we store it on the entity.
* Later we will come back and figure out which table it belongs to.
*/
private void addCheckToEntity(Check check) {
final String name = check.name();
final String constraint = check.constraints();
persistentClass.addCheckConstraint( name.isEmpty()
? new CheckConstraint( constraint )
: new CheckConstraint( name, constraint ) );
}
private void callTypeBinders(PersistentClass persistentClass) {
for ( Annotation containingAnnotation : findContainingAnnotations( annotatedClass, TypeBinderType.class ) ) {
final TypeBinderType binderType = containingAnnotation.annotationType().getAnnotation( TypeBinderType.class );
@ -643,14 +674,12 @@ public class EntityBinder {
String catalog,
List<UniqueConstraintHolder> uniqueConstraints,
InFlightMetadataCollector collector) {
final Check check = getOverridableAnnotation( annotatedClass, Check.class, context );
final RowId rowId = annotatedClass.getAnnotation( RowId.class );
bindTable(
schema,
catalog,
table,
uniqueConstraints,
check == null ? null : check.constraints(),
rowId == null ? null : rowId.value(),
inheritanceState.hasDenormalizedTable()
? collector.getEntityTableXref( superEntity.getEntityName() )
@ -1662,7 +1691,6 @@ public class EntityBinder {
String catalog,
String tableName,
List<UniqueConstraintHolder> uniqueConstraints,
String checkConstraint,
String rowId,
InFlightMetadataCollector.EntityTableXref denormalizedSuperTableXref) {
@ -1687,9 +1715,6 @@ public class EntityBinder {
);
table.setRowId( rowId );
if ( checkConstraint != null ) {
table.addCheckConstraint( checkConstraint );
}
// final Comment comment = annotatedClass.getAnnotation( Comment.class );
// if ( comment != null ) {

View File

@ -182,19 +182,8 @@ public class OneToOneSecondPass implements SecondPass {
manyToOne.markAsLogicalOneToOne();
property.setValue( manyToOne );
for ( Column column: otherSideJoin.getKey().getColumns() ) {
Column copy = new Column();
copy.setLength( column.getLength() );
copy.setScale( column.getScale() );
Column copy = column.clone();
copy.setValue( manyToOne );
copy.setName( column.getQuotedName() );
copy.setNullable( column.isNullable() );
copy.setPrecision( column.getPrecision() );
copy.setUnique( column.isUnique() );
copy.setSqlType( column.getSqlType() );
copy.setCheckConstraint( column.getCheckConstraint() );
copy.setComment( column.getComment() );
copy.setDefaultValue( column.getDefaultValue() );
copy.setGeneratedAs( column.getGeneratedAs() );
manyToOne.addColumn( copy );
}
mappedByJoin.addProperty( property );
@ -275,25 +264,16 @@ public class OneToOneSecondPass implements SecondPass {
join.setOptional( true );
key.setOnDeleteAction( null );
for ( Column column: otherSideProperty.getValue().getColumns() ) {
Column copy = new Column();
copy.setLength( column.getLength() );
copy.setScale( column.getScale() );
Column copy = column.clone();
copy.setValue( key );
copy.setName( column.getQuotedName() );
copy.setNullable( column.isNullable() );
copy.setPrecision( column.getPrecision() );
copy.setUnique( column.isUnique() );
copy.setSqlType( column.getSqlType() );
copy.setCheckConstraint( column.getCheckConstraint() );
copy.setComment( column.getComment() );
copy.setDefaultValue( column.getDefaultValue() );
column.setGeneratedAs( column.getGeneratedAs() );
key.addColumn( copy );
}
if ( otherSideProperty.getValue() instanceof SortableValue
&& !( (SortableValue) otherSideProperty.getValue() ).isSorted() ) {
if ( otherSideProperty.getValue() instanceof SortableValue ) {
final SortableValue value = (SortableValue) otherSideProperty.getValue();
if ( !value.isSorted() ) {
key.sortProperties();
}
}
persistentClass.addJoin( join );
return join;
}

View File

@ -31,7 +31,7 @@ public class AggregateColumn extends Column {
setSqlType( column.getSqlType() );
setSqlTypeCode( column.getSqlTypeCode() );
uniqueInteger = column.uniqueInteger; //usually useless
setCheckConstraint( column.getCheckConstraint() );
checkConstraint = column.checkConstraint;
setComment( column.getComment() );
setDefaultValue( column.getDefaultValue() );
setGeneratedAs( column.getGeneratedAs() );

View File

@ -334,18 +334,18 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
}
if ( resolution.getValueConverter() != null ) {
column.setSpecializedTypeDeclaration(
resolution.getLegacyResolvedBasicType().getSpecializedTypeDeclaration( dialect )
);
final String declaration = resolution.getLegacyResolvedBasicType().getSpecializedTypeDeclaration(dialect);
if ( declaration != null ) {
column.setSpecializedTypeDeclaration( declaration );
}
}
if ( dialect.supportsColumnCheck() && !column.hasCheckConstraint() ) {
column.setCheckConstraint(
resolution.getLegacyResolvedBasicType().getCheckCondition(
column.getQuotedName( dialect ),
dialect
)
);
final String checkCondition = resolution.getLegacyResolvedBasicType()
.getCheckCondition( column.getQuotedName( dialect ), dialect );
if ( checkCondition != null ) {
column.setCheck( new CheckConstraint( checkCondition ) );
}
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.mapping;
/**
* Represents a table or column level {@code check} constraint.
*
* @author Gavin King
*/
public class CheckConstraint {
private String name;
private String constraint;
public CheckConstraint(String name, String constraint) {
this.name = name;
this.constraint = constraint;
}
public CheckConstraint(String constraint) {
this.constraint = constraint;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getConstraint() {
return constraint;
}
public void setConstraint(String constraint) {
this.constraint = constraint;
}
public String constraintString() {
return name == null
? " check (" + constraint + ")"
: " constraint " + name + " check (" + constraint + ")";
}
}

View File

@ -58,7 +58,7 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn
private Integer sqlTypeCode;
private boolean quoted;
int uniqueInteger;
private String checkConstraint;
CheckConstraint checkConstraint;
private String comment;
private String defaultValue;
private String generatedAs;
@ -490,20 +490,31 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn
return specializedTypeDeclaration != null;
}
@Deprecated(since = "6.2")
public String getCheckConstraint() {
return checkConstraint;
return checkConstraint == null ? null : checkConstraint.getConstraint();
}
public void setCheckConstraint(String checkConstraint) {
this.checkConstraint = checkConstraint;
@Deprecated(since = "6.2")
public void setCheckConstraint(String constraint) {
checkConstraint = constraint == null ? null : new CheckConstraint( constraint );
}
public void setCheck(CheckConstraint check) {
checkConstraint = check;
}
public boolean hasCheckConstraint() {
return checkConstraint != null;
}
@Deprecated(since = "6.2")
public String checkConstraint() {
return checkConstraint == null ? null : " check (" + checkConstraint + ")";
return checkConstraint == null ? null : checkConstraint.constraintString();
}
public CheckConstraint getCheck() {
return checkConstraint;
}
@Override
@ -632,24 +643,27 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn
@Override
public Column clone() {
Column copy = new Column();
copy.setLength( length );
copy.setScale( scale );
copy.setValue( value );
copy.setTypeIndex( typeIndex );
copy.setName( getQuotedName() );
copy.setNullable( nullable );
copy.setPrecision( precision );
copy.setUnique( unique );
copy.setSqlType( sqlTypeName );
copy.setSqlTypeCode( sqlTypeCode );
copy.length = length;
copy.precision = precision;
copy.scale = scale;
copy.value = value;
copy.typeIndex = typeIndex;
copy.name = name;
copy.quoted = quoted;
copy.nullable = nullable;
copy.unique = unique;
copy.sqlTypeName = sqlTypeName;
copy.sqlTypeCode = sqlTypeCode;
copy.uniqueInteger = uniqueInteger; //usually useless
copy.setCheckConstraint( checkConstraint );
copy.setComment( comment );
copy.setDefaultValue( defaultValue );
copy.setGeneratedAs( generatedAs );
copy.setAssignmentExpression( assignmentExpression );
copy.setCustomRead( customRead );
copy.setCustomWrite( customWrite );
copy.checkConstraint = checkConstraint;
copy.comment = comment;
copy.defaultValue = defaultValue;
copy.generatedAs = generatedAs;
copy.assignmentExpression = assignmentExpression;
copy.customRead = customRead;
copy.customWrite = customWrite;
copy.specializedTypeDeclaration = specializedTypeDeclaration;
copy.columnSize = columnSize;
return copy;
}

View File

@ -48,15 +48,15 @@ public class Join implements AttributeContainer, Serializable {
private ExecuteUpdateResultCheckStyle deleteCheckStyle;
@Override
public void addProperty(Property prop) {
properties.add(prop);
declaredProperties.add(prop);
prop.setPersistentClass( getPersistentClass() );
public void addProperty(Property property) {
properties.add( property );
declaredProperties.add( property );
property.setPersistentClass( getPersistentClass() );
}
public void addMappedsuperclassProperty(Property prop) {
properties.add(prop);
prop.setPersistentClass( getPersistentClass() );
public void addMappedsuperclassProperty( Property property ) {
properties.add( property );
property.setPersistentClass( getPersistentClass() );
}
public List<Property> getDeclaredProperties() {
@ -72,8 +72,8 @@ public class Join implements AttributeContainer, Serializable {
return declaredProperties.iterator();
}
public boolean containsProperty(Property prop) {
return properties.contains(prop);
public boolean containsProperty(Property property) {
return properties.contains( property );
}
@Deprecated(since = "6.0")
@ -84,6 +84,7 @@ public class Join implements AttributeContainer, Serializable {
public Table getTable() {
return table;
}
public void setTable(Table table) {
this.table = table;
}
@ -91,6 +92,7 @@ public class Join implements AttributeContainer, Serializable {
public KeyValue getKey() {
return key;
}
public void setKey(KeyValue key) {
this.key = key;
}
@ -116,11 +118,11 @@ public class Join implements AttributeContainer, Serializable {
public void createPrimaryKey() {
//Primary key constraint
PrimaryKey pk = new PrimaryKey( table );
pk.setName( PK_ALIAS.toAliasString( table.getName() ) );
table.setPrimaryKey(pk);
PrimaryKey primaryKey = new PrimaryKey( table );
primaryKey.setName( PK_ALIAS.toAliasString( table.getName() ) );
table.setPrimaryKey(primaryKey);
pk.addColumns( getKey() );
primaryKey.addColumns( getKey() );
}
public int getPropertySpan() {
@ -194,9 +196,8 @@ public class Join implements AttributeContainer, Serializable {
}
public boolean isLazy() {
Iterator<Property> iter = getPropertyIterator();
while ( iter.hasNext() ) {
if ( !iter.next().isLazy() ) {
for ( Property property : properties ) {
if ( !property.isLazy() ) {
return false;
}
}

View File

@ -9,7 +9,6 @@ package org.hibernate.mapping;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@ -26,19 +25,24 @@ import org.hibernate.boot.spi.ClassLoaderAccess;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.engine.OptimisticLockStyle;
import org.hibernate.engine.spi.ExecuteUpdateResultCheckStyle;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.internal.FilterConfiguration;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.collections.JoinedIterator;
import org.hibernate.internal.util.collections.JoinedList;
import org.hibernate.internal.util.collections.SingletonIterator;
import org.hibernate.metamodel.MappingMetamodel;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.jpa.event.spi.CallbackDefinition;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.sql.Alias;
import org.hibernate.sql.Template;
import org.hibernate.type.Type;
import org.hibernate.type.spi.TypeConfiguration;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static java.util.Comparator.comparing;
import static org.hibernate.internal.util.StringHelper.qualify;
import static org.hibernate.internal.util.StringHelper.root;
/**
@ -98,6 +102,8 @@ public abstract class PersistentClass implements AttributeContainer, Serializabl
private Component identifierMapper;
private List<CallbackDefinition> callbackDefinitions;
private final List<CheckConstraint> checkConstraints = new ArrayList<>();
// Custom SQL
private String customSQLInsert;
private boolean customInsertCallable;
@ -727,7 +733,7 @@ public abstract class PersistentClass implements AttributeContainer, Serializabl
final int actualColumns = prop.getColumnSpan();
final int requiredColumns = type.getColumnSpan( mapping );
throw new MappingException(
"Property '" + StringHelper.qualify( getEntityName(), prop.getName() )
"Property '" + qualify( getEntityName(), prop.getName() )
+ "' maps to " + actualColumns + " columns but " + requiredColumns
+ " columns are required (type '" + type.getName()
+ "' spans " + requiredColumns + " columns)"
@ -1219,7 +1225,52 @@ public abstract class PersistentClass implements AttributeContainer, Serializabl
// End of @MappedSuperclass support
public void prepareForMappingModel() {
properties.sort( Comparator.comparing( Property::getName ) );
properties.sort( comparing( Property::getName ) );
}
public void mappingModelReady(MappingMetamodel mappingMetamodel) {
for ( CheckConstraint checkConstraint : checkConstraints ) {
final TypeConfiguration typeConfiguration = mappingMetamodel.getTypeConfiguration();
final SessionFactoryImplementor sessionFactory = typeConfiguration.getSessionFactory();
final List<String> constrainedColumnNames =
Template.collectColumnNames( checkConstraint.getConstraint(), typeConfiguration, sessionFactory );
final Table primary = getTable();
long matches = matchesInTable( constrainedColumnNames, primary );
if ( matches == constrainedColumnNames.size() ) {
// perfect, all columns matched in the primary table
primary.addCheck( checkConstraint );
}
else {
// go searching for a secondary table which better matches
Table table = primary;
long max = matches;
for ( Join join : getJoins() ) {
final Table secondary = join.getTable();
long secondaryMatches = matchesInTable( constrainedColumnNames, secondary );
if ( secondaryMatches > max ) {
table = secondary;
max = secondaryMatches;
}
}
table.addCheck( checkConstraint );
}
}
}
private static long matchesInTable(List<String> names, Table table) {
return table.getColumns().stream()
.filter( col -> col.isQuoted()
? names.contains( col.getName() )
: names.stream().anyMatch( name -> name.equalsIgnoreCase( col.getName() ) )
)
.count();
}
public void addCheckConstraint(CheckConstraint checkConstraint) {
checkConstraints.add( checkConstraint );
}
public List<CheckConstraint> getCheckConstraints() {
return checkConstraints;
}
}

View File

@ -38,6 +38,7 @@ import org.jboss.logging.Logger;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableMap;
import static java.util.stream.Collectors.toList;
/**
* A mapping model object representing a relational database {@linkplain jakarta.persistence.Table table}.
@ -64,7 +65,7 @@ public class Table implements Serializable, ContributableDatabaseObject {
private final Map<String, Index> indexes = new LinkedHashMap<>();
private final Map<String,UniqueKey> uniqueKeys = new LinkedHashMap<>();
private int uniqueInteger;
private final List<String> checkConstraints = new ArrayList<>();
private final List<CheckConstraint> checkConstraints = new ArrayList<>();
private String rowId;
private String subselect;
private boolean isAbstract;
@ -593,8 +594,13 @@ public class Table implements Serializable, ContributableDatabaseObject {
return idValue;
}
@Deprecated(since = "6.2")
public void addCheckConstraint(String constraint) {
checkConstraints.add( constraint );
addCheck( new CheckConstraint( constraint ) );
}
public void addCheck(CheckConstraint check) {
checkConstraints.add( check );
}
public boolean containsColumn(Column column) {
@ -672,7 +678,12 @@ public class Table implements Serializable, ContributableDatabaseObject {
return getCheckConstraints().iterator();
}
@Deprecated(since = "6.2")
public List<String> getCheckConstraints() {
return checkConstraints.stream().map( CheckConstraint::getConstraint ).collect( toList() );
}
public List<CheckConstraint> getChecks() {
return unmodifiableList( checkConstraints );
}

View File

@ -252,6 +252,9 @@ public class MappingMetamodelImpl implements MappingMetamodelImplementor, Metamo
bootModel.getNamedEntityGraphs().values(),
runtimeModelCreationContext
);
bootModel.getEntityBindings().forEach( persistentClass -> persistentClass.mappingModelReady( this ) );
}
private void registerEmbeddableMappingType(MetadataImplementor bootModel) {

View File

@ -15,6 +15,7 @@ import java.util.StringTokenizer;
import org.hibernate.HibernateException;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.query.sqm.function.SqmFunctionDescriptor;
import org.hibernate.query.sqm.function.SqmFunctionRegistry;
import org.hibernate.type.spi.TypeConfiguration;
@ -31,6 +32,8 @@ public final class Template {
private static final Set<String> KEYWORDS = new HashSet<>();
private static final Set<String> BEFORE_TABLE_KEYWORDS = new HashSet<>();
private static final Set<String> FUNCTION_KEYWORDS = new HashSet<>();
public static final String PUNCTUATION = "=><!+-*/()',|&`";
static {
KEYWORDS.add("and");
KEYWORDS.add("or");
@ -128,7 +131,7 @@ public final class Template {
// identifier references.
String symbols = new StringBuilder()
.append( "=><!+-*/()',|&`" )
.append( PUNCTUATION )
.append( WHITESPACE )
.append( dialect.openQuote() )
.append( dialect.closeQuote() )
@ -358,6 +361,40 @@ public final class Template {
return result.toString();
}
public static List<String> collectColumnNames(
String checkConstraint,
TypeConfiguration typeConfiguration,
SessionFactoryImplementor sessionFactory) {
final String template = renderWhereStringTemplate(
checkConstraint,
sessionFactory.getJdbcServices().getDialect(),
typeConfiguration,
sessionFactory.getQueryEngine().getSqmFunctionRegistry()
);
final List<String> names = new ArrayList<>();
int begin = 0;
int match;
while ( ( match = template.indexOf(TEMPLATE, begin) ) >= 0 ) {
int start = match + TEMPLATE.length() + 1;
for ( int loc = start;; loc++ ) {
if ( loc == template.length() - 1 ) {
names.add( template.substring( start ) );
begin = template.length();
break;
}
else {
char ch = template.charAt( loc );
if ( PUNCTUATION.indexOf(ch) >= 0 || WHITESPACE.indexOf(ch) >= 0 ) {
names.add( template.substring( start, loc ) );
begin = loc;
break;
}
}
}
}
return names;
}
// /**
// * Takes the where condition provided in the mapping attribute and interpolates the alias.
// * Handles sub-selects, quoted identifiers, quoted strings, expressions, SQL functions,

View File

@ -101,7 +101,7 @@ class ColumnDefinitions {
}
if ( dialect.supportsColumnCheck() && column.hasCheckConstraint() ) {
definition.append( column.checkConstraint() );
definition.append( column.getCheck().constraintString() );
}
}

View File

@ -20,6 +20,7 @@ import org.hibernate.boot.model.relational.SqlStringGenerationContext;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.aggregate.AggregateSupport;
import org.hibernate.mapping.AggregateColumn;
import org.hibernate.mapping.CheckConstraint;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.Property;
@ -151,8 +152,8 @@ public class StandardTableExporter implements Exporter<Table> {
protected void applyTableCheck(Table table, StringBuilder buf) {
if ( dialect.supportsTableCheck() ) {
for ( String constraint : table.getCheckConstraints() ) {
buf.append( ", check (" ).append( constraint ).append( ')' );
for ( CheckConstraint constraint : table.getChecks() ) {
buf.append( "," ).append( constraint.constraintString() );
}
final AggregateSupport aggregateSupport = dialect.getAggregateSupport();
if ( aggregateSupport != null && aggregateSupport.supportsComponentCheckConstraints() ) {
@ -231,7 +232,8 @@ public class StandardTableExporter implements Exporter<Table> {
}
else {
for ( Column subColumn : value.getColumns() ) {
final String checkConstraint = subColumn.getCheckConstraint();
final CheckConstraint check = subColumn.getCheck();
final String checkConstraint = check == null ? null : check.getConstraint();
if ( !subColumn.isNullable() || checkConstraint != null ) {
final String subColumnName = subColumn.getQuotedName( dialect );
final String columnExpression = aggregateSupport.aggregateComponentCustomReadExpression(