Chapter 8. Advanced entity association mappings

In this chapter

  • Mapping one-to-one entity associations
  • One-to-many mapping options
  • Many-to-many and ternary entity relationships

In the previous chapter, we demonstrated a unidirectional many-to-one association, made it bidirectional, and finally enabled transitive state changes with cascading options. One reason we discuss more advanced entity mappings in a separate chapter is that we consider quite a few of them rare, or at least optional. It’s possible to only use component mappings and many-to-one (occasionally one-to-one) entity associations. You can write a sophisticated application without ever mapping a collection! We’ve shown the particular benefits you gain from collection mappings in the previous chapter; the rules for when a collection mapping is appropriate also apply to all examples in this chapter. Always make sure you actually need a collection before you attempt a complex collection mapping.

Let’s start with mappings that don’t involve collections: one-to-one entity -associations.

Major new features in JPA 2

  • Many-to-one and one-to-one associations may now be mapped with an intermediate join/link table.
  • Embeddable component classes may have unidirectional associations to entities, even many-valued with collections.

8.1. One-to-one associations

We argued in section 5.2 that the relationships between User and Address (the user has a billingAddress, homeAddress, and shippingAddress) are best represented with an @Embeddable component mapping. This is usually the simplest way to represent one-to-one relationships, because the life cycle is typically dependent in such a case. It’s either an aggregation or a composition in UML.

What about using a dedicated ADDRESS table and mapping both User and Address as entities? One benefit of this model is the possibility for shared references—another entity class (let’s say Shipment) can also have a reference to a particular Address instance. If a User also has a reference to this instance, as their shippingAddress, the Address instance has to support shared references and needs its own identity.

In this case, User and Address classes have a true one-to-one association. Look at the revised class diagram in figure 8.1.

Figure 8.1. Address as an entity with two associations, supporting shared references

There are several possible mappings for one-to-one associations. The first strategy we consider is a shared primary key value.

8.1.1. Sharing a primary key

Rows in two tables related by a primary key association share the same primary key values. The User has the same primary key value as their (shipping-) Address. The main difficulty with this approach is ensuring that associated instances are assigned the same primary key value when the instances are saved. Before we look at this issue, let’s create the basic mapping. The Address class is now a standalone entity; it’s no longer a component.

Listing 8.1. Address class as a standalone entity

Path: /model/src/main/java/org/jpwh/model/associations/onetoone/sharedprimarykey/Address.java

@Entity
public class Address {
<enter/>
    @Id
    @GeneratedValue(generator = Constants.ID_GENERATOR)
    protected Long id;
<enter/>
    @NotNull
    protected String street;
<enter/>
    @NotNull
    protected String zipcode;
<enter/>
    @NotNull
    protected String city;
<enter/>
    // ...
}

The User class is also an entity, with the shippingAddress association property.

Listing 8.2. User entity and shippingAddress association

Path: /model/src/main/java/org/jpwh/model/associations/onetoone/sharedprimarykey/User.java

For the User, you don’t declare an identifier generator . As mentioned in section 4.2.4, this is one of the rare cases when you use an application-assigned identifier value. You can see that the constructor design (weakly) enforces this : the public API of the class requires an identifier value to create an instance.

Two new annotations are present in the example. @OneToOne does what you’d expect: it’s required to mark an entity-valued property as a one-to-one association. As usual, you should prefer the lazy-loading strategy, so you override the default FetchType.EAGER with LAZY . The second new annotation is @PrimaryKeyJoinColumn , selecting the shared primary key strategy you’d like to map. This is now a unidirectional shared primary key one-to-one association mapping, from User to Address.

The optional=false switch defines that a User must have a shippingAddress. The Hibernate-generated database schema reflects this with a foreign key constraint. The primary key of the USERS table also has a foreign key constraint referencing the primary key of the ADDRESS table. See the tables in figure 8.2.

Figure 8.2. The USERS table has a foreign key constraint on its primary key.

The JPA specification doesn’t include a standardized method to deal with the problem of shared primary key generation. This means you’re responsible for setting the identifier value of a User instance correctly before you save it, to the identifier value of the linked Address instance:

Path: /examples/src/test/java/org/jpwh/test/associations/OneToOneSharedPrimaryKey.java

After persisting the Address, you take its generated identifier value and set it on the User before saving it, too. The last line of this example is optional: your code now expects a value when calling someUser.getShippingAddress(), so you should set it. Hibernate won’t give you an error if you forget this last step.

There are three problems with the mapping and code:

  • You have to remember that the Address must be saved first and then get its identifier value after the call to persist(). This is only possible if the Address entity has an identifier generator that produces values on persist() before the INSERT, as we discussed in section 4.2.5. Otherwise, someAddress.getId() returns null, and you can’t manually set the identifier value of the User.
  • Lazy loading with proxies only works if the association is non-optional. This is often a surprise for developers new to JPA. The default for @OneToOne is FetchType.EAGER: when Hibernate loads a User, it loads the shippingAddress right away. Conceptually, lazy loading with proxies only makes sense if Hibernate knows that there is a linked shippingAddress. If the property were nullable, Hibernate would have to check in the database whether the property value is NULL, by querying the ADDRESS table. If you have to check the database, you might as well load the value right away, because there would be no benefit in using a proxy.
  • The one-to-one association is unidirectional; sometimes you need bidirectional navigation.

