@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…

Hi,

Bu I don’t find the conversion of =? in is null when the value is null ?
The code exist ?
Thank you.


For information, the implementation of a version with a null value :

@MappedSuperclass
public class HermesBusinessObject extends GenericBusinessObject {

  @Version
  @Column(insertable = false)
  private Timestamp dateModification;
public class EfluidOracleSqlAstTranslator extends OracleSqlAstTranslator {

  public EfluidOracleSqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) {
    super(sessionFactory, statement);
  }

  @Override
  public JdbcOperation translate(JdbcParameterBindings jdbcParameterBindings, QueryOptions queryOptions) {
    JdbcOperation translate = super.translate(jdbcParameterBindings, queryOptions);
    return new AjoutClauseDateModificationNulle().ajouterClause(translate);
  }
}
public final class AjoutClauseDateModificationNulle {

  public static final String ATTRIBUT_SQL = "sql";
  public static final String CLAUSE_WHERE_DATE_MODIFICATION = " and DATEMODIFICATION=?";
  public static final String CLAUSE_WHERE_DATE_MODIFICATION_AVEC_GESTION_NULLE = " and (DATEMODIFICATION is null or DATEMODIFICATION=? ) ";

  /**
   * Uniquement si requête d'update ou delete
   */
  public JdbcOperation ajouterClause(JdbcOperation abstractJdbcMutation) {
    if (abstractJdbcMutation instanceof JdbcUpdateMutation || abstractJdbcMutation instanceof JdbcDeleteMutation) {
      modifierField(abstractJdbcMutation);
    }
    return abstractJdbcMutation;
  }

  private static void modifierField(final JdbcOperation abstractJdbcMutation) {
    try {
      Field field = AbstractJdbcMutation.class.getDeclaredField(ATTRIBUT_SQL);
      field.setAccessible(true);
      String sql = (String) field.get(abstractJdbcMutation);
      field.set(abstractJdbcMutation, sql.replace(CLAUSE_WHERE_DATE_MODIFICATION, CLAUSE_WHERE_DATE_MODIFICATION_AVEC_GESTION_NULLE));
    } catch (NoSuchFieldException | IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }
}
public class EfluidDeleteCoordinator extends DeleteCoordinator {

  public EfluidDeleteCoordinator(AbstractEntityPersister entityPersister, SessionFactoryImplementor factory) {
    super(entityPersister, factory);
  }

  @Override
  protected void doStaticDelete(Object entity, Object id, Object[] loadedState, Object version, SharedSessionContractImplementor session) {
    super.doStaticDelete(entity, id, loadedState, getVersion(version), session);
  }

  /**
   * Si la version (datemodification) est nulle, alors on applique une fausse date juste pour la construction de la requête.
   * {@link com.efluid.hibernate.AjoutClauseDateModificationNulle}
   */
  public static Object getVersion(Object version) {
    if (version == null) {
      return DateUtils.getDate(1977, 27, 07);
    }
    return version;
  }
}
public class EfluidSingleTableEntityPersister extends SingleTableEntityPersister {

  public EfluidSingleTableEntityPersister(PersistentClass persistentClass, EntityDataAccess cacheAccessStrategy, NaturalIdDataAccess naturalIdRegionAccessStrategy, PersisterCreationContext creationContext) throws HibernateException {
    super(persistentClass, cacheAccessStrategy, naturalIdRegionAccessStrategy, creationContext);
  }

  @Override
  public void delete(Object id, Object version, Object object, SharedSessionContractImplementor session) {
    if (isEtatObjetPresent(object, session)) {
      EfluidSoftDelete.of(this).softDelete(id, EfluidDeleteCoordinator.getVersion(version), object, session);
    } else {
      super.delete(id, version, object, session);
    }
  }

  protected DeleteCoordinator buildDeleteCoordinator() {
    return new EfluidDeleteCoordinator( this, getFactory() );
  }
}

I don’t know what you are doing, but optimistic locking with a null version makes no sense, which is why this wasn’t implemented.

In fact, it is to remain backward compatible during the migration with our old ORM.

We use the creation date and the modification date as the version.
In creation, the creation date is filled in and the modification date is null.
Then we only base ourselves on the date of modification.

We plan to switch everything back with a single column named version of type int, but only after having migrated everything.

For information, performUpdate has protected visibility and has oldVersion parameter, which I can change to a fake value.

But then we’re going to use a dedicated column and remove all that very borderline code.

Thank you.

public class EfluidUpdateCoordinator extends UpdateCoordinatorStandard {

  public EfluidUpdateCoordinator(AbstractEntityPersister entityPersister, SessionFactoryImplementor factory) {
    super(entityPersister, factory);
    gererClauseLockOptimisteAvecValeurNulle();
  }

  private void gererClauseLockOptimisteAvecValeurNulle() {
    if (getStaticUpdateGroup() != null) {
      getStaticUpdateGroup().getOperations().forEach(abstractJdbcMutation -> new AjoutClauseDateModificationNulle().ajouterClause(abstractJdbcMutation));
    }
    if (getVersionUpdateGroup() != null) {
      getVersionUpdateGroup().getOperations().forEach(abstractJdbcMutation -> new AjoutClauseDateModificationNulle().ajouterClause(abstractJdbcMutation));
    }
  }

  @Override
  protected void performUpdate(Object entity, Object id, Object rowId, Object[] values, Object oldVersion, Object[] incomingOldValues, boolean hasDirtyCollection, SharedSessionContractImplementor session, EntityVersionMapping versionMapping, int[] dirtyAttributeIndexes, boolean[] attributeUpdateability, boolean forceDynamicUpdate) {
    if (oldVersion == null) {
      oldVersion = DateUtils.getDate(1977, 27, 07);
    }
    super.performUpdate(entity, id, rowId, values, oldVersion, incomingOldValues, hasDirtyCollection, session, versionMapping, dirtyAttributeIndexes, attributeUpdateability, forceDynamicUpdate);
  }
}