HHH-15958 much better support for @RowId annotation

- the rowid pseudo-column and type are now determined automatically from Dialect
- works (after all these years) in Postgres (and also on h2)
- introduce RowIdJdbcType (not strictly necessary, but a nicety)
This commit is contained in:
Gavin 2023-01-01 04:26:02 +01:00 committed by Gavin King
parent 6da38d0b05
commit 689cca1963
13 changed files with 167 additions and 22 deletions

View File

@ -15,6 +15,9 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Specifies that an Oracle-style {@code rowid} should be used in SQL
* {@code update} statements for an entity, instead of the primary key.
* <p>
* If the {@linkplain org.hibernate.dialect.Dialect SQL dialect} does
* not support some sort of {@code rowid}, this annotation is ignored.
*
* @author Steve Ebersole
*/
@ -25,6 +28,10 @@ public @interface RowId {
* Specifies the {@code rowid} identifier.
* <p>
* For example, on Oracle, this should be just {@code "rowid"}.
*
* @deprecated the {@code rowid} identifier is now inferred
* automatically from the {@link org.hibernate.dialect.Dialect}
*/
String value();
@Deprecated(since = "6.2")
String value() default "";
}

View File

@ -215,6 +215,7 @@ import static org.hibernate.type.SqlTypes.NCLOB;
import static org.hibernate.type.SqlTypes.NUMERIC;
import static org.hibernate.type.SqlTypes.NVARCHAR;
import static org.hibernate.type.SqlTypes.REAL;
import static org.hibernate.type.SqlTypes.ROWID;
import static org.hibernate.type.SqlTypes.SMALLINT;
import static org.hibernate.type.SqlTypes.TIME;
import static org.hibernate.type.SqlTypes.TIMESTAMP;
@ -4622,4 +4623,23 @@ public abstract class Dialect implements ConversionContext {
ServiceRegistryImplementor registry) {
return new HibernateSchemaManagementTool();
}
/**
* The name of a {@code rowid}-like pseudo-column which
* acts as a high-performance row locator, or null if
* this dialect has no such pseudo-column.
*/
public String rowId() {
return null;
}
/**
* The JDBC type code of the {@code rowid}-like pseudo-column
* which acts as a high-performance row locator.
*
* @return {@link Types#ROWID} by default
*/
public int rowIdSqlType() {
return ROWID;
}
}

View File

@ -75,6 +75,7 @@ import jakarta.persistence.TemporalType;
import static org.hibernate.query.sqm.TemporalUnit.SECOND;
import static org.hibernate.type.SqlTypes.ARRAY;
import static org.hibernate.type.SqlTypes.BIGINT;
import static org.hibernate.type.SqlTypes.BINARY;
import static org.hibernate.type.SqlTypes.CHAR;
import static org.hibernate.type.SqlTypes.DECIMAL;
@ -856,4 +857,14 @@ public class H2Dialect extends Dialect {
public UniqueDelegate getUniqueDelegate() {
return uniqueDelegate;
}
@Override
public String rowId() {
return "_rowid_";
}
@Override
public int rowIdSqlType() {
return BIGINT;
}
}

View File

@ -1406,4 +1406,9 @@ public class OracleDialect extends Dialect {
public String getCreateUserDefinedTypeKindString() {
return "object";
}
@Override
public String rowId() {
return "rowid";
}
}

View File