The first issue has no other solution, and it’s one of the reasons you should always prefer identifier generators capable of producing values before any SQL INSERT.

An @OneToOne(optional=true) association doesn’t support lazy loading with proxies. This is consistent with the JPA specification. FetchType.LAZY is a hint for the persistence provider, not a requirement. You could get lazy loading of nullable @OneToOne with bytecode instrumentation, as we’ll show in section 12.1.3.

As for the last problem, if you make the association bidirectional, you can also use a special Hibernate-only identifier generator to help with assigning key values.

8.1.2. The foreign primary key generator

A bidirectional mapping always requires a mappedBy side. Here, pick the User side (this is a matter of taste and perhaps other, secondary requirements):

Path: /model/src/main/java/org/jpwh/model/associations/onetoone/foreigngenerator/User.java

@Entity
@Table(name = "USERS")
public class User {
<enter/>
    @Id
    @GeneratedValue(generator = Constants.ID_GENERATOR)
    protected Long id;
<enter/>
    @OneToOne(
        mappedBy = "user",
        cascade = CascadeType.PERSIST
    )
    protected Address shippingAddress;
    // ...
}

Compare this with the previous mapping: you add the mappedBy option, telling Hibernate that the lower-level details are now mapped by the “property on the other side,” named user. As a convenience, you enable CascadeType.PERSIST; transitive persistence will make it easier to save the instances in the right order. When you make the User persistent, Hibernate makes the shippingAddress persistent and generates the identifier for the primary key automatically.

Next, let’s look at the “other side”: the Address.

Listing 8.3. Address has the special foreign key generator

Path: /model/src/main/java/org/jpwh/model/associations/onetoone/foreigngenerator/Address.java

That’s quite a bit of new code. Let’s start with the identifier property and then the one-to-one association.

Hibernate Feature

The @GenericGenerator on the identifier property defines a special-purpose primary key value generator with the Hibernate-only foreign strategy. We didn’t mention this generator in the overview in section 4.2.5; the shared primary key one-to-one association is its only use case. When you persist an instance of Address, this special generator grabs the value of the user property and takes the identifier value of the referenced entity instance, the User.

Let’s continue with the @OneToOne mapping . The user property is marked as a shared primary key entity association with the @PrimaryKeyJoinColumn annotation . It’s set to optional=false, so an Address must have a reference to a User. The public constructors of Address now require a User instance. The foreign key constraint reflecting optional=false is now on the primary key column of the ADDRESS table, as you can see in the schema in figure 8.3.

Figure 8.3. The ADDRESS table has a foreign key constraint on its primary key.

You no longer have to call someAddress.getId() or someUser.getId()in your unit of work. Storing data is simplified:

Path: /examples/src/test/java/org/jpwh/test/associations/OneToOneForeignGenerator.java

Don’t forget that you must link both sides of a bidirectional entity association. Note that with this mapping, you won’t get lazy loading of User#shippingAddress (it’s optional/nullable), but you can load Address#user on demand with proxies (it’s non-optional).

Shared primary key one-to-one associations are relatively rare. Instead, you’ll often map a “to-one” association with a foreign key column and a unique constraint.

8.1.3. Using a foreign key join column

Instead of sharing a primary key, two rows can have a relationship based on a simple additional foreign key column. One table has a foreign key column that references the primary key of the associated table. (The source and target of this foreign key constraint can even be the same table: we call this a self-referencing relationship.)

Let’s change the mapping for User#shippingAddress. Instead of the shared primary key, you now add a SHIPPINGADDRESS_ID column in the USERS table. Additionally, the column has a UNIQUE constraint, so no two users can reference the same shipping address. Look at the schema in figure 8.4.

Figure 8.4. A one-to-one join column association between the USERS and ADDRESS tables

The Address is a regular entity class, like the first one we showed in this chapter in listing 8.1. The User entity class has the shippingAddress property, implementing this unidirectional association:

Path: /model/src/main/java/org/jpwh/model/associations/onetoone/foreignkey/User.java

You don’t need any special identifier generators or primary key assignment; instead of @PrimaryKeyJoinColumn, you apply the regular @JoinColumn. If you’re more familiar with SQL than JPA, it helps to think “foreign key column” every time you see @JoinColumn in a mapping.

You should enable lazy loading for this association. Unlike for shared primary keys, you don’t have a problem with lazy loading here: When a row of the USERS table has been loaded, it contains the value of the SHIPPINGADDRESS_ID column. Hibernate therefore knows whether an ADDRESS row is present, and a proxy can be used to load the Address instance on demand.

In the mapping, though, you set optional=false, so the user must have a shipping address. This won’t affect loading behavior but is a logical consequence of the unique=true setting on the @JoinColumn. This setting adds the unique constraint to the generated SQL schema. If the values of the SHIPPINGADDRESS_ID column must be unique for all users, only one user could possibly have “no shipping address.” Hence, nullable unique columns typically aren’t meaningful.

Creating, linking, and storing instances is straightforward:

Path: /examples/src/test/java/org/jpwh/test/associations/OneToOneForeignKey.java

You’ve now completed two basic one-to-one association mappings: the first with a shared primary key, the second with a foreign key reference and a unique column constraint. The last option we want to discuss is a bit more exotic: mapping a one-to-one association with the help of an additional table.

8.1.4. Using a join table

You’ve probably noticed that nullable columns can be problematic. Sometimes a better solution for optional values is an intermediate table, which contains a row if a link is present, or doesn’t if not.

