@Version with null value or on two attributes

Hello,
I’m still migrating our old framework and implementing the optimistic lock.

Our current optimize lock is based on two attributes, the creation date and the modification date which are two columns in bases.

  1. Persist
    I tried to handle null version on modification date but it doesn’t work well because it is always initialized (During the persist :AbstractSaveEventListener#Versioning#seedVersion) and I read that it is not advisable to have null version.

  2. Flush
    I tried to intervene also in EntityInsertAction during the flush, but same I can’t change the behavior.

  3. Type
    Maybe by creating a specific type but I don’t have access to the object to retrieve the dates. (TimestampCustomType extends TimestampType)

  4. Two columns
    I don’t think it is possible to manage the lock on two attributes.
    As with a :
    @Version
    getDateCreationOrDateModification()
    And then in database idem (With SingleTableEntityPersister#sqlVersionSelectString)

Any idea how to handle this case?

Thanking you

I also thought of modifying the SQLUpdate query to add to the or null version column.

But I didn’t find a way to add this to the query, only create a query from scratch with @SQLUpdate

My problem is that the condition is with = and it doesn’t work with =null because it would need is null

DateModification is the @Version attribute.

update TOBJETHIBERNATE set ACTEURCREATION=‘DEV001|DEV|test’, ACTEURMODIFICATION=‘DEV001|DEV|test’, ACTEURSUPPRESSION=NULL, CODEFOURNISSEUR=‘F-UEM’, CODEGRD=‘G-UEM’, DATECREATION=timestamp ‘2022-07-05 18:15:17.696’, DATEMODIFICATION=timestamp ‘2022-07-05 18:15:18.175’, DATESUPPRESSION=NULL, ETATOBJET=0, ACTIVITE=6, AMOUNT_CURRENCY=NULL, AMOUNT_VALUE=NULL, AMPERAGE_UNIT=NULL, AMPERAGE_VALUE=NULL, APPLICATION=NULL, AREA_UNIT=NULL, AREA_VALUE=NULL, BOOL=0, DATEBDD=NULL, DATEHEURE=NULL, DUREE_UNIT=NULL, DUREE_VALUE=NULL, ENERGY_UNIT=NULL, ENERGY_VALUE=NULL, FLOW_UNIT=NULL, FLOW_VALUE=NULL, GAZCAPACITY_UNIT=NULL, GAZCAPACITY_VALUE=NULL, IMPEDANCE_UNIT=NULL, IMPEDANCE_VALUE=NULL, LENGTH_UNIT=NULL, LENGTH_VALUE=NULL, LISTENUM=NULL, MASS_UNIT=NULL, MASS_VALUE=NULL, POWER_UNIT=NULL, POWER_VALUE=NULL, PRESSURE_UNIT=NULL, PRESSURE_VALUE=NULL, REFTO_ID=NULL, STRING=NULL, VOLTAGE_UNIT=NULL, VOLTAGE_VALUE=NULL, VOLUME_UNIT=NULL, VOLUME_VALUE=NULL where ID=‘2771000’ and DATEMODIFICATION=NULL

Here is the solution I implemented.

I leveraged a SessionFactory Observer and am modifying the SQLStringUpdate to accept null values

What do you think ?

Properties :
- Map.entry(AvailableSettings.SESSION_FACTORY_OBSERVER, EfluidSessionFactoryObserver.class.getName())

public class EfluidSessionFactoryObserver implements SessionFactoryObserver {
  @Override
  public void sessionFactoryCreated(SessionFactory factory) {
    new VersionningAccepteValeurNulle().execute(factory.getMetamodel());
  }
}

public class VersionningAccepteValeurNulle {

  public void execute(final Metamodel metamodel) {
    if (metamodel instanceof MetamodelImpl metamodelImpl) {
      metamodelImpl.entityPersisters().values().forEach(this::ajouterPredicatASQLUpdate);
    }
  }

  private void ajouterPredicatASQLUpdate(final EntityPersister entityPersister) {
    if (entityPersister instanceof AbstractEntityPersister abstractEntityPersister) {
      ajouterOrDateModificationIsNull(abstractEntityPersister.getSQLUpdateStrings());
      ajouterOrDateModificationIsNull(abstractEntityPersister.getSQLLazyUpdateStrings());
    }
  }

  private void ajouterOrDateModificationIsNull(final String[] sqlUpdateStrings) {
    if (sqlUpdateStrings != null) {
      for (int i = 0; i < sqlUpdateStrings.length; i++) {
        if (sqlUpdateStrings[i] != null) {
          sqlUpdateStrings[i] = sqlUpdateStrings[i].replaceFirst("and DATEMODIFICATION=\\?", "and (DATEMODIFICATION is null or DATEMODIFICATION=\\?)");
        }
      }
    }
  }
}

With another modification to make it work :

public class HermesBusinessObject {
...
  @Version
  @Generated(NEVER)
  private Timestamp dateModification;
...
}


public class EfluidInterceptor extends EmptyInterceptor {

  @Override
  public Boolean isTransient(Object entity) {
    return entity instanceof HermesBusinessObject objetMetier ? !objetMetier.isStored() : super.isTransient(entity);
  }
}

and… (with a implementation custom of StandardPersisterClassResolver)

public class EfluidDelete extends Delete {

  @Override
  public String toStatementString() {
    String sqlDelete = super.toStatementString();
    return sqlDelete.replaceFirst(versionColumnName+"=\\?", "("+versionColumnName +" is null or "+versionColumnName+"=\\?)");
  }
}

I wouldn’t do any of what you did so far. Can’t you just initialize all modification dates to the creation date? You could create insert/update triggers that set the modification date to the creation date if the modification date is null. After creating this trigger, you run an update statement update tbl t set t.modificationDate = t.creationDate where t.modificationDate is null. That way, the default Hibernate behavior should just work fine.

My solution is too invasive I agree.
To simplify, I would have to create a version column of type int for all the tables.
But we already have more than 1000 tables…