Null embedded ID transient field for multiple returned entities


#1

I have an entity whose embedded ID includes a transient field that is set from a non-transient one within a parameterised constructor. The class of that ID has a field bridge which converts it to/from a string. When a FullTextQuery is issued, the results include projected FullTextQuery.THIS entity documents.

Everything works fine as long as a single entity is returned. For two or more entities, their ID transient fields are set to null. I noticed that changing the database retrieval method from QUERY to FIND_BY_ID makes it work i.e. the ID transient field of each returned entity is correctly set (to a non-null value). Wondering if the behaviour shouldn’t be the same regardless of the retrieval method and the number of returned entities.


#2

I suspect this is not a Hibernate Search issue, but an issue with your code or ORM. Try executing a Hibernate ORM query on this entity, with multiple results; I suspect you will have the same problem.

To be honest, what surprises me most is that this works at all, even partially. I would expect the field to always be null. But it seems in some cases the ID object you created is set on the loaded entity, which leads to your single working case. I suppose in most other cases, a new ID object is built.

The proper way to initialize this transient field, I think, would be to declare a @PostLoad method on the entity, which would call some initialize() method on the ID. But that probably won’t work on proxies.

I guess updating this transient field anytime one of the fields it’s based on is modified would be the safest course of action. Something like that:

@Access(AccessType.PROPERTY)
public class MyId {
  private Integer myField1;
  private Integer myField2;
  @Transient
  private transient Integer myTransientField;

  protected MyId() {
    // For Hibernate
  }

  public MyId(int myField1, int myField2) {
    this.myField1 = myField1;
    this.myField2 = myField2;
    updateTransientField();
  }

  protected int setMyField1(int value) {
    this.myField1 = value;
    updateTransientField();
  }

  protected int setMyField2(int value) {
    this.myField2 = value;
    updateTransientField();
  }

  private void updateTransientField() {
    if ( myField1 != null && myField2 != null ) {
      this.myTransientField = myField1 + myField2;
    }
  }
}

#3

Indeed this seems to be Hibernate ORM related.

The call trace to FullTextQuery.getResultList goes more or less like this:

FullTextQueryImpl.getResultList calls
  FullTextQueryImpl.list calls
    FullTextQueryImpl.doHibernateSearchList calls
      ProjectionLoader.load calls objectLoader.load
        AbstractLoader.load(List<EntityInfo> entityInfos) calls
          QueryLoader.executeLoad(executeLoad(List<EntityInfo> entityInfos))

At this point, the flow diverges for the different database retrieval methods:

For QUERY:

QueryLoader.executeLoad(executeLoad(List<EntityInfo> entityInfos)) calls
  CriteriaObjectInitializer calls CriteriaImpl.list
    CriteriaImpl.list calls SessionImpl.list
      SessionImpl.list calls CriteriaLoader.list
        CriteriaLoader.list calls CriteriaQueryTranslator.getQueryParameters
          CriteriaQueryTranslator.getQueryParameters calls
            Junction.getTypedValue (1)
	CriteriaLoader.list calls Loader.list
          Loader.list calls Loader.listIgnoreQueryCache
            Loader.listIgnoreQueryCache calls Loader.getResultList (2)

(1) returns non-transient fields only i.e. the transient field is lost
(2) based on (1), returns entities with non-transient fields only i.e. without the transient one

For FIND_BY_ID:

QueryLoader.executeLoad calls
  LookUpObjectInitializer calls
    ObjectLoaderHelper.load calls (3)
      ObjectLoaderHelper.executeLoad calls
        SessionImpl.load calls
          SessionImpl.doLoad (4)

(4) returns the entity with all fields (non-transient and transient) set.

Furthermore, before the part where the flow diverges, in the AbstractLoader class, there’s the following logic:

else if ( entityInfos.size() == 1 ) {
  final Object entity = executeLoad( entityInfos.get( 0 ) );
  if ( entity == null ) {
    loadedObjects = Collections.EMPTY_LIST;
  }
  else {
    loadedObjects = Collections.singletonList( entity );
  }
}
else {
  loadedObjects = executeLoad( entityInfos );
}

If a single entity is being loaded (else if block) then the flow goes to (3) i.e. the entity is returned with all fields (non-transient and transient) set. This seems to explain why, although not working for multiple entities, DatabaseRetrievalMethod.QUERY works when a single entity is being returned.


#4

@yrodiere I can create a ticket for this if the Hibernate ORM team considers this a bug or something that could be improved.


#5

@sant0s You can give it a try, but I doubt it will be fixed soon, if at all, since the basic problem is that the your ID class is flawed when used as Hibernate ORM expects (i.e. default constructor, then setters or direct writes to fields).

Did you try the piece of code I gave you in my last message? It could solve your problem.

If it doesn’t, you could open a ticket to ask for something like the @PostLoad annotation, but for embedded IDs instead of entities.

EDIT: this could be of interest to you too: https://hibernate.atlassian.net/browse/HHH-9440


#6

@yrodiere As you mentioned, @PostLoad is unfortunately not working (with neither DatabaseRetrievalMethod.FIND_BY_ID nor DatabaseRetrievalMethod.QUERY). I also tried your suggestion about setting the transient field from a non-transient field setter, but to no avail. Adding a no-args constructor and getters/setters to the ID class doesn’t make it work either. At some point, the setters get actually called, including the transient field one, but later in the flow FullTextQuery returns entities with null (embedded ID) transient fields due to, I guess, the logic I described earlier.

I’ve just created HHH-13052. Thanks for your help.


#7

I also tried your suggestion about setting the transient field from a non-transient field setter, but to no avail.

@sant0s You made me doubt, so I tried it. There were some errors in the snippet I gave you that prevented Hibernate ORM to boot, but once they are fixed, my solution works. Proof: https://github.com/yrodiere/hibernate-test-case-templates/tree/HHH-13052/search/hibernate-search-lucene . Build this directory with mvn clean install, it will execute the tests, and the tests will pass.

Check that you added the @Access annotation to your ID class, and that you added the @java.persistence.Transient annotation on the getter for your transient field, in particular.