From 2daaf3a660c80939e6b3fe5813e7f79ca0555d64 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Tue, 15 Jul 2025 11:57:10 +0200 Subject: [PATCH 1/9] rework handling of check constraint 'options' on SQL Server --- .../dialect/SQLServerLegacyDialect.java | 26 +++++++++++-------- .../java/org/hibernate/dialect/Dialect.java | 23 +++++++++++----- .../hibernate/dialect/SQLServerDialect.java | 20 +++++++++----- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java index a9ed9ca58bb5..655139699ab6 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java @@ -51,7 +51,6 @@ import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.JdbcExceptionHelper; -import org.hibernate.internal.util.StringHelper; import org.hibernate.mapping.AggregateColumn; import org.hibernate.mapping.CheckConstraint; import org.hibernate.mapping.Column; @@ -98,6 +97,8 @@ import jakarta.persistence.TemporalType; import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; +import static org.hibernate.internal.util.StringHelper.isBlank; +import static org.hibernate.internal.util.StringHelper.isNotEmpty; import static org.hibernate.query.common.TemporalUnit.NANOSECOND; import static org.hibernate.query.sqm.produce.function.FunctionParameterType.INTEGER; import static org.hibernate.type.SqlTypes.*; @@ -1234,19 +1235,22 @@ public boolean supportsFromClauseInUpdate() { @Override public String getCheckConstraintString(CheckConstraint checkConstraint) { + // The only useful option is 'NOT FOR REPLICATION' + // and it comes before the constraint expression final String constraintName = checkConstraint.getName(); - return constraintName == null - ? - " check " + getCheckConstraintOptions( checkConstraint ) + "(" + checkConstraint.getConstraint() + ")" - : - " constraint " + constraintName + " check " + getCheckConstraintOptions( checkConstraint ) + "(" + checkConstraint.getConstraint() + ")"; + final String checkWithName = + isBlank( constraintName ) + ? " check" + : " constraint " + constraintName + " check"; + return appendCheckConstraintOptions( checkConstraint, checkWithName ) + + " (" + checkConstraint.getConstraint() + ")"; } - private String getCheckConstraintOptions(CheckConstraint checkConstraint) { - if ( StringHelper.isNotEmpty( checkConstraint.getOptions() ) ) { - return checkConstraint.getOptions() + " "; - } - return ""; + @Override + public String appendCheckConstraintOptions(CheckConstraint checkConstraint, String sqlCheckConstraint) { + return isNotEmpty( checkConstraint.getOptions() ) + ? sqlCheckConstraint + " " + checkConstraint.getOptions() + : sqlCheckConstraint; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index 0186ada499a2..1effd717dd1c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -10,6 +10,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.HibernateException; import org.hibernate.Incubating; +import org.hibernate.Internal; import org.hibernate.Length; import org.hibernate.LockMode; import org.hibernate.LockOptions; @@ -6031,36 +6032,46 @@ public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSuppor */ public String getCheckConstraintString(CheckConstraint checkConstraint) { final String constraintName = checkConstraint.getName(); - final String constraint = isBlank( constraintName ) - ? " check (" + checkConstraint.getConstraint() + ")" - : " constraint " + constraintName + " check (" + checkConstraint.getConstraint() + ")"; + final String checkWithName = + isBlank( constraintName ) + ? " check" + : " constraint " + constraintName + " check"; + final String constraint = checkWithName + " (" + checkConstraint.getConstraint() + ")"; return appendCheckConstraintOptions( checkConstraint, constraint ); } /** - * Append the {@link CheckConstraint} options to SQL check sqlCheckConstraint + * Append the {@linkplain CheckConstraint#getOptions() options} to the given DDL + * string declaring a SQL {@code check} constraint. * * @param checkConstraint an instance of {@link CheckConstraint} * @param sqlCheckConstraint the SQL to append the {@link CheckConstraint} options * * @return a SQL expression + * + * @since 7.0 */ + @Internal @Incubating public String appendCheckConstraintOptions(CheckConstraint checkConstraint, String sqlCheckConstraint) { return sqlCheckConstraint; } /** - * Does this dialect support appending table options SQL fragment at the end of the SQL Table creation statement? + * Does this dialect support appending table options SQL fragment at the end of the SQL table creation statement? * * @return {@code true} indicates it does; {@code false} indicates it does not; + * + * @since 7.0 */ + @Deprecated(since = "7.1", forRemoval = true) public boolean supportsTableOptions() { return false; } /** * Does this dialect support binding {@link Types#NULL} for {@link PreparedStatement#setNull(int, int)}? - * if it does, then call of {@link PreparedStatement#getParameterMetaData()} could be eliminated for better performance. + * If it does, then the call to {@link PreparedStatement#getParameterMetaData()} may be skipped for + * better performance. * * @return {@code true} indicates it does; {@code false} indicates it does not; * @see org.hibernate.type.descriptor.jdbc.ObjectNullResolvingJdbcType diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index 806bb08543d1..1f401c903d5f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -1219,16 +1219,22 @@ public CallableStatementSupport getCallableStatementSupport() { @Override public String getCheckConstraintString(CheckConstraint checkConstraint) { + // The only useful option is 'NOT FOR REPLICATION' + // and it comes before the constraint expression final String constraintName = checkConstraint.getName(); - return isBlank( constraintName ) - ? " check " + getCheckConstraintOptions( checkConstraint ) - + "(" + checkConstraint.getConstraint() + ")" - : " constraint " + constraintName + " check " + getCheckConstraintOptions( checkConstraint ) - + "(" + checkConstraint.getConstraint() + ")"; + final String checkWithName = + isBlank( constraintName ) + ? " check" + : " constraint " + constraintName + " check"; + return appendCheckConstraintOptions( checkConstraint, checkWithName ) + + " (" + checkConstraint.getConstraint() + ")"; } - private String getCheckConstraintOptions(CheckConstraint checkConstraint) { - return isNotEmpty( checkConstraint.getOptions() ) ? checkConstraint.getOptions() + " " : ""; + @Override + public String appendCheckConstraintOptions(CheckConstraint checkConstraint, String sqlCheckConstraint) { + return isNotEmpty( checkConstraint.getOptions() ) + ? sqlCheckConstraint + " " + checkConstraint.getOptions() + : sqlCheckConstraint; } @Override From bd7ce8d24bed8182793b04787c72df3452ae8ab7 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Tue, 15 Jul 2025 11:57:47 +0200 Subject: [PATCH 2/9] @link -> @linkplain --- .../java/org/hibernate/type/descriptor/java/BasicJavaType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BasicJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BasicJavaType.java index 59233d496f2a..acd88110c9b2 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BasicJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BasicJavaType.java @@ -15,7 +15,7 @@ */ public interface BasicJavaType extends JavaType { /** - * Obtain the "recommended" {@link JdbcType SQL type descriptor} + * Obtain the "recommended" {@linkplain JdbcType SQL type descriptor} * for this Java type. Often, but not always, the source of this * recommendation is the JDBC specification. * From 887a769aaa6152f2f3c67c628a68ac4fdf721ba4 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Tue, 15 Jul 2025 11:59:38 +0200 Subject: [PATCH 3/9] add missing @since --- hibernate-core/src/main/java/org/hibernate/Hibernate.java | 4 +++- hibernate-core/src/main/java/org/hibernate/Session.java | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/Hibernate.java b/hibernate-core/src/main/java/org/hibernate/Hibernate.java index 5d6e0b7843f3..4b3a24feb38e 100644 --- a/hibernate-core/src/main/java/org/hibernate/Hibernate.java +++ b/hibernate-core/src/main/java/org/hibernate/Hibernate.java @@ -118,7 +118,7 @@ private Hibernate() { throw new UnsupportedOperationException(); } - private static final LobHelperImpl lobHelper = new LobHelperImpl(); + private static final LobHelper lobHelper = new LobHelperImpl(); /** * Force initialization of a proxy or persistent collection. In the case of a @@ -601,6 +601,8 @@ else if (collectionClass == Collection.class) { * and {@link java.sql.Clob}. * * @return an instance of {@link LobHelper} + * + * @since 7.1 */ public static LobHelper getLobHelper() { return lobHelper; diff --git a/hibernate-core/src/main/java/org/hibernate/Session.java b/hibernate-core/src/main/java/org/hibernate/Session.java index 9c54178e6d00..59120ec90a75 100644 --- a/hibernate-core/src/main/java/org/hibernate/Session.java +++ b/hibernate-core/src/main/java/org/hibernate/Session.java @@ -1326,8 +1326,7 @@ public interface Session extends SharedSessionContract, EntityManager { * * @return an instance of {@link LobHelper} * - * @deprecated This method will be removed. - * use {@link Hibernate#getLobHelper()} instead + * @deprecated Use {@link Hibernate#getLobHelper()} instead. */ @Deprecated(since="7.0", forRemoval = true) LobHelper getLobHelper(); From 4d39b2074765af1e895ff5f77eb3d50779c96dcd Mon Sep 17 00:00:00 2001 From: Gavin King Date: Tue, 15 Jul 2025 12:10:06 +0200 Subject: [PATCH 4/9] add HHH-19614 to migration guide --- migration-guide.adoc | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/migration-guide.adoc b/migration-guide.adoc index 67b3e9ec0538..3815181db86a 100644 --- a/migration-guide.adoc +++ b/migration-guide.adoc @@ -22,7 +22,19 @@ earlier versions, see any other pertinent migration guides as well. This section describes changes to contracts (classes, interfaces, methods, etc.) which are considered https://hibernate.org/community/compatibility-policy/#api[API]. [[session-getLobHelper]] -==== Session#getLobHelper +=== Session#getLobHelper The `Session#getLobHelper` method has been marked as deprecated in favor of the static `Hibernate#getLobHelper` and will be removed in a future *major* version. +[[ddl-changes]] +== Changes to DDL generation + +This section describes changes to DDL generated by the schema export tooling. +Such changes typically do not impact programs using a relational schema managed externally to Hibernate. + +[[single-table-check]] +=== Automatic check constraints with single table inheritance mappings + +Previously, the non-nullability of the column mapped by an attribute declared `optional=false` by a subclass in a single table inheritance hierarchy was not enforced by the database. +Hibernate now automatically generates DDL `check` constraints to enforce the non-nullability of such columns. + From 809d0396a96f5e03600fbf19c5270ea99f39f389 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Tue, 15 Jul 2025 12:29:03 +0200 Subject: [PATCH 5/9] HHH-19614 Validation @NotNull should mark whole property optional=false --- .../beanvalidation/TypeSafeActivator.java | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java index f84df1666210..13f02edb6182 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java @@ -311,7 +311,7 @@ private static boolean applyConstraints( // Apply Hibernate Validator specific constraints - we cannot import any HV specific classes though! // No need to check explicitly for @Range. @Range is a composed constraint using @Min and @Max which - // will be taken care later. + // will be taken care of later. applyLength( property, descriptor, propertyDesc ); // Composing constraints @@ -360,7 +360,7 @@ private static boolean isConstraintCompositionOfTypeOr( return false; } - final Class composedAnnotation = descriptor.getAnnotation().annotationType(); + final var composedAnnotation = descriptor.getAnnotation().annotationType(); return constraintCompositionTypeCache.computeIfAbsent( composedAnnotation, value -> { for ( Annotation annotation : value.getAnnotations() ) { if ( "org.hibernate.validator.constraints.ConstraintComposition" @@ -383,7 +383,7 @@ private static boolean isConstraintCompositionOfTypeOr( private static void applyMin(Property property, ConstraintDescriptor descriptor, Dialect dialect) { if ( Min.class.equals( descriptor.getAnnotation().annotationType() ) ) { @SuppressWarnings("unchecked") - final ConstraintDescriptor minConstraint = (ConstraintDescriptor) descriptor; + final var minConstraint = (ConstraintDescriptor) descriptor; final long min = minConstraint.getAnnotation().value(); for ( Selectable selectable : property.getSelectables() ) { if ( selectable instanceof Column column ) { @@ -396,7 +396,7 @@ private static void applyMin(Property property, ConstraintDescriptor descript private static void applyMax(Property property, ConstraintDescriptor descriptor, Dialect dialect) { if ( Max.class.equals( descriptor.getAnnotation().annotationType() ) ) { @SuppressWarnings("unchecked") - final ConstraintDescriptor maxConstraint = (ConstraintDescriptor) descriptor; + final var maxConstraint = (ConstraintDescriptor) descriptor; final long max = maxConstraint.getAnnotation().value(); for ( Selectable selectable : property.getSelectables() ) { if ( selectable instanceof Column column ) { @@ -420,8 +420,8 @@ private static void applySQLCheck(Column column, String checkConstraint) { private static boolean isNotNullDescriptor(ConstraintDescriptor descriptor) { final Class annotationType = descriptor.getAnnotation().annotationType(); return NotNull.class.equals(annotationType) - || NotEmpty.class.equals(annotationType) - || NotBlank.class.equals(annotationType); + || NotEmpty.class.equals(annotationType) + || NotBlank.class.equals(annotationType); } private static void markNotNull(Property property) { @@ -429,6 +429,7 @@ private static void markNotNull(Property property) { if ( !( property.getPersistentClass() instanceof SingleTableSubclass ) ) { // composite should not add not-null on all columns if ( !property.isComposite() ) { + property.setOptional( false ); for ( Selectable selectable : property.getSelectables() ) { if ( selectable instanceof Column column ) { column.setNullable( false ); @@ -449,7 +450,7 @@ private static void markNotNull(Property property) { private static void applyDigits(Property property, ConstraintDescriptor descriptor) { if ( Digits.class.equals( descriptor.getAnnotation().annotationType() ) ) { @SuppressWarnings("unchecked") - final ConstraintDescriptor digitsConstraint = (ConstraintDescriptor) descriptor; + final var digitsConstraint = (ConstraintDescriptor) descriptor; final int integerDigits = digitsConstraint.getAnnotation().integer(); final int fractionalDigits = digitsConstraint.getAnnotation().fraction(); for ( Selectable selectable : property.getSelectables() ) { @@ -466,7 +467,7 @@ private static void applySize(Property property, ConstraintDescriptor descrip if ( Size.class.equals( descriptor.getAnnotation().annotationType() ) && String.class.equals( propertyDescriptor.getElementClass() ) ) { @SuppressWarnings("unchecked") - final ConstraintDescriptor sizeConstraint = (ConstraintDescriptor) descriptor; + final var sizeConstraint = (ConstraintDescriptor) descriptor; final int max = sizeConstraint.getAnnotation().max(); for ( Column col : property.getColumns() ) { if ( max < Integer.MAX_VALUE ) { @@ -520,11 +521,11 @@ private static Property findPropertyByName(PersistentClass associatedClass, Stri property = associatedClass.getProperty( element ); } else { - if ( !property.isComposite() ) { - return null; + if ( property.isComposite() ) { + property = ( (Component) property.getValue() ).getProperty( element ); } else { - property = ( (Component) property.getValue() ).getProperty( element ); + return null; } } } @@ -532,7 +533,7 @@ private static Property findPropertyByName(PersistentClass associatedClass, Stri } catch ( MappingException e ) { try { - //if we do not find it try to check the identifier mapper + //if we do not find it, try to check the identifier mapper if ( associatedClass.getIdentifierMapper() == null ) { return null; } @@ -544,11 +545,11 @@ private static Property findPropertyByName(PersistentClass associatedClass, Stri property = associatedClass.getIdentifierMapper().getProperty( element ); } else { - if ( !property.isComposite() ) { - return null; + if ( property.isComposite() ) { + property = ( (Component) property.getValue() ).getProperty( element ); } else { - property = ( (Component) property.getValue() ).getProperty( element ); + return null; } } } @@ -562,8 +563,9 @@ private static Property findPropertyByName(PersistentClass associatedClass, Stri } private static ValidatorFactory getValidatorFactory(ActivationContext context) { - // IMPL NOTE : We can either be provided a ValidatorFactory or make one. We can be provided - // a ValidatorFactory in 2 different ways. So here we "get" a ValidatorFactory in the following order: + // IMPL NOTE: We can either be provided a ValidatorFactory or make one. We can be provided + // a ValidatorFactory in 2 different ways. So here we "get" a ValidatorFactory + // in the following order: // 1) Look into SessionFactoryOptions.getValidatorFactoryReference() // 2) Look into ConfigurationService // 3) build a new ValidatorFactory From 89ed467de35bcd89da3e8d1fc9919833646165af Mon Sep 17 00:00:00 2001 From: Gavin King Date: Tue, 15 Jul 2025 12:29:30 +0200 Subject: [PATCH 6/9] HHH-19614 add some @Basic( optional = false ) to a test just for fun --- .../mapping/inheritance/SingleTableInheritanceTests.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/inheritance/SingleTableInheritanceTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/inheritance/SingleTableInheritanceTests.java index 2a52adf0fa37..02d44274c838 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/inheritance/SingleTableInheritanceTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/inheritance/SingleTableInheritanceTests.java @@ -5,6 +5,8 @@ package org.hibernate.orm.test.mapping.inheritance; import java.util.List; + +import jakarta.persistence.Basic; import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; @@ -170,6 +172,7 @@ public void setId(Integer id) { this.id = id; } + @Basic( optional = false ) public String getName() { return name; } @@ -192,6 +195,7 @@ public DomesticCustomer(Integer id, String name, String taxId) { this.taxId = taxId; } + @Basic( optional = false ) public String getTaxId() { return taxId; } @@ -214,6 +218,7 @@ public ForeignCustomer(Integer id, String name, String vat) { this.vat = vat; } + @Basic( optional = false ) public String getVat() { return vat; } From 5fd2239af9464fbaa3a9e0688f2022c222280012 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Tue, 15 Jul 2025 13:05:11 +0200 Subject: [PATCH 7/9] HHH-19614 update doc --- documentation/src/main/asciidoc/introduction/Mapping.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/src/main/asciidoc/introduction/Mapping.adoc b/documentation/src/main/asciidoc/introduction/Mapping.adoc index c638dd0e049b..fd98763c9d5f 100644 --- a/documentation/src/main/asciidoc/introduction/Mapping.adoc +++ b/documentation/src/main/asciidoc/introduction/Mapping.adoc @@ -57,7 +57,7 @@ Let's put them in a table, so we can more easily compare the points of differenc | `SINGLE_TABLE` | Map every class in the hierarchy to the same table, and uses the value of a _discriminator column_ to determine which concrete class each row represents. | To retrieve instances of a given class, we only need to query the one table. -| Attributes declared by subclasses map to columns without `NOT NULL` constraints. 💀 +| Attributes declared by subclasses map to columns without `NOT NULL` constraints, and so their non-nullability is enforced via a `CHECK` constraint. Any association may have a `FOREIGN KEY` constraint. 🤓 | Subclass data is denormalized. 🧐 From e6c86db656a9b21e8ec7f09766bcafbda30d131a Mon Sep 17 00:00:00 2001 From: Gavin King Date: Tue, 15 Jul 2025 13:05:27 +0200 Subject: [PATCH 8/9] squash some warnings in HANADialect --- .../org/hibernate/dialect/HANADialect.java | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java index 68d3f3c1928e..31c7a1c1d8e1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java @@ -106,6 +106,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; +import java.io.Serial; import java.io.StringReader; import java.io.Writer; import java.nio.charset.StandardCharsets; @@ -886,16 +887,6 @@ public SequenceSupport getSequenceSupport() { return HANASequenceSupport.INSTANCE; } - @Override - public boolean supportsTableCheck() { - return true; - } - - @Override - public boolean supportsTupleDistinctCounts() { - return true; - } - @Override public boolean dropConstraints() { return false; @@ -1413,17 +1404,17 @@ public MaterializedBlob(byte[] bytes) { } @Override - public long length() throws SQLException { + public long length() { return this.getBytes().length; } @Override - public byte[] getBytes(long pos, int length) throws SQLException { + public byte[] getBytes(long pos, int length) { return Arrays.copyOfRange( this.bytes, (int) ( pos - 1 ), (int) ( pos - 1 + length ) ); } @Override - public InputStream getBinaryStream() throws SQLException { + public InputStream getBinaryStream() { return new ByteArrayInputStream( this.getBytes() ); } @@ -1438,7 +1429,7 @@ public long position(Blob pattern, long start) throws SQLException { } @Override - public int setBytes(long pos, byte[] bytes) throws SQLException { + public int setBytes(long pos, byte[] bytes) { int bytesSet = 0; if ( this.bytes.length < pos - 1 + bytes.length ) { this.bytes = Arrays.copyOf( this.bytes, (int) ( pos - 1 + bytes.length ) ); @@ -1450,7 +1441,7 @@ public int setBytes(long pos, byte[] bytes) throws SQLException { } @Override - public int setBytes(long pos, byte[] bytes, int offset, int len) throws SQLException { + public int setBytes(long pos, byte[] bytes, int offset, int len) { int bytesSet = 0; if ( this.bytes.length < pos - 1 + len ) { this.bytes = Arrays.copyOf( this.bytes, (int) ( pos - 1 + len ) ); @@ -1472,17 +1463,17 @@ public OutputStream setBinaryStream(long pos) { } @Override - public void truncate(long len) throws SQLException { + public void truncate(long len) { this.setBytes( Arrays.copyOf( this.getBytes(), (int) len ) ); } @Override - public void free() throws SQLException { + public void free() { this.setBytes( null ); } @Override - public InputStream getBinaryStream(long pos, long length) throws SQLException { + public InputStream getBinaryStream(long pos, long length) { return new ByteArrayInputStream( this.getBytes(), (int) ( pos - 1 ), (int) length ); } @@ -1505,19 +1496,19 @@ public MaterializedNClob(String data) { } @Override - public void truncate(long len) throws SQLException { + public void truncate(long len) { this.data = ""; } @Override - public int setString(long pos, String str, int offset, int len) throws SQLException { + public int setString(long pos, String str, int offset, int len) { this.data = this.data.substring( 0, (int) ( pos - 1 ) ) + str.substring( offset, offset + len ) + this.data.substring( (int) ( pos - 1 + len ) ); return len; } @Override - public int setString(long pos, String str) throws SQLException { + public int setString(long pos, String str) { this.data = this.data.substring( 0, (int) ( pos - 1 ) ) + str + this.data.substring( (int) ( pos - 1 + str.length() ) ); return str.length(); } @@ -1533,32 +1524,32 @@ public OutputStream setAsciiStream(long pos) throws SQLException { } @Override - public long position(Clob searchstr, long start) throws SQLException { + public long position(Clob searchstr, long start) { return this.data.indexOf( extractString( searchstr ), (int) ( start - 1 ) ); } @Override - public long position(String searchstr, long start) throws SQLException { + public long position(String searchstr, long start) { return this.data.indexOf( searchstr, (int) ( start - 1 ) ); } @Override - public long length() throws SQLException { + public long length() { return this.data.length(); } @Override - public String getSubString(long pos, int length) throws SQLException { + public String getSubString(long pos, int length) { return this.data.substring( (int) ( pos - 1 ), (int) ( pos - 1 + length ) ); } @Override - public Reader getCharacterStream(long pos, long length) throws SQLException { + public Reader getCharacterStream(long pos, long length) { return new StringReader( this.data.substring( (int) ( pos - 1 ), (int) ( pos - 1 + length ) ) ); } @Override - public Reader getCharacterStream() throws SQLException { + public Reader getCharacterStream() { return new StringReader( this.data ); } @@ -1568,7 +1559,7 @@ public InputStream getAsciiStream() { } @Override - public void free() throws SQLException { + public void free() { this.data = null; } } @@ -1616,6 +1607,7 @@ protected X doExtract(CallableStatement statement, String name, WrapperOptions o private static class HANAStreamBlobType implements JdbcType { + @Serial private static final long serialVersionUID = -2476600722093442047L; final int maxLobPrefetchSize; @@ -1698,6 +1690,7 @@ public String toString() { } /** serial version uid. */ + @Serial private static final long serialVersionUID = -379042275442752102L; final int maxLobPrefetchSize; @@ -1801,6 +1794,7 @@ public boolean isUseUnicodeStringTypes() { private static class HANANClobJdbcType extends NClobJdbcType { /** serial version uid. */ + @Serial private static final long serialVersionUID = 5651116091681647859L; final int maxLobPrefetchSize; @@ -1899,6 +1893,7 @@ public int getMaxLobPrefetchSize() { public static class HANABlobType implements JdbcType { + @Serial private static final long serialVersionUID = 5874441715643764323L; public static final JdbcType INSTANCE = new HANABlobType( MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE ); From d9ac018fa364ff1ded8dbd4544a46e40cba02a5d Mon Sep 17 00:00:00 2001 From: Gavin King Date: Tue, 15 Jul 2025 15:00:15 +0200 Subject: [PATCH 9/9] HHH-19614 fix bug where @ManyToOne(optional=false) FKs for single table inheritance were 'not null' --- .../boot/internal/InFlightMetadataCollectorImpl.java | 2 +- .../hibernate/boot/model/internal/ColumnsBuilder.java | 2 +- .../org/hibernate/boot/model/internal/EntityBinder.java | 3 +-- .../hibernate/boot/model/internal/PropertyBinder.java | 2 ++ .../org/hibernate/boot/model/internal/ToOneBinder.java | 9 ++++++++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/InFlightMetadataCollectorImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/InFlightMetadataCollectorImpl.java index 10d353909be8..e81e6a83ba28 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/InFlightMetadataCollectorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/InFlightMetadataCollectorImpl.java @@ -1757,7 +1757,7 @@ public void processSecondPasses(MetadataBuildingContext buildingContext) { processFkSecondPassesInOrder(); - processSecondPasses(createKeySecondPassList); + processSecondPasses( createKeySecondPassList ); processSecondPasses( secondaryTableSecondPassList ); processSecondPasses( querySecondPassList ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ColumnsBuilder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ColumnsBuilder.java index 511c67105b1e..dca6d21e6ac6 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ColumnsBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ColumnsBuilder.java @@ -129,7 +129,7 @@ else if ( columnsAnn != null ) { else if ( joinColumns == null && ( property.hasDirectAnnotationUsage( OneToMany.class ) || property.hasDirectAnnotationUsage( ElementCollection.class ) ) ) { - OneToMany oneToMany = property.getDirectAnnotationUsage( OneToMany.class ); + final OneToMany oneToMany = property.getDirectAnnotationUsage( OneToMany.class ); joinColumns = AnnotatedJoinColumns.buildJoinColumns( null, oneToMany == null ? null : nullIfEmpty( oneToMany.mappedBy() ), diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java index 3e8b8aa35dbf..bf13a4c15377 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java @@ -220,12 +220,11 @@ public static void bindEntityClass( superEntity.addSubclass( subclass ); } - persistentClass.createConstraints( context ); - collector.addEntityBinding( persistentClass ); // process secondary tables and complementary definitions (ie o.h.a.Table) collector.addSecondPass( new SecondaryTableFromAnnotationSecondPass( entityBinder, holder ) ); collector.addSecondPass( new SecondaryTableSecondPass( entityBinder, holder ) ); + collector.addSecondPass( ignored -> persistentClass.createConstraints( context ) ); // comment, checkConstraint, and indexes are processed here entityBinder.processComplementaryTableDefinitions(); resolveLifecycleCallbacks( clazzToProcess, persistentClass, context.getMetadataCollector() ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java index ada7ae1230d9..0823642dd01a 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java @@ -859,6 +859,7 @@ private AnnotatedColumns bindProperty( else if ( isManyToOne( property ) ) { bindManyToOne( propertyHolder, + nullability, inferredData, isIdentifierMapper, inSecondPass, @@ -870,6 +871,7 @@ else if ( isManyToOne( property ) ) { else if ( isOneToOne( property ) ) { bindOneToOne( propertyHolder, + nullability, inferredData, isIdentifierMapper, inSecondPass, diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ToOneBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ToOneBinder.java index 4ca09f8bfeb2..7a660ddd516d 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ToOneBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ToOneBinder.java @@ -71,6 +71,7 @@ public class ToOneBinder { static void bindManyToOne( PropertyHolder propertyHolder, + Nullability nullability, PropertyData inferredData, boolean isIdentifierMapper, boolean inSecondPass, @@ -102,6 +103,7 @@ && isIdentifier( propertyHolder, propertyBinder, isIdentifierMapper ) ) { aggregateCascadeTypes( manyToOne.cascade(), hibernateCascade, false, context ), joinColumns, propertyHolder, + nullability, inferredData, manyToOne.fetch(), manyToOne.optional(), @@ -140,6 +142,7 @@ private static void bindManyToOne( EnumSet cascadeStrategy, AnnotatedJoinColumns joinColumns, PropertyHolder propertyHolder, + Nullability nullability, PropertyData inferredData, FetchType fetchType, boolean explicitlyOptional, @@ -172,7 +175,7 @@ private static void bindManyToOne( manyToOne.setNotFoundAction( notFoundAction ); manyToOne.setOnDeleteAction( onDeleteAction ); //value.setLazy( fetchMode != FetchMode.JOIN ); - if ( !optional ) { + if ( !optional && nullability != Nullability.FORCED_NULL ) { for ( AnnotatedJoinColumn column : joinColumns.getJoinColumns() ) { column.setNullable( false ); } @@ -413,6 +416,7 @@ else if ( oneToOne != null ) { static void bindOneToOne( PropertyHolder propertyHolder, + Nullability nullability, PropertyData inferredData, boolean isIdentifierMapper, boolean inSecondPass, @@ -449,6 +453,7 @@ static void bindOneToOne( oneToOne.optional(), oneToOne.fetch(), propertyHolder, + nullability, inferredData, nullIfEmpty( oneToOne.mappedBy() ), trueOneToOne, @@ -465,6 +470,7 @@ private static void bindOneToOne( boolean explicitlyOptional, FetchType fetchMode, PropertyHolder propertyHolder, + Nullability nullability, PropertyData inferredData, String mappedBy, boolean trueOneToOne, @@ -492,6 +498,7 @@ private static void bindOneToOne( cascadeStrategy, joinColumns, propertyHolder, + nullability, inferredData, fetchMode, explicitlyOptional,