Let’s consider the Shipment entity in CaveatEmptor and discuss its purpose. Sellers and buyers interact in CaveatEmptor by starting and bidding on auctions. Shipping goods seems outside the scope of the application; the seller and the buyer agree on a method of shipment and payment after the auction ends. They can do this offline, outside of CaveatEmptor.

On the other hand, you could offer an escrow service in CaveatEmptor. Sellers would use this service to create a trackable shipment once the auction ends. The buyer would pay the price of the auction item to a trustee (you), and you’d inform the seller that the money was available. Once the shipment arrived and the buyer accepted it, you’d transfer the money to the seller.

If you’ve ever participated in an online auction of significant value, you’ve probably used such an escrow service. But you want more in CaveatEmptor: not only will you provide trust services for completed auctions, but you also allow users to create a trackable and trusted shipment for any deal they make outside an auction, outside CaveatEmptor.

This scenario calls for a Shipment entity with an optional one-to-one association to Item. Look at the class diagram for this domain model in figure 8.5.

Figure 8.5. A Shipment has an optional link with an auction Item.

Note

We briefly considered abandoning the CaveatEmptor example for this section, because we couldn’t find a natural scenario that requires optional one-to-one associations. If this escrow example seems contrived, consider the equivalent problem of assigning employees to workstations. This is also an optional one-to-one relationship.

In the database schema, you add an intermediate link table called ITEM_SHIPMENT. A row in this table represents a Shipment made in the context of an auction. Figure 8.6 shows the tables.

Figure 8.6. The intermediate table links items and shipments.

Note how the schema enforces uniqueness and the one-to-one relationship: the primary key of ITEM_SHIPMENT is the SHIPMENT_ID column, and the ITEM_ID column is unique. An item can therefore be in only one shipment. Of course, that also means a shipment can contain only one item.

You map this model with an @OneToOne annotation in the Shipment entity class:

Path: /model/src/main/java/org/jpwh/model/associations/onetoone/jointable/Shipment.java

Lazy loading has been enabled, with a twist: when Hibernate loads a Shipment, it queries both the SHIPMENT and the ITEM_SHIPMENT join table. Hibernate has to know if there is a link to an Item present before it can use a proxy. It does that in one outer join SQL query, so you won’t see any extra SQL statements. If there is a row in ITEM_SHIPMENT, Hibernate uses an Item placeholder.

The @JoinTable annotation is new; you always have to specify the name of the intermediate table. This mapping effectively hides the join table; there is no corresponding Java class. The annotation defines the column names of the ITEM_SHIPMENT table, and Hibernate generates in the schema the UNIQUE constraint on the ITEM_ID column. Hibernate also generates the appropriate foreign key constraints on the columns of the join table.

Here you store a Shipment without Items and another linked to a single Item:

Path: /examples/src/test/java/org/jpwh/test/associations/OneToOneJoinTable.java

Shipment someShipment = new Shipment();
em.persist(someShipment);
<enter/>
Item someItem = new Item("Some Item");
em.persist(someItem);
<enter/>
Shipment auctionShipment = new Shipment(someItem);
em.persist(auctionShipment);

This completes our discussion of one-to-one association mappings. To summarize, use a shared primary key association if one of the two entities is always stored before the other and can act as the primary key source. Use a foreign key association in all other cases, and a hidden intermediate join table when your one-to-one association is optional.

We now focus on plural, or many-valued entity associations, beginning by exploring some advanced options for one-to-many.

8.2. One-to-many associations

A plural entity association is by definition a collection of entity references. You mapped one of these, a one-to-many association, in the previous chapter, section 7.3.2. One-to-many associations are the most important kind of entity association that involves a collection. We go so far as to discourage the use of more complex association styles when a simple bidirectional many-to-one/one-to-many will do the job.

Also, remember that you don’t have to map any collection of entities if you don’t want to; you can always write an explicit query instead of direct access through iteration. If you decide to map collections of entity references, you have a few options, and we discussed some more complex situations now.

8.2.1. Considering one-to-many bags

So far, you have only seen a @OneToMany on a Set, but it’s possible to use a bag mapping instead for a bidirectional one-to-many association. Why would you do this?

Bags have the most efficient performance characteristics of all the collections you can use for a bidirectional one-to-many entity association. By default, collections in Hibernate are loaded when they’re accessed for the first time in the application. Because a bag doesn’t have to maintain the index of its elements (like a list) or check for duplicate elements (like a set), you can add new elements to the bag without triggering the loading. This is an important feature if you’re going to map a possibly large collection of entity references.

On the other hand, you can’t eager-fetch two collections of bag type simultaneously: for example, if bids and images of an Item were one-to-many bags. This is no big loss, because fetching two collections simultaneously always results in a Cartesian product; you want to avoid this kind of operation whether the collections are bags, sets, or lists. We’ll come back to fetching strategies in chapter 12. In general, we’d say that a bag is the best inverse collection for a one-to-many association, if mapped as a @OneToMany(mappedBy = "...").

To map a bidirectional one-to-many association as a bag, you have to replace the type of the bids collection in the Item entity with a Collection and an ArrayList implementation. The mapping for the association between Item and Bid remains essentially unchanged:

Path: /model/src/main/java/org/jpwh/model/associations/onetomany/bag/Item.java

