From fa06f978e22268258b92372c5af7e7e7eb87b47a Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Mon, 9 Sep 2024 11:35:38 +0800 Subject: [PATCH] HHH-18581 Introduce `supportsBindingNullSqlTypeForSetNull()` and `supportsBindingNullForSetObject()` for `Dialect` to optimize binding null The method `PreparedStatement.getParameterMetaData().getParameterType(int)` call is expensive for some JDBC driver such as pgJDBC, we should avoid it if the driver supports binding `Types.NULL` for `setNull()` or `null` for `setObject()`. --- .../java/org/hibernate/dialect/Dialect.java | 22 ++++ .../java/org/hibernate/dialect/H2Dialect.java | 5 + .../org/hibernate/dialect/MySQLDialect.java | 5 + .../hibernate/dialect/PostgreSQLDialect.java | 5 + .../hibernate/dialect/SQLServerDialect.java | 5 + .../jdbc/ObjectNullResolvingJdbcType.java | 18 +++- .../orm/test/typedescriptor/NullTest.java | 100 ++++++++++++++++++ 7 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/typedescriptor/NullTest.java 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 1d686d28dacd..e6d8b3dd6c7b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -5774,4 +5774,26 @@ public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSuppor return FunctionalDependencyAnalysisSupportImpl.NONE; } + /** + * 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. + * + * @return {@code true} indicates it does; {@code false} indicates it does not; + * @see org.hibernate.type.descriptor.jdbc.ObjectNullResolvingJdbcType + */ + public boolean supportsBindingNullSqlTypeForSetNull() { + return false; + } + + /** + * Does this dialect support binding {@code null} for {@link PreparedStatement#setObject(int, Object)}? + * if it does, then call of {@link PreparedStatement#getParameterMetaData()} could be eliminated for better performance. + * + * @return {@code true} indicates it does; {@code false} indicates it does not; + * @see org.hibernate.type.descriptor.jdbc.ObjectNullResolvingJdbcType + */ + public boolean supportsBindingNullForSetObject() { + return false; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index 6aa5aba5726a..e9c299548105 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -1028,4 +1028,9 @@ public String getDual() { return "dual"; } + @Override + public boolean supportsBindingNullSqlTypeForSetNull() { + return true; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index 20ce0f69b5eb..44102a28ad72 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -1577,4 +1577,9 @@ public boolean supportsFromClauseInUpdate() { public String getDual() { return "dual"; } + + @Override + public boolean supportsBindingNullSqlTypeForSetNull() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index 404f10609cba..1baa19a23e8c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -1609,4 +1609,9 @@ public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { public boolean supportsFromClauseInUpdate() { return true; } + + @Override + public boolean supportsBindingNullSqlTypeForSetNull() { + return true; + } } 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 1ecf96529d22..ec129e43a5ff 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -1202,4 +1202,9 @@ public CallableStatementSupport getCallableStatementSupport() { return SQLServerCallableStatementSupport.INSTANCE; } + + @Override + public boolean supportsBindingNullForSetObject() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ObjectNullResolvingJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ObjectNullResolvingJdbcType.java index 00e6a3f14d7c..b1fd1bdb0178 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ObjectNullResolvingJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ObjectNullResolvingJdbcType.java @@ -42,13 +42,27 @@ public ValueBinder getBinder(JavaType javaType) { @Override protected void doBindNull(PreparedStatement st, int index, WrapperOptions options) throws SQLException { - st.setNull( index, st.getParameterMetaData().getParameterType( index ) ); + if ( options.getDialect().supportsBindingNullForSetObject() ) { + st.setObject( index, null ); + } + else { + final int sqlType = options.getDialect().supportsBindingNullSqlTypeForSetNull() ? Types.NULL + : st.getParameterMetaData().getParameterType( index ); + st.setNull( index, sqlType ); + } } @Override protected void doBindNull(CallableStatement st, String name, WrapperOptions options) throws SQLException { - st.setNull( name, Types.JAVA_OBJECT ); + if ( options.getDialect().supportsBindingNullForSetObject() ) { + st.setObject( name, null ); + } + else { + final int sqlType = options.getDialect().supportsBindingNullSqlTypeForSetNull() ? Types.NULL + : Types.JAVA_OBJECT; + st.setNull( name, sqlType ); + } } @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/typedescriptor/NullTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/typedescriptor/NullTest.java new file mode 100644 index 000000000000..77aa747e025a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/typedescriptor/NullTest.java @@ -0,0 +1,100 @@ +/* + * 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 . + */ +package org.hibernate.orm.test.typedescriptor; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * @author Yanming Zhou + */ +@DomainModel( + annotatedClasses = NullTest.SimpleEntity.class +) +@SessionFactory +public class NullTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> session.persist( new SimpleEntity() ) + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> + session.createMutationQuery( "delete from SimpleEntity" ).executeUpdate() + ); + } + + @Test + @JiraKey("HHH-18581") + public void passingNullAsParameterOfNativeQuery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + SimpleEntity persisted = session.createNativeQuery( + "select * from SimpleEntity where name is null or name=:name", + SimpleEntity.class + ).setParameter( "name", null ).uniqueResult(); + + assertNotNull( persisted ); + } + ); + } + + @Test + public void passingNullAsParameterOfQuery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + SimpleEntity persisted = session.createQuery( + "from SimpleEntity where name is null or name=:name", + SimpleEntity.class + ).setParameter( "name", null ).uniqueResult(); + + assertNotNull( persisted ); + } + ); + } + + @Entity(name = "SimpleEntity") + static class SimpleEntity { + @Id + @GeneratedValue + private Integer id; + + private String name; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +}