Search code examples
javasql-serverhibernatejpaspring-data-jpa

Hibernate: Multiple @Version columns andfields when using inheritance


I have a problem with @Version when dealing with inheritance in Hibernate.

I use InheritanceType.JOINED solution.

I have a BaseEntity which defines a common fields - including @Version:

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

  @Id
  @GeneratedValue(generator = "uuid")
  @GenericGenerator(name = "uuid", strategy = "uuid")
  private String id;

  @Column(nullable = false)
  @Temporal(TemporalType.TIMESTAMP)
  @CreatedDate
  private Date createdAt;

  @Column
  @Temporal(TemporalType.TIMESTAMP)
  @LastModifiedDate
  private Date updatedAt;

  @Version @Builder.Default private long version = 1;
}

Credential entity which is a "middle" part in this hierarchy. It's a parent for UserIdCredentials entity below:

@Entity
@Table(name = "credentials")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Credential extends BaseEntity {
  @ManyToOne
  @JoinColumn(name = "online_account_id")
  private OnlineAccount onlineAccount;
}

UserIdCredentials which is the lowest member of the inheritance:

@Entity
@Table(
    name = "userid_credentials",
    uniqueConstraints = {@UniqueConstraint(columnNames = "username")})
@PrimaryKeyJoinColumn(name = "id")
public class UserIdCredentials extends Credential {
  @Column(unique = true)
  private String username;

  @Transient private String password;
  @Column private String passwordEncoded;
}

Because I use a InheritanceType.JOINED, every class above has its own DB table and therefore version column. Here is my DDL:

Here is my DDL:

REATE TABLE credentials (
    id VARCHAR(255) NOT NULL PRIMARY KEY,
    online_account_id VARCHAR(255) NOT NULL,

    created_at DATETIME NOT NULL,
    updated_at DATETIME,
    version BIGINT NOT NULL,

    FOREIGN KEY (online_account_id) REFERENCES online_accounts(id)
);

CREATE TABLE userid_credentials (
    id VARCHAR(255) NOT NULL PRIMARY KEY,
    username VARCHAR(255) NOT NULL,
    password_encoded VARCHAR(60) NOT NULL,

    created_at DATETIME NOT NULL,
    updated_at DATETIME,
    version BIGINT NOT NULL,

    CONSTRAINT uc_username UNIQUE (username),
    FOREIGN KEY (id) REFERENCES credentials(id)
);

But after I save a UserIdCredentials entity I get an error:

    com.microsoft.sqlserver.jdbc.SQLServerException: Cannot insert the value NULL into column 'version', table 'testdb.dbo.userid_credentials'; column does not allow nulls. INSERT fails.
    at com.microsoft.sqlserver.jdbc.SQLServerException.makeFromDatabaseError(SQLServerException.java:262) ~[mssql-jdbc-8.4.0.jre14.jar:na]
    at com.microsoft.sqlserver.jdbc.SQLServerStatement.getNextResult(SQLServerStatement.java:1632) ~[mssql-jdbc-8.4.0.jre14.jar:na]
    at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement.doExecutePreparedStatement(SQLServerPreparedStatement.java:602) ~[mssql-jdbc-8.4.0.jre14.jar:na]
    at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement$PrepStmtExecCmd.doExecute(SQLServerPreparedStatement.java:524) ~[mssql-jdbc-8.4.0.jre14.jar:na]
    at com.microsoft.sqlserver.jdbc.TDSCommand.execute(IOBuffer.java:7375) ~[mssql-jdbc-8.4.0.jre14.jar:na]
    at com.microsoft.sqlserver.jdbc.SQLServerConnection.executeCommand(SQLServerConnection.java:3200) ~[mssql-jdbc-8.4.0.jre14.jar:na]
    at com.microsoft.sqlserver.jdbc.SQLServerStatement.executeCommand(SQLServerStatement.java:247) ~[mssql-jdbc-8.4.0.jre14.jar:na]
    at com.microsoft.sqlserver.jdbc.SQLServerStatement.executeStatement(SQLServerStatement.java:222) ~[mssql-jdbc-8.4.0.jre14.jar:na]
    at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement.executeUpdate(SQLServerPreparedStatement.java:473) ~[mssql-jdbc-8.4.0.jre14.jar:na]
    at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61) ~[HikariCP-3.4.5.jar:na]
    at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java) ~[HikariCP-3.4.5.jar:na]
    at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:197) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3254) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3779) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:107) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:723) ~[na:na]
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:475) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:348) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:102) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1360) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:451) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:3210) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2378) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:447) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:183) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$300(JdbcResourceLocalTransactionCoordinatorImpl.java:40) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:281) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101) ~[hibernate-core-5.4.17.Final.jar:5.4.17.Final]
    at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:534) ~[spring-orm-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:743) ~[spring-tx-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711) ~[spring-tx-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:632) ~[spring-tx-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:386) ~[spring-tx-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118) ~[spring-tx-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
   ...

It looks like hibernate uses only a version field declared in a BaseEntity, meaning when Hibernate executes inserts to credentials and userid_credentials tables, only first one gets non-null version value and insert statement to userid_credentials has null value so it fails.

When I remove common fields (version, updated_at and created_ad) from userid_credentials - then everything works just fine.

But is it the proper way to deal with @Version field? I think it should be updated in both tables.


Solution

  • The Javadoc of javax.persistence.Version says:

    Only a single Version property or field should be used per class; applications that use more than one Version property or field will not be portable.

    Even if you try to add a version2-Field to the Child-Class, you'll get an exception.

    We had the same Issue regarding @Version and @Inheritance(strategy = InheritanceType.JOINED).

    However, we observed that with every update of the Child-Object e.g. UserIdCredentials the Parent-Table of Credential also gets updated, and in that update, the version-Property is used.

    You can verify that yourself by setting the Log-Level of org.hibernate.SQL to DEBUG and org.hibernate.type.descriptor.sql to TRACE - this will log the SQL-Query and all used SQL-Parameters.

    We ended up omitting the version-column for the Child-Table and added a Unittest (with an In-Memory-DB) that verifies, that an update on a old version of the Child-Object will cause an PessimisticLockException.