TwoWayFieldBridge on composite ID referring to another entity


#1

Consider an indexed entity SomeEntity with a composite ID referring to a non-indexed entity SomeOtherEntity:

@Entity
@Indexed
public class SomeEntity ... {
	@Id
	@FieldBridge(impl = CompositeIdBridge.class)
	private CompositeId id;
	...
}

@Embeddable
public class CompositeId ... {
	@ManyToOne(...)
	@JoinColumn(...)
	@IndexedEmbedded(includeEmbeddedObjectId = true)
	private SomeOtherEntity idPart1;

	private String idPart2;

	private String idPart3;
	...
	public CompositeId() { ... }
	public CompositeId(SomeOtherEntity idPart1, String idPart2, String idPart3) { ... }

)

@Entity
public class SomeOtherEntity ... {
	@Id
	public Long id;
}

public class CompositeIdBridge implements TwoWayStringBridge {

	@Override
	public String objectToString(Object object) {
			CompositeId compositeId = (CompositeId) object;
			return String.format("%s.%s.%s", compositeId.getIdPart1().getId(), compositeId.getIdPart2(), compositeId.getIdPart3());
	}

	@Override
	public Object stringToObject(String stringValue) {
			String[] compositeIdProperties = stringValue.split("\\.");
			return new CompositeId(SomeOtherEntity.findById(Long.parseLong(compositeIdProperties[0])), compositeIdProperties[1], compositeIdProperties[2]);
	}

}

Is retrieving SomeOtherEntity instances in the stringToObject method an acceptable approach?

The goal is to search for (indexed) SomeEntity documents and then retrieve the associated (non-indexed) SomeOtherEntity entities from the DB.


#2

So… several issues here.

First, you will have a hard time implementing the SomeOtherEntity.findById static method properly.
If it is generated by some framework that can somehow retrieve the entity manager from some thread-local context, then ignore this comment: it should just work.
Otherwise, you’ll probably want to use some dependency injection framework. CDI integration should come out of the box for Hibernate Search 5.10, and Spring integration should come soon (as soon as they implement support for JPA 2.2). In those cases, you should be able to just inject the entity manager using @PersistenceContext.

Second, you need to use the @EmbeddedId annotation on SomeEntity.id, not the @Id annotation.

Third, I didn’t even know you could use associations in an ID… Does it even work? It seems weird.
If you can change your schema, I would advise against putting any kind of association in your ID.
One option would be to keep your embeddable, but don’t make it an ID. Put a unique index on it if you must ensure unicity, but use a generated, technical identifier instead of this embeddable. See http://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#identifiers-generators

Finally, even if it works, I am not sure performance will be great, especially when you don’t need to use “SomeOtherEntity” in the results. Unless of course there are only a small number of “SomeOtherEntity” instances and they can be cached in some second-level cache.
I don’t have a solution for this, unfortunately: it comes with the weird composite ID containing an association.

So I guess the question is: can you change your schema?


#3
  1. Yes, I am using a framework that takes care of that.

  2. Isn’t the @Id + @Embeddable approach to composite keys valid anymore? It’s been working ok so far.
    http://docs.jboss.org/hibernate/annotations/3.5/reference/en/html/entity.html#d0e2177

  3. Yes, it’s possible and it works. You may want to take a look at Example 149 of the Hibernate ORM user guide under section 2.6.3. Composite identifiers with @EmbeddedId (*).

  4. Yes, I can change the schema. There seems to have been some debate between using a technical ID as you mention and a natural composite ID (for example, “Why are composite keys discouraged in hibernate?” in Stack Overflow (*)). Is the Hibernate recommendation to always use the former? Unicity at the DB level must be guaranteed so I will keep your unique index suggestion in mind.

  5. Each SomeEntity returned in the (paginated) search results points to a single SomeOtherEntity. However, because different SomeEntities may refer to the same SomeOtherEntity, some (post-search) filtering is required:

Query fullTextQuery = fullTextEntityManager.createFullTextQuery(searchQuery, SomeEntity.class);
List<SomeEntity> someEntities = fullTextQuery.setFirstResult(...).setMaxResults(...).getResultList();
List<SomeOtherEntity> someOtherEntities = someEntities.stream().map(s -> s.getId().getIdPart1()).distinct().collect(Collectors.toList());

(wondering if this distinct-like filtering could be achieved at the Hibernate Search level)

As you wrote, SomeOtherEntity instances are not included in the results and that would be the ideal case. Because the relationship between SomeOtherEntity and SomeEntity is actually “one-to-few”, I started with @OneToMany between them. The SomeOtherEntity index included several SomeOtherEntities and I could search

“SomeOtherEntities having SomeEntities with property1 matching X OR property2 matching Y”

but not

“SomeOtherEntities having SomeEntities with property1 matching X AND property2 matching Y”

which is a problem that nested objects solve (www.elastic.co/guide/en/elasticsearch/guide/current/nested-objects.html). Because Hibernate Search does not yet support nested objects, I ended up turning to the “other side” of the relationship in order to be able to 1) perform AND-like queries on separate SomeEntity documents and 2) retrieve the corresponding SomeOtherEntity ones.

