2nd-level cache does not work as expected

We are trying to enable 2nd level cache in HBN application and we have faced with a couple of following issues:

#1 DefaultInitializeCollectionEventListener does not honour owner version when initializing collection from cache

I’m actually not sure the scenario below is valid from ORM perspective, however I do believe there is a room for improvement.

Say we have an entity class:

@NoArgsConstructor
@Setter
@Getter
public class TestEntity {

    @Id
    private Long id;

    @Version
    private Long stamp;

    @Setter(AccessLevel.NONE)
    @ElementCollection
    @CollectionTable(name = "test_entity_rows"
    @OrderColumn(name = "position")
    private List<String> rows = new ArrayList<>();

}

and some entities of that class get cached, now, for some reason, I can’t really explain, we would like to modify entity via direct SQL updates, since all such entities are backed up by version stamp I do except that direct SQL updates which increase version stamp as well should be “safe”:

  • application either sees previous version of entity and will fail to modify it
  • application loads new consistent entity state from DB

Unfortunately, that does not work as expected: DefaultInitializeCollectionEventListener fails to check “embedded collection version”, which is stored in AbstractReadWriteAccess.Item, when it loads collection from cache, and as a result we are getting new owner with stale collection.

#2 CollectionLoadContext does not honour hibernate.enable_lazy_load_no_trans setting

The problem is when hibernate.enable_lazy_load_no_trans is set to true and CollectionLoadContext tries to put collection into 2nd level cache it expects to find collection owner in temporary persistence context:

// !!! collection owner is always null when 
// !!! hibernate.enable_lazy_load_no_trans=true
if ( collectionOwner == null ) {
    throw new HibernateException(
            "Unable to resolve owner of loading collection [" +
                    MessageHelper.collectionInfoString( persister, lce.getCollection(), lce.getKey(), session ) +
                    "] for second level caching"
    );
}

#3 Detaching entities cause Hibernate to remove NaturalId mapping from cache

Here I’m not sure what is actually wrong and how that should work in case of detach:

  • SessionImpl#detach calls SessionImpl#evict which in turn fires evict event
  • DefaultEvictEventListener#doEvict calls PersistenceContext.NaturalIdHelper#handleEviction, as a result NaturalId mapping gets removed from 2nd-level cache, although our goal was just detach entity
  • successive new EvictVisitor( session, object ).process( object, persister ); call in DefaultEvictEventListener actually does nothing

#4 AbstractSharedSessionContract returns wrong instance of CacheTransactionSynchronization

There is a special handling of TransactionCoordinator in AbstractSharedSessionContract for cases when application is doing something like:

try(Session nestedSession = existingSession.sessionWithOptions()
        .connection()
        .openSession()) {
    // do some work within nestedSession
}

However, Hibernate does not perform similar handling for CacheTransactionSynchronization, as a result, when nested session modifies data and “commits transaction” it’s own instance of CacheTransactionSynchronization starts notifying global cache about changes (please check infinispan implementation for example) although data was not yet committed to DB.

#5 CacheEntityLoaderHelper fails to update NaturalId mapping in persistence context

The problem is when 2nd-level cache is enabled, entities have natural id and CacheEntityLoaderHelper#convertCacheEntryToEntity method gets called, it injects entities into persistence context and fires post load event:

persistenceContext.addEntry(
        entity,
        ( isReadOnly ? Status.READ_ONLY : Status.MANAGED ),
        values,
        null,
        entityId,
        version,
        LockMode.NONE,
        true,
        subclassPersister,
        false
);
subclassPersister.afterInitialize( entity, session );
persistenceContext.initializeNonLazyCollections();

//PostLoad is needed for EJB3
PostLoadEvent postLoadEvent = event.getPostLoadEvent()
        .setEntity( entity )
        .setId( entityId )
        .setPersister( persister );

and, unfortunately, it fails to update/load Natural Id mapping (neither direct calls of PersistenceContext.NaturalIdHelper nor firing load event), that would be just performance penalty if NaturalIdXrefDelegate didn’t perform validation of Natural Id in NaturalIdXrefDelegate#validateNaturalId, however in our case we are getting NullPointer exceptions in NaturalIdXrefDelegate#validateNaturalId if entities were loaded from cache via MultiIdentifierLoadAccess

Looks like you found a bunch of potential bugs. Please create individual issues for every problem in the issue tracker(https://hibernate.atlassian.net) with a test case(hibernate-test-case-templates/JPAUnitTestCase.java at main · hibernate/hibernate-test-case-templates · GitHub) that reproduces the issue.