Relationships between entities

Every time an entity class is annotated with @Indexed, by default Hibernate Search will create a Lucene index just for that class. We can have as many entities, and as many separate indexes, as we wish. However, searching each index separately would be a very awkward and cumbersome approach.

Most Hibernate ORM data models already capture the various associations between entity classes. When we search an entity's Lucene index, shouldn't Hibernate Search follow those associations? In this section we will see how to make it do just that.

Associated entities

So far, the entity fields in our example application have been simple data types. The App class represents a table named APP, and its member variables map to columns in that table. Now let's add a complex type field, for a second database table that is associated through a foreign key.

Online app stores usually support a range of different hardware devices. So we will create a new entity called Device, representing devices for which an App entity is available.

@Entity
public class Device {

   @Id
   @GeneratedValue
   private Long id;

   @Column
   @Field
   private String manufacturer;

   @Column
   @Field
   private String name;

   @ManyToMany(mappedBy="supportedDevices",
      fetch=FetchType.EAGER,
      cascade = { CascadeType.ALL }
   )
   @ContainedIn
   private Set<App> supportedApps;

   public Device() {
   }

   public Device(String manufacturer, String name,
         Set<App>supportedApps) {
      this.manufacturer = manufacturer;
      this.name = name;
      this.supportedApps = supportedApps;
   }

   //
   // Getters and setters for all fields...
   //

}

Most details of this class should be familiar from Chapter 1, Your First Application. Device is annotated with @Entity, so Hibernate Search will create a Lucene index just for it. The entity class contains searchable fields for device name, and manufacturer name.

However, the supportedApps member variable introduces a new annotation, for making the association between these two entities bidirectional. An App entity will contain a list of all its supported devices, and a Device entity will contain a list of all its supported apps.

Tip

If for no other reason, using bidirectional associations improves the reliability of Hibernate Search.

A Lucene index contains denormalized data from associated entities, but those entities are still primarily tied to their own Lucene indexes. To cut a long story short, when the association between two entities is bidirectional, and changes are set to cascade, then you can count on both indexes being updated when either entity changes.

The Hibernate ORM reference manual describes several bidirectional mapping types and options. Here we are using @ManyToMany, to declare a many-to-many relationship between the App and Device entities. The cascade element is set to ensure that changes on this end of the association properly update the other side.

Note

Normally, Hibernate is "lazy". It doesn't actually fetch associated entities from the database until they are needed.

However, here we are writing a multi-tiered application, and the controller servlet has already closed the Hibernate session by the time our search results JSP receives these entities. If a view tries to fetch associations after the session has closed, errors will occur.

There are several ways around this problem. For simplicity, we are also adding a fetch element to the @ManyToMany annotation, changing the fetch type from "lazy" to "eager". Now when we retrieve a Device entity, Hibernate will immediately fetch all the associated App entities while the session is still open.

Eager fetching is very inefficient with large amounts of data, however, so in Chapter 5, Advanced Querying, we will explore a more advanced strategy for handling this.

Everything about supportedApps discussed so far has been in the realm of Hibernate ORM. So last but not least, we will add the Hibernate Search @ContainedIn annotation, declaring that App's Lucene index should contain data from Device. Hibernate ORM already saw these two entities as being associated. The Hibernate Search @ContainedIn annotation sets up a bidirectional association from Lucene's perspective too.

The other half of the bidirectional association involves giving the App entity class a list of supported Device entity classes.

...
@ManyToMany(fetch=FetchType.EAGER, cascade = { CascadeType.ALL })
@IndexedEmbedded(depth=1)
private Set<Device>supportedDevices;
...
// Getter and setter methods
...

This is very similar to the Device side of the association, except that the @IndexedEmbedded annotation here serves as the counterpoint to @ContainedIn.

Note

If your associated objects contain other associated objects themselves, then you might end up indexing a lot more data than you wanted. Even worse, you could run into problems with circular dependencies.

To safeguard against this, set the @IndexEmbedded annotation's optional depth element to a max limit. When indexing objects, Hibernate Search will go no further than the specified number of levels.

The previous code specifies a depth of one level. This means that an app will be indexed with information about its supported devices, but not information about a device's other supported apps.

Querying associated entities

Once associated entities have been mapped for Hibernate Search, it is easy to include them in search queries. The following code snippet updates SearchServlet to add supportedDevices to the list of fields searched:

...
QueryBuilderqueryBuilder =
fullTextSession.getSearchFactory().buildQueryBuilder()
      .forEntity(App.class ).get();
org.apache.lucene.search.QueryluceneQuery = queryBuilder
   .keyword()
   .onFields("name", "description", "supportedDevices.name")
   .matching(searchString)
   .createQuery();
org.hibernate.QueryhibernateQuery =
   fullTextSession.createFullTextQuery(luceneQuery, App.class);
...