(*) There’s a limit of 2 links per post, so I had to replace some URLs.


#4

Isn’t the @Id + @Embeddable approach to composite keys valid anymore? It’s been working ok so far.

Ok, I didn’t know about this syntax. It may be supported for historical reasons, since it doesn’t seem to be part of the JPA spec. The fact that you had to look for Hibernate 3.5 documentation to find an example seems to check out :slight_smile:
@EmbeddedId would probably be more portable, but yes, if it works, it works.

Regarding the technical ID or composite/natural key problem the accepted answer to the StackOverflow question you mentioned is spot on in my opinion. But if I were you, I wouldn’t worry too much about performance in this case: I mentioned it, but it’s not the main problem. The main problem is how cumbersome composite keys are in general, but especially if they include an association.
You just experienced it with Hibernate Search: having to retrieve an entity in order to build the primary key of another entity is quite annoying. And I’m sure this is not the end of it: when handling parameters to a web page, when exporting data to other systems (in JSON, XML) and receiving data back, …
The thing is, with a technical ID + unique constraint, you will have the choice: you can still retrieve entities by some unique, composite key even if it’s not their primary key. Without a technical ID, you don’t have any choice.
As to “Is the Hibernate recommendation to always use the former [technical IDs instead of composite/natural IDs]?”, I wouldn’t speak for the Hibernate ORM guys, but maybe @vlad can give you an answer.

(wondering if this distinct-like filtering could be achieved at the Hibernate Search level)

The distinct-like filtering cannot be achieved at the Hibernate Search level at the moment. In order to work properly with paging it would require non-trivial pre-processing during search (aggregations), which we didn’t have time to work on yet. It doesn’t seem to be solvable in Elasticsearch, but we’ll have to look into it. You can follow progress on the aggregation topic on HSEARCH-3003 and progress on the “distinct” topic on HSEARCH-868.

Regarding the nested documents, we are aware that Hibernate Search lacks such support, and they are being implemented for both the Lucene integration and the Elasticsearch integration in Hibernate Search 6. So hopefully this should not be a problem anymore when Hibernate Search 6 is out (which should still take some time). In the meantime, your analysis and solution are correct: you’ll have to reverse the index.

There is only one other potential solution you could try, but it is a bit hackish and cannot always be applied: if you know you will have to look for two properties at the same time, and know there is a character that does not appear in either property, you can simply index the concatenation of property1 + '<the character>' + property2. A bit hackish, like I said, and clearly not a solution to every problem.

So to answer your original question:

Is retrieving SomeOtherEntity instances in the stringToObject method an acceptable approach?

Yes it’s acceptable if it works (which mostly depend on SomeOtherEntity.findById using the right entity manager). Performance may be worse than with IDs that do not contain any association. Technical IDs would be less trouble, though.


#5

An update: I recently stumbled upon the @javax.persistence.MapsId annotation while working on something else. It might be exactly what you need to have a composite, natural identifier without introducing associations within the identifier. From the Javadoc:

      // parent entity has simple primary key
  
      @Entity
      public class Employee {
         @Id long empId;
         String name;
         ...
      } 
  
      // dependent entity uses EmbeddedId for composite key
  
      @Embeddable
      public class DependentId {
         String name;
         long empid;   // corresponds to primary key type of Employee
      }
  
      @Entity
      public class Dependent {
         @EmbeddedId DependentId id;
          ...
         @MapsId("empid")  //  maps the empid attribute of embedded id
         @ManyToOne Employee emp;
      }

#6

I ended up moving from searching (indexed) SomeEntities and then retrieving associated (non-indexed) SomeOtherEntities from the DB to just searching SomeEntities which actually makes sense use case wise. This approach automatically solved the potential mismatch between the number of search results (SomeEntities) and the number of distinct SomeOtherEntities (e.g. pagination issues). I even removed the SomeOtherEntity-SomeEntity @OneToMany relationship.

Search results must still include SomeOtherEntities but not full-blown Hibernate-managed ones, only a couple of fields: the ID, which is included in the composite key and resolved by the bridge, and another field which I get via projection. Therefore, I optimised the bridge so it simply instantiates SomeOtherEntities via new (plain constructor) instead of via findById. This solved any potential performance issues.

Although I kept the composite key, thanks for your last post as well as all your feedback, @yoann. And yes, I replaced @Id/@Embeddable with @EmbeddedId. :slight_smile:


#7

Glad you found a solution. Thanks for letting us know :slight_smile:


#8

P.S.: While optimising associations, I ended up applying your @MapId suggestion which indeed is the recommended way for optimising @OneToOne and @ManyToOne relationships so that:

  1. Parent objects can be fetched lazily: less DB queries are now being executed
  2. Child objects can be fetched by parent IDs i.e. no need to load parents objects: the implementation of the CompositeIdBridge got simplified as the stringToObject method now instantiates CompositeId with SomeOtherEntity ID (no longer with SomeOtherEntity).