@Entity
public class Item {
<enter/>
    @OneToMany(mappedBy = "item")
    public Collection<Bid> bids = new ArrayList<>();
<enter/>
    // ...
}

The Bid side with its @ManyToOne (which is the “mapped by” side), and even the tables, are the same as in section 7.3.1.

A bag also allows duplicate elements, which the set you mapped earlier didn’t:

Path: /examples/src/test/java/org/jpwh/test/associations/OneToManyBag.java

It turns out this isn’t relevant in this case, because duplicate means you’ve added a particular reference to the same Bid instance several times. You wouldn’t do this in your application code. Even if you add the same reference several times to this collection, though, Hibernate ignores it. The side relevant for updates of the database is the @ManyToOne, and the relationship is already “mapped by” that side. When you load the Item, the collection doesn’t contain the duplicate:

Path: /examples/src/test/java/org/jpwh/test/associations/OneToManyBag.java

Item item = em.find(Item.class, ITEM_ID);
assertEquals(item.getBids().size(), 1);

As mentioned, the advantage of bags is that the collection doesn’t have to be initialized when you add a new element:

Path: /examples/src/test/java/org/jpwh/test/associations/OneToManyBag.java

This code example triggers one SQL SELECT to load the Item. If you use em.get-Reference() instead of em.find(), Hibernate still initializes and returns an Item proxy with a SELECT as soon as you call item.getBids(). But as long as you don’t iterate the Collection, no more queries are necessary, and an INSERT for the new Bid will be made without loading all the bids. If the collection is a Set or a List, Hibernate loads all the elements when you add another element.

Let’s change the collection to a persistent List.

8.2.2. Unidirectional and bidirectional list mappings

If you need a real list to hold the position of the elements in a collection, you have to store that position in an additional column. For the one-to-many mapping, this also means you should change the Item#bids property to List and initialize the variable with an ArrayList:

Path: /model/src/main/java/org/jpwh/model/associations/onetomany/list/Item.java

This is a unidirectional mapping: there is no other “mapped by” side. The Bid doesn’t have a @ManyToOne property. The new annotation @OrderColumn is required for persistent list indexes, where, as usual, you should make the column NOT NULL. The database view of the BID table, with the join and order columns, is shown in figure 8.7.

Figure 8.7. The BID table

The stored index of each collection starts at zero and is contiguous (there are no gaps). Hibernate will execute potentially many SQL statements when you add, remove, and shift elements of the List. We talked about this performance issue in section 7.1.6.

Let’s make this mapping bidirectional, with a @ManyToOne property on the Bid entity:

Path: /model/src/main/java/org/jpwh/model/associations/onetomany/list/Bid.java

You probably expected different code—maybe @ManyToOne(mappedBy="bids") and no additional @JoinColumn annotation. But @ManyToOne doesn’t have a mappedBy attribute: it’s always the “owning” side of the relationship. You’d have to make the other side, @OneToMany, the mappedBy side. Here you run into a conceptual problem and some Hibernate quirks.

The Item#bids collection is no longer read-only, because Hibernate now has to store the index of each element. If the Bid#item side was the owner of the relationship, Hibernate would ignore the collection when storing data and not write the element indexes. You have to map the @JoinColumn twice and then disable writing on the @ManyToOne side with updatable=false and insertable=false. Hibernate now considers the collection side when storing data, including the index of each element. The @ManyToOne is effectively read-only, as it would be if it had a mappedBy attribute.

Bidirectional list with mappedBy

Several existing bug reports related to this issue are open on Hibernate; a future version might allow the JPA-compliant usage of the @OneToMany(mappedBy) and @OrderColumn on a collection. At the time of writing, the shown mapping is the only working variation for a bidirectional one-to-many with a persistent List.

Finally, the Hibernate schema generator always relies on the @JoinColumn of the @ManyToOne side. Hence, if you want the correct schema produced, you should add the @NotNull on this side or declare @JoinColumn(nullable=false). The generator ignores the @OneToMany side and its join column if there is a @ManyToOne.

In a real application, you wouldn’t map the association with a List. Preserving the order of elements in the database seems like a common use case but on second thought isn’t very useful: sometimes you want to show a list with the highest or newest bid first, or only bids made by a certain user, or bids made within a certain time range. None of these operations requires a persistent list index. As mentioned in section 3.2.4, avoid storing a display order in the database; keep it flexible with queries instead of hardcoded mappings. Furthermore, maintaining the index when the application removes, adds, or shifts elements in the list can be expensive and may trigger many SQL statements. Map the foreign key join column with @ManyToOne, and drop the collection.

Next is one more scenario with a one-to-many relationship: an association mapped to an intermediate join table.

8.2.3. Optional one-to-many with a join table

A useful addition to the Item class is a buyer property. You can then call someItem.getBuyer() to access the User who made the winning bid. If made bidirectional, this association will also help to render a screen that shows all auctions a particular user has won: you call someUser.getBoughtItems() instead of writing a query.

From the point of view of the User class, the association is one-to-many. Figure 8.8 shows the classes and their relationship.

Figure 8.8. The User-Item “bought” relationship

Why is this association different from the one between Item and Bid? The multiplicity 0..* in UML indicates that the reference is optional. This doesn’t influence the Java domain model much, but it has consequences for the underlying tables. You expect a BUYER_ID foreign key column in the ITEM table. The column has to be nullable, because a user may not have bought a particular Item (as long as the auction is still running).