@ -1349,6 +1349,16 @@ public class PostgreSQLDialect extends Dialect {
// disabled foreign key constraints still prevent 'truncate table'
// (these would help if we used 'delete' instead of 'truncate')
@Override
public String rowId() {
return "ctid";
}
@Override
public int rowIdSqlType() {
return OTHER;
}
// @Override
// public String getDisableConstraintsStatement() {
// return "set constraints all deferred";

View File

@ -6,9 +6,9 @@
*/
package org.hibernate.metamodel.mapping.internal;
import java.sql.Types;
import java.util.function.BiConsumer;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.mapping.IndexedConsumer;
import org.hibernate.metamodel.mapping.EntityMappingType;
@ -41,9 +41,9 @@ public class EntityRowIdMappingImpl implements EntityRowIdMapping {
this.rowIdName = rowIdName;
this.tableExpression = tableExpression;
this.declaringType = declaringType;
this.rowIdType = declaringType.getEntityPersister().getFactory().getTypeConfiguration()
.getBasicTypeRegistry()
.resolve( Object.class, Types.ROWID );
final SessionFactoryImplementor factory = declaringType.getEntityPersister().getFactory();
this.rowIdType = factory.getTypeConfiguration().getBasicTypeRegistry()
.resolve( Object.class, factory.getJdbcServices().getDialect().rowIdSqlType() );
}
@Override

View File

@ -544,7 +544,13 @@ public abstract class AbstractEntityPersister
rootTableKeyColumnReaderTemplates = new String[identifierColumnSpan];
identifierAliases = new String[identifierColumnSpan];
rowIdName = persistentClass.getRootTable().getRowId();
final String rowId = persistentClass.getRootTable().getRowId();
if ( rowId == null ) {
rowIdName = null;
}
else {
rowIdName = rowId.isEmpty() ? dialect.rowId() : rowId;
}
if ( persistentClass.getLoaderName() != null ) {
// We must resolve the named query on-demand through the boot model because it isn't initialized yet

View File

@ -0,0 +1,77 @@
/*
* 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.type.descriptor.jdbc;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.descriptor.ValueBinder;
import org.hibernate.type.descriptor.ValueExtractor;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.JavaType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.RowId;
import java.sql.SQLException;
import java.sql.Types;
/**
* Descriptor for {@link Types#ROWID ROWID} handling.
*
* @author Gavin King
*/
public class RowIdJdbcType implements JdbcType {
public static final RowIdJdbcType INSTANCE = new RowIdJdbcType();
@Override
public int getJdbcTypeCode() {
return SqlTypes.ROWID;
}
@Override
public String toString() {
return "RowIdJdbcType";
}
@Override
public <X> ValueBinder<X> getBinder(JavaType<X> javaType) {
return new BasicBinder<>( javaType, this ) {
@Override
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options)
throws SQLException {
st.setRowId( index, getJavaType().unwrap( value, RowId.class, options ) );
}
@Override
protected void doBind(CallableStatement st, X value, String name, WrapperOptions options)
throws SQLException {
st.setRowId( name, getJavaType().unwrap( value, RowId.class, options ) );
}
};
}
@Override
@SuppressWarnings("unchecked")
public <X> ValueExtractor<X> getExtractor(JavaType<X> javaType) {
return new BasicExtractor<>( javaType, this ) {
@Override
protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException {
return getJavaType().wrap( rs.getRowId( paramIndex ), options );
}
@Override
protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException {
return getJavaType().wrap( statement.getRowId( index ), options );
}
@Override
protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException {
return getJavaType().wrap( statement.getObject( name ), options );
}
};
}
}

View File

@ -15,7 +15,6 @@ import java.sql.Types;
import org.hibernate.type.descriptor.ValueBinder;
import org.hibernate.type.descriptor.ValueExtractor;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.BasicJavaType;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.jdbc.internal.JdbcLiteralFormatterNumericData;
import org.hibernate.type.spi.TypeConfiguration;

View File

@ -25,6 +25,7 @@ import org.hibernate.type.descriptor.jdbc.LongVarbinaryJdbcType;
import org.hibernate.type.descriptor.jdbc.LongVarcharJdbcType;
import org.hibernate.type.descriptor.jdbc.NumericJdbcType;
import org.hibernate.type.descriptor.jdbc.RealJdbcType;
import org.hibernate.type.descriptor.jdbc.RowIdJdbcType;
import org.hibernate.type.descriptor.jdbc.SmallIntJdbcType;
import org.hibernate.type.descriptor.jdbc.TimeJdbcType;
import org.hibernate.type.descriptor.jdbc.TimestampJdbcType;
@ -84,5 +85,7 @@ public class JdbcTypeBaseline {
target.addDescriptor( Types.LONGNVARCHAR, LongVarcharJdbcType.INSTANCE );
target.addDescriptor( Types.NCLOB, ClobJdbcType.DEFAULT );
target.addDescriptor( new LongVarcharJdbcType(SqlTypes.LONG32NVARCHAR) );
target.addDescriptor( RowIdJdbcType.INSTANCE );
}
}