Complex types are a bit different from the simple data types we have worked with so far. With complex types, we are not really interested in the field itself, because the field is actually just an object reference (or a collection of object references).

What we really want our searches to match are the simple data type fields within the complex type. In other words, we want to search the Device entity's name field. So, as long as an associated class field has been indexed (that is, with the @Field annotation), it can be queried with a [entity field].[nested field] format, such as supportedDevices.name in the previous code.

In the sample code for this chapter, StartupDataLoader has been expanded to save some Device entities in the database, and associate them with the App entities. One of these test devices is named xPhone. When we run the VAPORware Marketplace application and search for this keyword, the search results will include apps that are compatible with the xPhone, even if that keyword doesn't appear in the name or description of the app itself.

Embedded objects

Associated entities are full-blown entities in their own right. They typically correspond to a database table and Lucene index of their own, and may stand apart from their associations. For example, if we delete an app entity that is supported on the xPhone, that doesn't mean we want to delete the xPhone Device too.

There is a different type of association, in which the lifecycle of associated objects depends on the entity that contains them. If the VAPORware Marketplace apps had customer reviews, and an app was permanently deleted from the database, then we would probably expect all its customer reviews to be removed along with it.

Note

Classic Hibernate ORM terminology refers to such objects as components (or sometimes elements ). In the newer JPA jargon, they are known as embedded objects.

Embedded objects are not entities themselves. Hibernate Search does not create separate Lucene indexes for them, and they cannot be searched outside the context of the entity containing them. Otherwise, they look and feel quite similar to associated entities.

Let's give the example application an embedded object type for customer reviews. A CustomerReview instance will consist of the username of the person submitting the review, the rating they gave (for example, five stars), and any additional comments they wrote.

@Embeddable
public class CustomerReview {

   @Field
   private String username;

   private int stars;

   @Field
   private String comments;

   publicCustomerReview() {
   }

   public CustomerReview(String username,
         int stars, String comments) {
      this.username = username;
      this.stars = stars;
      this.comments = comments;
   }

   // Getter and setter methods...

}

This class is annotated with @Embeddable rather than the usual @Entity annotation, telling Hibernate ORM that the lifecycle of a CustomerReview instance is dependent on whichever entity object contains it.

The @Field annotation is applied to searchable fields as before. However, Hibernate Search will not create a standalone Lucene index just for CustomerReview. This annotation only adds information to the indexes of other entities that contain this embeddable class.

In our case, the containing class will be App. Let's give it a set of customer reviews as a member variable:

...
@ElementCollection(fetch=FetchType.EAGER)
@Fetch(FetchMode.SELECT)
@IndexedEmbedded(depth=1)
private Set<CustomerReview>customerReviews;
...

Rather than one of the usual JPA relationship annotations (for example, @OneToOne, @ManyToMany, and so on), this field is annotated as a JPA @ElementCollection. If this field were a single object, no annotation would be necessary. JPA would simply figure it out based on that object class having the @Embeddable annotation. However, the @ElementCollection annotation is necessary when dealing with collections of embeddable elements.

Tip

When using classic XML-based Hibernate mapping, the hbm.xml file equivalents are <component> for single instances, and <composite-element> for collections. See the chapter2-xml variant of the downloadable sample application source.

The @ElementCollection annotation has a fetch element set to use eager fetching, for the same reasons discussed earlier in this chapter.

On the next line we use the Hibernate-specific @Fetch annotation, to ensure that the CustomerReview instances are fetched through multiple SELECT statements rather than a single OUTER JOIN. This avoids duplication of customer reviews, due to Hibernate ORM quirks that are discussed further in comments within the downloadable source code. Unfortunately, this mode is inefficient when dealing with very large collections, so you may wish to consider another approach in such cases.

Querying embedded objects is the same as with associated entities. Here is the query code snippet from SearchServlet, modified to also search against the comments fields of the embedded CustomerReview instances:

...
QueryBuilderqueryBuilder =
fullTextSession.getSearchFactory().buildQueryBuilder()
   .forEntity(App.class ).get();
org.apache.lucene.search.QueryluceneQuery = queryBuilder
   .keyword()
   .onFields("name", "description", "supportedDevices.name",
      "customerReviews.comments")
   .matching(searchString)
   .createQuery();
org.hibernate.QueryhibernateQuery = fullTextSession.createFullTextQuery(
   luceneQuery, App.class);
...

Now we have a query that is really doing some searching! The chapter2 version of StartupDataLoader has been extended to load some customer reviews for all of the test apps. Searches will now produce results when a match is found in a customer review, even though the keyword doesn't otherwise appear in the App itself.

The HTML in the VAPORware Marketplace application has also been updated. Now each search result has a Full Details button, which pops-up a modal box with supported devices and customer reviews for that app. Notice that the search keyword in this screenshot is matched against a customer review rather the actual app description:

Embedded objects
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset