Save with fetch - fieldenms/tg GitHub Wiki

Save-with-fetch is an entity companion method for saving entities using an explicit refetching strategy. It is a flexible alternative to the ordinary save method.

In Issue #2612, save-with-fetch has become public API. Any companion that supports save-with-fetch should implement interface ISaveWithFetch.

interface ISaveWithFetch<T extends AbstractEntity<?>> {
    Either<Long, T> save(T entity, Optional<fetch<T>> maybeFetch);
}

How to implement save-with-fetch

There are several approaches to implementing save-with-fetch.

The simplest approach is to view save-with-fetch as an additive extension to the ordinary save. To adopt this view it is helpful to understand the following:

1. Within the implementation of save-with-fetch, CommonEntityDao.save(entity) must not be used (i.e., the ordinary super.save), because such a call will lead to non-termination.

Entity save(Entity entity, Optional<fetch<Entity>> maybeFetch) {
  // This is a programming error.
  super.save(entity);
  // This is correct.
  super.save(entity, maybeFetch);
}

Converting from ordinary save

// Replace each intermediate save
super.save(entity)
// with
super.save(entity, FetchModelReconstructor.reconstruct(entity)).asRight().value()

The emphasis here is on intermediate, which means those super.save calls that precede the final super.save call to return a saved entity from the save method. The simplest of companions do not use any intermediate save operations, while more complex ones, with non-trivial business logic, often use intermediate save operations.

To reiterate, the only exception to this conversion rule is when the result of super.save is returned from the save method. In that case, maybeFetch must be used to abide by the contract of save-with-fetch.

2. Save-with-fetch must return an entity by refetching it with maybeFetch (or a wider fetch model).

If maybeFetch is present, it is the lower bound for a fetch model that must be used for refetching the entity before returning it. If maybeFetch is absent, only the entity ID must be returned.

Converting from ordinary save

// Replace
return entity;
// with
return super.save(entity, maybeFetch);

Note that entity may be any entity-typed expression, not strictly a local variable.

This conversion rule is simple and correct, but not always the best in terms of performance. It is sub-optimal if return super.save(entity, maybeFetch) is preceded by another, intermediate save operation, because it will save and refetch an unmodified entity that has just been saved.

Entity save(Entity entity, Optional<fetch<Entity>> maybeFetch) {
  var updated = updateTotals(entity);
  return super.save(updated, maybeFetch);
}

Entity updateTotals(Entity entity) {
  entity.setTotal(...);
  return super.save(entity, Optional.of(FetchModelReconstructor.reconstruct(entity))).asRight().value();
}

In the example above, the save method performs 2 save operations: one in updateTotals and another one in return from save-with-fetch. This could be optimised by performing a "tail fetching optimisation" by merging the two fetching operations into one.

Entity save(Entity entity, Optional<fetch<Entity>> maybeFetch) {
  return updateTotals(entity, maybeFetch);
}

Either<Long, Entity> updateTotals(Entity entity, Optional<fetch<Entity>> maybeFetch) {
  entity.setTotal(...);
  return super.save(entity, maybeFetch);
}

With the optimisation applied, the entity is saved and refetched only once.

In general, there may be multiple intermediate save operations within a save-with-fetch method. The above example could have more methods like updateTotals that save entity. If that were so, the tail fetching optimisation would have to be applied only to the last such method call, hence "tail" in the name.

Entity save(Entity entity, Optional<fetch<Entity>> maybeFetch) {
  var entityFirst = updateFirst(entity);
  ...
  var entityMore = updateMore(entity...);
  return updateLast(entityMore, maybeFetch);
}

Either<Long, Entity> updateLast(Entity entity, Optional<fetch<Entity>> maybeFetch) {
  entity.set...
  return super.save(entity, maybeFetch);
}

Overriding ordinary save

In some cases, when overriding the ordinary save is necessary, it should be annotated with @SkipVerification to suppress verification errors.

When overriding the ordinary save is necessary:

  1. For JMC Job entities (e.g., DfaEscalationJob), the ordinary save must be overridden, as it is annotated with @JobLifecycleParams which can be applied only to the ordinary save.

  2. To avoid creating a session when save(entity) is called.

    @Override
    @SkipVerification // Overridden to remove @SessionRequired.
    public Entity save(Entity entity) {
        return super.save(entity);
    }
    
    @Override
    // @SessionRequired, but not here
    public Either<Long, Entity> save(Entity entity, Optional<fetch<Entity>> maybeFetch) {
      ...
    }
⚠️ **GitHub.com Fallback** ⚠️