You could accept that the foreign key column can be NULL and apply additional constraints: “Allowed to be NULL only if the auction end time hasn’t been reached or if no bid has been made.” We always try to avoid nullable columns in a relational database schema. Unknown information degrades the quality of the data you store. Tuples represent propositions that are true; you can’t assert something you don’t know. Moreover, in practice, many developers and DBAs don’t create the right constraint and rely on often buggy application code to provide data integrity.

An optional entity association, be it one-to-one or one-to-many, is best represented in an SQL database with a join table. Figure 8.9 shows an example schema.

Figure 8.9. An intermediate table links users and items.

You added a join table earlier in this chapter, for a one-to-one association. To guarantee the multiplicity of one-to-one, you applied a unique constraint on a foreign key column of the join table. In the current case, you have a one-to-many multiplicity, so only the ITEM_ID primary key column has to be unique: only one User can buy any given Item, once. The BUYER_ID column isn’t unique because a User can buy many Items.

The mapping of the User#boughtItems collection is simple:

Path: /model/src/main/java/org/jpwh/model/associations/onetomany/jointable/User.java

@Entity
@Table(name = "USERS")
public class User {
<enter/>
    @OneToMany(mappedBy = "buyer")
    protected Set<Item> boughtItems = new HashSet<Item>();
<enter/>
    // ...
}

This is the usual read-only side of a bidirectional association, with the actual mapping to the schema on the “mapped by” side, the Item#buyer:

Path: /model/src/main/java/org/jpwh/model/associations/onetomany/jointable/Item.java

This is now a clean, optional one-to-many/many-to-one relationship. If an Item hasn’t been bought, there is no corresponding row in the join table ITEM_BUYER. You don’t have any problematic nullable columns in your schema. Still, you should write a procedural constraint and a trigger that runs on INSERT, for the ITEM_BUYER table: “Only allow insertion of a buyer if the auction end time for the given item has been reached and the user made the winning bid.”

The next example is our last with one-to-many associations. So far, you’ve seen one-to-many associations from an entity to another entity. An embeddable component class may also have a one-to-many association to an entity.

8.2.4. One-to-many association in an embeddable class

Consider again the embeddable component mapping you’ve been repeating for a few chapters: the Address of a User. You now extend this example by adding a one-to-many association from Address to Shipment: a collection called deliveries. Figure 8.10 shows the UML class diagram for this model.

Figure 8.10. The one-to-may relationship from Address to Shipment

The Address is an @Embeddable class, not an entity. It can own a unidirectional association to an entity; here it’s one-to-many multiplicity to Shipment. (You see an embeddable class having a many-to-one association with an entity in the next section.)

The Address class has a Set<Shipment> representing this association:

Path: /model/src/main/java/org/jpwh/model/associations/onetomany/embeddable/Address.java

The first mapping strategy for this association is with an @JoinColumn named DELIVERY_ADDRESS_USER_ID. This foreign key-constrained column is in the SHIPMENT table, as you can see in figure 8.11.

Figure 8.11. A primary key in the USERS table links the USERS and SHIPMENT tables

Embeddable components don’t have their own identifier, so the value in the foreign key column is the value of a User’s identifier, which embeds the Address. Here you also declare the join column nullable = false, so a Shipment must have an associated delivery address. Of course, bidirectional navigation isn’t possible: the Shipment can’t have a reference to the Address, because embedded components can’t have shared references.

If the association is optional and you don’t want a nullable column, you can map the association to an intermediate join/link table, as shown in figure 8.12. The mapping of the collection in Address now uses an @JoinTable instead of an @JoinColumn:

Path: /model/src/main/java/org/jpwh/model/associations/onetomany/embeddablejointable/Address.java

Figure 8.12. Using an intermediate table between USERS and SHIPMENT to represent an optional association

Note that if you declare neither @JoinTable nor @JoinColumn, the @OneToMany in an embeddable class defaults to a join table strategy.

From within the owning entity class, you can override property mappings of an embedded class with @AttributeOverride, as shown in section 5.2.3. If you want to override the join table or column mapping of an entity association in an embeddable class, use @AssociationOverride in the owning entity class instead. You can’t, however, switch the mapping strategy; the mapping in the embeddable component class decides whether a join table or join column is used.

A join table mapping is of course also applicable in true many-to-many mappings.

8.3. Many-to-many and ternary associations

The association between Category and Item is a many-to-many association, as you can see in figure 8.13. In a real system, you may not have a many-to-many association. Our experience is that there is almost always other information that must be attached to each link between associated instances. Some examples are the timestamp when an Item was added to a Category and the User responsible for creating the link. We expand the example later in this section to cover such a case. You should start with a regular and simpler many-to-many association.

Figure 8.13. A many-to-many association between Category and Item

8.3.1. Unidirectional and bidirectional many-to-many associations

A join table in the database represents a regular many-to-many association, which some developers also call the link table, or association table. Figure 8.14 shows a many-to-many relationship with a link table.

Figure 8.14. A many-to-many relationship with a link table

The link table CATEGORY_ITEM has two columns, both with a foreign key constraint referencing the CATEGORY and ITEM tables, respectively. Its primary key is a composite key of both columns. You can only link a particular Category and Item once, but you can link the same item to several categories.

In JPA, you map many-to-many associations with @ManyToMany on a collection:

Path: /model/src/main/java/org/jpwh/model/associations/manytomany/bidirectional/Category.java