View File

@ -19,7 +19,6 @@
* <ul>
* <li>{@link java.sql.Types#DATALINK DATALINK}</li>
* <li>{@link java.sql.Types#DISTINCT DISTINCT}</li>
* <li>{@link java.sql.Types#ROWID ROWID}</li>
* <li>{@link java.sql.Types#REF REF}</li>
* <li>{@link java.sql.Types#REF_CURSOR REF_CURSOR}</li>
* </ul>

View File

@ -13,11 +13,9 @@ import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.RowId;
import org.hibernate.dialect.OracleDialect;
import org.hibernate.testing.jdbc.SQLStatementInspector;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.RequiresDialect;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.BeforeEach;
@ -33,14 +31,13 @@ import static org.junit.Assert.assertThat;
*/
@DomainModel( annotatedClasses = RowIdTest.Product.class )
@SessionFactory(statementInspectorClass = SQLStatementInspector.class)
@RequiresDialect( value = OracleDialect.class)
public class RowIdTest {
@BeforeEach
void setUp(SessionFactoryScope scope) {
scope.inTransaction( session -> {
Product product = new Product();
product.setId( 1L );
product.setId( "1L" );
product.setName( "Mobile phone" );
product.setNumber( "123-456-7890" );
session.persist( product );
@ -51,15 +48,19 @@ public class RowIdTest {
void testRowId(SessionFactoryScope scope) {
final String updatedName = "Smart phone";
scope.inTransaction( session -> {
String rowId = scope.getSessionFactory().getJdbcServices().getDialect().rowId();
SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector();
statementInspector.clear();
Product product = session.find( Product.class, 1L );
Product product = session.find( Product.class, "1L" );
List<String> sqls = statementInspector.getSqlQueries();
assertThat( sqls, hasSize( 1 ) );
assertThat( sqls.get(0).matches( "(?i).*\\bselect\\b.+\\.ROWID.*\\bfrom\\s+product\\b.*" ), is( true ) );
assertThat( rowId == null
|| sqls.get(0).matches( "(?i).*\\bselect\\b.+\\." + rowId + ".*\\bfrom\\s+product\\b.*" ),
is( true ) );
assertThat( product.getName(), not( is( updatedName ) ) );
@ -71,7 +72,9 @@ public class RowIdTest {
sqls = statementInspector.getSqlQueries();
assertThat( sqls, hasSize( 1 ) );
assertThat( sqls.get( 0 ).matches( "(?i).*\\bupdate\\s+product\\b.+?\\bwhere\\s+ROWID\\s*=.*" ), is( true ) );
assertThat( rowId == null
|| sqls.get( 0 ).matches( "(?i).*\\bupdate\\s+product\\b.+?\\bwhere\\s+" + rowId + "\\s*=.*" ),
is( true ) );
} );
scope.inTransaction( session -> {
@ -82,11 +85,11 @@ public class RowIdTest {
@Entity(name = "Product")
@Table(name = "product")
@RowId("ROWID")
@RowId
public static class Product {
@Id
private Long id;
private String id;
@Column(name = "`name`")
private String name;
@ -94,11 +97,11 @@ public class RowIdTest {
@Column(name = "`number`")
private String number;
public Long getId() {
public String getId() {
return id;
}
public void setId(Long id) {
public void setId(String id) {
this.id = id;
}

View File

@ -17,8 +17,6 @@ import org.hibernate.dialect.NationalizationSupport;
import org.hibernate.dialect.PostgreSQLDialect;
import org.hibernate.dialect.TiDBDialect;
import org.hibernate.testing.orm.junit.DialectFeatureCheck;
/**
* Container class for different implementation of the {@link DialectCheck} interface.
*
@ -312,4 +310,11 @@ abstract public class DialectChecks {
return dialect.supportsRecursiveCTE();
}
}
public static class SupportsRowId implements DialectCheck {
@Override
public boolean isMatch(Dialect dialect) {
return dialect.rowId() != null;
}
}
}