From 596debed4d301de84a207f2d0fa7fda9043908cb Mon Sep 17 00:00:00 2001 From: Gavin King Date: Sun, 9 Jan 2022 22:32:43 +0100 Subject: [PATCH] make @Check work when applied at the field level This was a bug! Also add checkConstraint member to @Table to allow check constraints on secondary tables Also clean up some Javadoc of some related annotations --- .../hibernate/userguide/schema/CheckTest.java | 10 +-- .../java/org/hibernate/annotations/Check.java | 8 +- .../org/hibernate/annotations/ForeignKey.java | 13 ++-- .../java/org/hibernate/annotations/Index.java | 2 +- .../java/org/hibernate/annotations/Table.java | 77 +++++++++++++------ .../org/hibernate/cfg/AnnotatedColumn.java | 36 ++++++++- .../hibernate/cfg/AnnotatedJoinColumn.java | 11 +-- .../cfg/annotations/EntityBinder.java | 3 + 8 files changed, 116 insertions(+), 44 deletions(-) diff --git a/documentation/src/test/java/org/hibernate/userguide/schema/CheckTest.java b/documentation/src/test/java/org/hibernate/userguide/schema/CheckTest.java index 8f740070b3..f16546666a 100644 --- a/documentation/src/test/java/org/hibernate/userguide/schema/CheckTest.java +++ b/documentation/src/test/java/org/hibernate/userguide/schema/CheckTest.java @@ -12,7 +12,8 @@ import jakarta.persistence.PersistenceException; import org.hibernate.annotations.Check; import org.hibernate.annotations.NaturalId; -import org.hibernate.dialect.PostgreSQL81Dialect; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.exception.ConstraintViolationException; import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; @@ -26,7 +27,8 @@ import static org.junit.Assert.fail; /** * @author Vlad Mihalcea */ -@RequiresDialect(PostgreSQL81Dialect.class) +@RequiresDialect(PostgreSQLDialect.class) +@RequiresDialect(H2Dialect.class) public class CheckTest extends BaseEntityManagerFunctionalTestCase { @Override @@ -81,7 +83,6 @@ public class CheckTest extends BaseEntityManagerFunctionalTestCase { } @Entity(name = "Person") - @Check(constraints = "code > 0") public static class Person { @Id @@ -89,8 +90,7 @@ public class CheckTest extends BaseEntityManagerFunctionalTestCase { private String name; - // This one does not work! Only the entity-level annotation works. - // @Check(constraints = "code > 0") + @Check(constraints = "code > 0") private Long code; public Long getId() { diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Check.java b/hibernate-core/src/main/java/org/hibernate/annotations/Check.java index 261e871ffa..71b19d6e56 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Check.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Check.java @@ -15,7 +15,11 @@ import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * Arbitrary SQL CHECK constraints which can be defined at the class, property or collection level. + * Specifies a {@code check} constraint to be included in the generated DDL. + * * * @author Emmanuel Bernard */ @@ -23,7 +27,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; @Retention(RUNTIME) public @interface Check { /** - * The check constraints string. + * The check constraint, written in native SQL. */ String constraints(); } diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/ForeignKey.java b/hibernate-core/src/main/java/org/hibernate/annotations/ForeignKey.java index 6b9e3b05fc..184e1fb2ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/ForeignKey.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/ForeignKey.java @@ -15,23 +15,24 @@ import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * Define the foreign key name. + * Specifies a foreign key name. * - * @deprecated Prefer the JPA 2.1 introduced {@link jakarta.persistence.ForeignKey} instead. + * @deprecated use the JPA 2.1 {@link jakarta.persistence.ForeignKey} annotation */ @Target({FIELD, METHOD, TYPE}) @Retention(RUNTIME) @Deprecated public @interface ForeignKey { /** - * Name of the foreign key. Used in OneToMany, ManyToOne, and OneToOne - * relationships. Used for the owning side in ManyToMany relationships + * Name of the foreign key of a {@code OneToMany}, {@code ManyToOne}, or + * {@code OneToOne} association. May also be applied to the owning side a + * {@code ManyToMany} association. */ String name(); /** - * Used for the non-owning side of a ManyToMany relationship. Ignored - * in other relationships + * Used for the non-owning side of a {@code ManyToMany} association. + * Ignored for other association cardinalities. */ String inverseName() default ""; } diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Index.java b/hibernate-core/src/main/java/org/hibernate/annotations/Index.java index 9584262e5d..f1bdb6832f 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Index.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Index.java @@ -13,7 +13,7 @@ import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * Define a DB index. + * Defines an index of a database table. * * @author Emmanuel Bernard * diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Table.java b/hibernate-core/src/main/java/org/hibernate/annotations/Table.java index fa17e8128d..fb62aa833e 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Table.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Table.java @@ -14,82 +14,111 @@ import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * Complementary information to a table either primary or secondary. + * Complementary information for a table declared using the {@link jakarta.persistence.Table}, + * or {@link jakarta.persistence.SecondaryTable} annotation. Usually used only for secondary + * tables. * * @author Emmanuel Bernard + * + * @see jakarta.persistence.Table + * @see jakarta.persistence.SecondaryTable */ -@Target({TYPE}) +@Target(TYPE) @Retention(RUNTIME) @Repeatable(Tables.class) public @interface Table { /** - * name of the targeted table. + * The name of the targeted table. */ String appliesTo(); /** * Indexes. + * + * @deprecated use {@link jakarta.persistence.Table#indexes()} or + * {@link jakarta.persistence.SecondaryTable#indexes()} */ + @Deprecated Index[] indexes() default {}; /** - * Define a table comment. + * A check constraint, written in native SQL. + * + * @see Check + */ + String checkConstraint() default ""; + + /** + * Specifies comment to add to the generated DDL for the table. * * @see Comment */ String comment() default ""; /** - * Defines the Foreign Key name of a secondary table pointing back to the primary table. + * Specifies a foreign key of a secondary table, which points back to the primary table. + * + * @deprecated use {@link jakarta.persistence.SecondaryTable#foreignKey()} */ + @Deprecated ForeignKey foreignKey() default @ForeignKey( name="" ); /** - * If set to JOIN, the default, Hibernate will use an inner join to retrieve a - * secondary table defined by a class or its superclasses and an outer join for a - * secondary table defined by a subclass. - * If set to select then Hibernate will use a - * sequential select for a secondary table defined on a subclass, which will be issued only if a row - * turns out to represent an instance of the subclass. Inner joins will still be used to retrieve a - * secondary defined by the class and its superclasses. - * - * Only applies to secondary tables + * Defines a fetching strategy for the secondary table. + * + *

+ * Only applies to secondary tables. */ FetchMode fetch() default FetchMode.JOIN; /** - * If true, Hibernate will not try to insert or update the properties defined by this join. - * - * Only applies to secondary tables + * If enabled, Hibernate will never insert or update the columns of the secondary table. + *

+ * Only applies to secondary tables. */ boolean inverse() default false; /** - * If enabled, Hibernate will insert a row only if the properties defined by this join are non-null - * and will always use an outer join to retrieve the properties. - * - * Only applies to secondary tables + * If enabled, Hibernate will insert a row only if the columns of the secondary 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. + *

+ * Only applies to secondary tables.

*/ boolean optional() default true; /** * Defines a custom SQL insert statement. + *

+ * Only applies to secondary tables. * - * Only applies to secondary tables + * @see SQLInsert */ SQLInsert sqlInsert() default @SQLInsert(sql=""); /** * Defines a custom SQL update statement. + *

+ * Only applies to secondary tables. * - * Only applies to secondary tables + * @see SQLUpdate */ SQLUpdate sqlUpdate() default @SQLUpdate(sql=""); /** * Defines a custom SQL delete statement. + *

+ * Only applies to secondary tables. * - * Only applies to secondary tables + * @see SQLDelete */ SQLDelete sqlDelete() default @SQLDelete(sql=""); } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedColumn.java b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedColumn.java index 5c6e5f2ff0..3782cdf9ff 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedColumn.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedColumn.java @@ -11,6 +11,7 @@ import java.util.Map; import org.hibernate.AnnotationException; import org.hibernate.AssertionFailure; import org.hibernate.MappingException; +import org.hibernate.annotations.Check; import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.GeneratedColumn; import org.hibernate.annotations.ColumnTransformer; @@ -73,6 +74,7 @@ public class AnnotatedColumn { private String generatedAs; private String comment; + private String checkConstraint; public void setTable(Table table) { this.table = table; @@ -193,10 +195,18 @@ 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 String getComment() { return comment; } @@ -229,6 +239,9 @@ public class AnnotatedColumn { if ( defaultValue != null ) { mappingColumn.setDefaultValue( defaultValue ); } + if ( checkConstraint !=null ) { + mappingColumn.setCheckConstraint( checkConstraint ); + } if ( StringHelper.isNotEmpty( comment ) ) { mappingColumn.setComment( comment ); } @@ -266,6 +279,7 @@ public class AnnotatedColumn { this.mappingColumn.setNullable( nullable ); this.mappingColumn.setSqlType( sqlType ); this.mappingColumn.setUnique( unique ); + this.mappingColumn.setCheckConstraint( checkConstraint ); if ( writeExpression != null ) { final int numberOfJdbcParams = StringHelper.count( writeExpression, '?' ); @@ -645,6 +659,7 @@ public class AnnotatedColumn { column.setBuildingContext( context ); column.applyColumnDefault( inferredData, length ); column.applyGeneratedAs( inferredData, length ); + column.applyCheckConstraint( inferredData, length ); column.extractDataFromPropertyData(inferredData); column.bind(); columns[index] = column; @@ -685,7 +700,25 @@ public class AnnotatedColumn { } else { LOG.trace( - "Could not perform @ColumnGeneratedAlways lookup as 'PropertyData' did not give access to XProperty" + "Could not perform @GeneratedColumn lookup as 'PropertyData' did not give access to XProperty" + ); + } + } + + private void applyCheckConstraint(PropertyData inferredData, int length) { + final XProperty xProperty = inferredData.getProperty(); + if ( xProperty != null ) { + Check columnDefaultAnn = xProperty.getAnnotation( Check.class ); + if ( columnDefaultAnn != null ) { + if (length!=1) { + throw new MappingException("@Check may only be applied to single-column mappings (use a table-level @Check)"); + } + setCheckConstraint( columnDefaultAnn.constraints() ); + } + } + else { + LOG.trace( + "Could not perform @Check lookup as 'PropertyData' did not give access to XProperty" ); } } @@ -761,6 +794,7 @@ public class AnnotatedColumn { column.setImplicit( implicit ); column.applyColumnDefault( inferredData, 1 ); column.applyGeneratedAs( inferredData, 1 ); + column.applyCheckConstraint( inferredData, 1 ); column.extractDataFromPropertyData( inferredData ); column.bind(); diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedJoinColumn.java b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedJoinColumn.java index 7af1bf71b6..0741e63258 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedJoinColumn.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotatedJoinColumn.java @@ -894,18 +894,19 @@ public class AnnotatedJoinColumn extends AnnotatedColumn { * @param column the referenced column. */ public void overrideFromReferencedColumnIfNecessary(Column column) { - if (getMappingColumn() != null) { + Column mappingColumn = getMappingColumn(); + if (mappingColumn != null) { // columnDefinition can also be specified using @JoinColumn, hence we have to check // whether it is set or not if ( StringHelper.isEmpty( sqlType ) ) { sqlType = column.getSqlType(); - getMappingColumn().setSqlType( sqlType ); + mappingColumn.setSqlType( sqlType ); } // these properties can only be applied on the referenced column - we can just take them over - getMappingColumn().setLength(column.getLength()); - getMappingColumn().setPrecision(column.getPrecision()); - getMappingColumn().setScale(column.getScale()); + mappingColumn.setLength( column.getLength() ); + mappingColumn.setPrecision( column.getPrecision() ); + mappingColumn.setScale( column.getScale() ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/EntityBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/EntityBinder.java index 1210f9ea77..93fe2b15df 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/EntityBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/EntityBinder.java @@ -1230,6 +1230,9 @@ public class EntityBinder { if ( !BinderHelper.isEmptyAnnotationValue( table.comment() ) ) { hibTable.setComment( table.comment() ); } + if ( !BinderHelper.isEmptyAnnotationValue( table.checkConstraint() ) ) { + hibTable.addCheckConstraint( table.checkConstraint() ); + } TableBinder.addIndexes( hibTable, table.indexes(), context ); }