@Entity
public class Category {
<enter/>
    @ManyToMany(cascade = CascadeType.PERSIST)
    @JoinTable(
        name = "CATEGORY_ITEM",
        joinColumns = @JoinColumn(name = "CATEGORY_ID"),
        inverseJoinColumns = @JoinColumn(name = "ITEM_ID")
    )
    protected Set<Item> items = new HashSet<Item>();
<enter/>
    // ...
}

As usual, you can enable CascadeType.PERSIST to make it easier to save data. When you reference a new Item from the collection, Hibernate makes it persistent. Let’s make this association bidirectional (you don’t have to, if you don’t need it):

Path: /model/src/main/java/org/jpwh/model/associations/manytomany/bidirectional/Item.java

@Entity
public class Item {
<enter/>
    @ManyToMany(mappedBy = "items")
    protected Set<Category> categories = new HashSet<Category>();
<enter/>
    // ...
}

As for any bidirectional mapping, one side is “mapped by” the other side. The Item#categories collection is effectively read-only; Hibernate will analyze the content of the Category#items side when storing data. Next you create two categories and two items and link them with many-to-many multiplicity:

Path: /examples/src/test/java/org/jpwh/test/associations/ManyToManyBidirectional.java

Category someCategory = new Category("Some Category");
Category otherCategory = new Category("Other Category");
<enter/>
Item someItem = new Item("Some Item");
Item otherItem = new Item("Other Item");
<enter/>
someCategory.getItems().add(someItem);
someItem.getCategories().add(someCategory);
<enter/>
someCategory.getItems().add(otherItem);
otherItem.getCategories().add(someCategory);
<enter/>
otherCategory.getItems().add(someItem);
someItem.getCategories().add(otherCategory);
<enter/>
em.persist(someCategory);
em.persist(otherCategory);

Because you enabled transitive persistence, saving the categories makes the entire network of instances persistent. On the other hand, the cascading options ALL, REMOVE, and orphan deletion (see section 7.3.3) aren’t meaningful for many-to-many associations. This is a good point to test whether you understand entities and value types. Try to come up with reasonable answers as to why these cascading types don’t make sense for a many-to-many association.

Can you use a List instead of a Set, or even a bag? The Set matches the database schema perfectly, because there can be no duplicate links between Category and Item.

A bag implies duplicate elements, so you need a different primary key for the join table. The proprietary @CollectionId annotation of Hibernate can provide this, as shown in section 7.1.5. One of the alternative many-to-many strategies we discuss in a moment is a better choice if you need to support duplicate links.

You can map indexed collections such as a List with the regular @ManyToMany, but only on one side. Remember that in a bidirectional relationship one side has to be “mapped by” the other side, meaning its value is ignored when Hibernate synchronizes with the database. If both sides are lists, you can only make persistent the index of one side.

A regular @ManyToMany mapping hides the link table; there is no corresponding Java class, only some collection properties. So whenever someone says, “My link table has more columns with information about the link”—and, in our experience, someone always says this sooner rather than later—you need to map this information to a Java class.

8.3.2. Many-to-many with an intermediate entity

You may always represent a many-to-many association as two many-to-one associations to an intervening class. You don’t hide the link table but represent it with a Java class. This model is usually more easily extensible, so we tend not to use regular many-to-many associations in applications. It’s a lot of work to change code later, when inevitably more columns are added to a link table; so before you map an @ManyToMany as shown in the previous section, consider the alternative shown in figure 8.15.

Figure 8.15. CategorizedItem is the link between Category and Item.

Imagine that you need to record some information each time you add an Item to a Category. The CategorizedItem captures the timestamp and user who created the link. This domain model requires additional columns on the join table, as you can see in figure 8.16.

Figure 8.16. Additional columns on the join table in a many-to-many relationship

The new CategorizedItem entity maps to the link table, as shown next.

Listing 8.4. Mapping a many-to-many relationship with CategorizedItem

Path: /model/src/main/java/org/jpwh/model/associations/manytomany/linkentity/CategorizedItem.java

This is a large chunk of code with some new annotations. First, it’s an immutable entity class, so you’ll never update properties after creation. Hibernate can do some optimizations, such avoiding dirty checking during flushing of the persistence context, if you declare the class immutable .

An entity class needs an identifier property. The primary key of the link table is the composite of CATEGORY_ID and ITEM_ID. Hence, the entity class also has a composite key, which you encapsulate in a static nested embeddable component class for convenience. You can externalize this class into its own file, of course. The new @EmbeddedId annotation maps the identifier property and its composite key columns to the entity’s table.

Next are two basic properties mapping the addedBy username and the addedOn timestamp to columns of the join table. This is the “additional information about the link” that interests you.

Then two @ManyToOne properties, category and item , map columns that are already mapped in the identifier. The trick here is to make them read-only, with the updatable=false, insertable=false setting. This means Hibernate writes the values of these columns by taking the identifier value of CategorizedItem. At the same time, you can read and browse the associated instances through categorizedItem.getItem() and getCategory(), respectively. (If you map the same column twice without making one mapping read-only, Hibernate will complain on startup about a duplicate column mapping.)

You can also see that constructing a CategorizedItem involves setting the values of the identifier—the application always assigns composite key values; Hibernate doesn’t generate them. Pay extra attention to the constructor and how it sets the field values and guarantees referential integrity by managing collections on both sides of the association. You map these collections next, to enable bidirectional navigation.

