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
This commit is contained in:
Gavin King 2022-01-09 22:32:43 +01:00
parent bd8bf15e00
commit 596debed4d
8 changed files with 116 additions and 44 deletions

View File

@ -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() {

View File

@ -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.
* <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.
* </ul>
*
* @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();
}

View File

@ -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 "";
}

View File

@ -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
*

View File

@ -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.
*
* <b>Only applies to secondary tables</b>
* Defines a fetching strategy for the secondary table.
* <ul>
* <li>If set to {@link FetchMode#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.
* <li>If set to {@link FetchMode#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 table defined by the class and its superclasses.
* </ul>
* <p>
* <em>Only applies to secondary tables.</em>
*/
FetchMode fetch() default FetchMode.JOIN;
/**
* If true, Hibernate will not try to insert or update the properties defined by this join.
*
* <b>Only applies to secondary tables</b>
* If enabled, Hibernate will never insert or update the columns of the secondary table.
* <p>
* <em>Only applies to secondary tables.</em>
*/
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.
*
* <b>Only applies to secondary tables</b>
* 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.
* <p>
* <em>Only applies to secondary tables.<p></em>
*/
boolean optional() default true;
/**
* Defines a custom SQL insert statement.
* <p>
* <em>Only applies to secondary tables.</em>
*
* <b>Only applies to secondary tables</b>
* @see SQLInsert
*/
SQLInsert sqlInsert() default @SQLInsert(sql="");
/**
* Defines a custom SQL update statement.
* <p>
* <em>Only applies to secondary tables.</em>
*
* <b>Only applies to secondary tables</b>
* @see SQLUpdate
*/
SQLUpdate sqlUpdate() default @SQLUpdate(sql="");
/**
* Defines a custom SQL delete statement.
* <p>
* <em>Only applies to secondary tables.</em>
*
* <b>Only applies to secondary tables</b>
* @see SQLDelete
*/
SQLDelete sqlDelete() default @SQLDelete(sql="");
}

View File

@ -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();

View File

@ -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() );
}
}

View File

@ -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 );
}