This is a unidirectional mapping and enough to support the many-to-many -relationship between Category and Item. To create a link, you instantiate and persist a CategorizedItem. If you want to break a link, you remove the CategorizedItem. The constructor of CategorizedItem requires that you provide already persistent -Category and Item instances.

If bidirectional navigation is required, map an @OneToMany collection in Category and/or Item:

Path: /model/src/main/java/org/jpwh/model/associations/manytomany/linkentity/Category.java

@Entity
public class Category {
<enter/>
    @OneToMany(mappedBy = "category")
    protected Set<CategorizedItem> categorizedItems = new HashSet<>();
<enter/>
    // ...
}
<enter/>
<enter/>

Path: /model/src/main/java/org/jpwh/model/associations/manytomany/linkentity/Item.java

@Entity
public class Item {
<enter/>
    @OneToMany(mappedBy = "item")
    protected Set<CategorizedItem> categorizedItems = new HashSet<>();
<enter/>
    // ...
}

Both sides are “mapped by” the annotations in CategorizedItem, so Hibernate already knows what to do when you iterate through the collection returned by either getCategorizedItems() method.

This is how you create and store links:

Path: /examples/src/test/java/org/jpwh/test/associations/ManyToManyLinkEntity.java

Category someCategory = new Category("Some Category");
Category otherCategory = new Category("Other Category");
em.persist(someCategory);
em.persist(otherCategory);
<enter/>
Item someItem = new Item("Some Item");
Item otherItem = new Item("Other Item");
em.persist(someItem);
em.persist(otherItem);
<enter/>
CategorizedItem linkOne = new CategorizedItem(
    "johndoe", someCategory, someItem
);
<enter/>
CategorizedItem linkTwo = new CategorizedItem(
    "johndoe", someCategory, otherItem
);
<enter/>
CategorizedItem linkThree = new CategorizedItem(
    "johndoe", otherCategory, someItem
);
<enter/>
em.persist(linkOne);
em.persist(linkTwo);
em.persist(linkThree);

The primary advantage of this strategy is the possibility for bidirectional navigation: you can get all items in a category by calling someCategory.getCategorizedItems() and then also navigate from the opposite direction with someItem.get-Categorized-Items(). A disadvantage is the more complex code needed to manage the Categorized-Item entity instances to create and remove links, which you have to save and delete independently. You also need some infrastructure in the CategorizedItem class, such as the composite identifier. One small improvement would be to enable CascadeType.PERSIST on some of the associations, reducing the number of calls to persist().

In the previous example, you stored the user who created the link between -Category and Item as a simple name string. If the join table instead had a foreign key column called USER_ID, you’d have a ternary relationship. The CategorizedItem would have a @ManyToOne for Category, Item, and User.

In the following section, you see another many-to-many strategy. To make it a bit more interesting, we make it a ternary association.

8.3.3. Ternary associations with components

In the previous section, you represented a many-to-many relationship with an entity class mapped to the link table. A potentially simpler alternative is a mapping to an embeddable component class:

Path: /model/src/main/java/org/jpwh/model/associations/manytomany/ternary/CategorizedItem.java

The new mappings here are @ManyToOne associations in an @Embeddable, and the additional foreign key join column USER_ID, making this a ternary relationship. Look at the database schema in figure 8.17.

Figure 8.17. A link table with three foreign key columns

The owner of the embeddable component collection is the Category entity:

Path: /model/src/main/java/org/jpwh/model/associations/manytomany/ternary/Category.java

@Entity
public class Category {
<enter/>
    @ElementCollection
    @CollectionTable(
        name = "CATEGORY_ITEM",
        joinColumns = @JoinColumn(name = "CATEGORY_ID")
    )
    protected Set<CategorizedItem> categorizedItems = new HashSet<>();
<enter/>
    // ...
}

Unfortunately, this mapping isn’t perfect: when you map an @ElementCollection of embeddable type, all properties of the target type that are nullable=false become part of the (composite) primary key. You want all columns in CATEGORY_ITEM to be NOT NULL. Only CATEGORY_ID and ITEM_ID columns should be part of the primary key, though. The trick is to use the Bean Validation @NotNull annotation on properties that shouldn’t be part of the primary key. In that case (because it’s an embeddable class), Hibernate ignores the Bean Validation annotation for primary key realization and SQL schema generation. The downside is that the generated schema won’t have the appropriate NOT NULL constraints on the USER_ID and ADDEDON columns, which you should fix manually.

The advantage of this strategy is the implicit life cycle of the link components. To create an association between a Category and an Item, add a new CategorizedItem instance to the collection. To break the link, remove the element from the collection. No extra cascading settings are required, and the Java code is simplified (albeit spread over more lines):

Path: /examples/src/test/java/org/jpwh/test/associations/ManyToManyTernary.java

Category someCategory = new Category("Some Category");
Category otherCategory = new Category("Other Category");
em.persist(someCategory);
em.persist(otherCategory);
<enter/>
Item someItem = new Item("Some Item");
Item otherItem = new Item("Other Item");
em.persist(someItem);
em.persist(otherItem);
<enter/>
User someUser = new User("johndoe");
em.persist(someUser);
<enter/>
CategorizedItem linkOne = new CategorizedItem(
    someUser, someItem
);
someCategory.getCategorizedItems().add(linkOne);
<enter/>
CategorizedItem linkTwo = new CategorizedItem(
    someUser, otherItem
);
someCategory.getCategorizedItems().add(linkTwo);
<enter/>
CategorizedItem linkThree = new CategorizedItem(
    someUser, someItem
);
otherCategory.getCategorizedItems().add(linkThree);

There is no way to enable bidirectional navigation: an embeddable component, such as CategorizedItem by definition, can’t have shared references. You can’t navigate from Item to CategorizedItem, and there is no mapping of this link in Item. Instead, you can write a query to retrieve the categories given an Item:

Path: /examples/src/test/java/org/jpwh/test/associations/ManyToManyTernary.java

Item item = em.find(Item.class, ITEM_ID);
<enter/>
List<Category> categoriesOfItem =
    em.createQuery(
        "select c from Category c " +
            "join c.categorizedItems ci " +
            "where ci.item = :itemParameter")
    .setParameter("itemParameter", item)
    .getResultList();
<enter/>
assertEquals(categoriesOfItem.size(), 2);

You’ve now completed your first ternary association mapping. In the previous chapters, you saw ORM examples with maps; the keys and values of the shown maps were always of basic or embeddable type. In the following section, you see more complex key/value pair types and their mappings.

8.4. Entity associations with Maps

Map keys and values can be references to other entities, providing another strategy for mapping many-to-many and ternary relationships. First, let’s assume that only the value of each map entry is a reference to another entity.

8.4.1. One-to-many with a property key

If the value of each map entry is a reference to another entity, you have a one-to-many entity relationship. The key of the map is of a basic type: for example, a Long value.

An example of this structure would be the Item entity with a map of Bid instances, where each map entry is a pair of Bid identifier and reference to a Bid instance. When you iterate through someItem.getBids(), you iterate through map entries that look like (1, <reference to Bid with PK 1>), (2, <reference to Bid with PK 2>), and so on:

Path: /examples/src/test/java/org/jpwh/test/associations/MapsMapKey.java

The underlying tables for this mapping are nothing special; you have an ITEM and a BID table, with an ITEM_ID foreign key column in the BID table. This is the same schema as shown in figure 7.14 for a one-to-many/many-to-one mapping with a regular collection instead of a Map. Your motivation here is a slightly different representation of the data in the application.

In the Item class, include a Map property named bids:

Path: /model/src/main/java/org/jpwh/model/associations/maps/mapkey/Item.java

@Entity
public class Item {
<enter/>
    @MapKey(name = "id")
    @OneToMany(mappedBy = "item")
    protected Map<Long, Bid> bids = new HashMap<>();
<enter/>
    // ...
}

New here is the @MapKey annotation. It maps a property of the target entity, in this case the Bid entity, as the key of the map. The default if you omit the name attribute is the identifier property of the target entity, so the name option here is redundant. Because the keys of a map form a set, you should expect values to be unique for a particular map. This is the case for Bid primary keys but likely not for any other property of Bid. It’s up to you to ensure that the selected property has unique values—Hibernate won’t check.

The primary, and rare, use case for this mapping technique is the desire to iterate map entries with some property of the entry entity value as the entry key, maybe because it’s convenient for how you’d like to render the data. A more common situation is a map in the middle of a ternary association.

8.4.2. Key/Value ternary relationship

You may be a little bored by now, but we promise this is the last time we show another way to map the association between Category and Item. Previously, in section 8.3.3, you used an embeddable CategorizedItem component to represent the link. Here we show a representation of the relationship with a Map, instead of an additional Java class. The key of each map entry is an Item, and the related value is the User who added the Item to the Category, as shown in -figure 8.18.

Figure 8.18. A Map with entity associations as key/value pairs

The link/join table in the schema, as you can see in figure 8.19, has three columns: CATEGORY_ID, ITEM_ID, and USER_ID. The Map is owned by the Category entity:

Figure 8.19. The link table represents the Map key/value pairs.

Path: /model/src/main/java/org/jpwh/model/associations/maps/ternary/Category.java

The @MapKeyJoinColumn is optional; Hibernate would default to the column name ITEMADDEDBY_KEY for the join/foreign key column referencing the ITEM table.

To create a link between all three entities, all instances must already be in persistent state and then put into the map:

Path: /examples/src/test/java/org/jpwh/test/associations/MapsTernary.java

someCategory.getItemAddedBy().put(someItem, someUser);
someCategory.getItemAddedBy().put(otherItem, someUser);
otherCategory.getItemAddedBy().put(someItem, someUser);

To remove the link, remove the entry from the map. This is a convenient Java API for managing a complex relationship, hiding a database link table with three columns. But remember that in practice, link tables often grow additional columns, and changing all the Java application code later is expensive if you depend on a Map API. Earlier we had an ADDEDON column with a timestamp when the link was created, but we had to drop it for this mapping.

8.5. Summary

  • You learned how to map complex entity associations using one-to-one associations, one-to-many associations, many-to-many associations, ternary associations, and entity associations with maps.
  • Simplify the relationships between your classes, and you’ll rarely need many of the techniques we’ve shown. In particular, you can often best represent many-to-many entity associations as two many-to-one associations from an intermediate entity class, or with a collection of components.
  • Before you attempt a complex collection mapping, always make sure you actually need a collection. Ask yourself whether you frequently iterate through its elements.
  • The Java structures shown in this chapter may make data access easier sometimes, but typically they complicate data storage, updates, and deletion.
..................Content has been hidden....................

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