• No results found

This custom equality check in equals() B compares all values of one Image to the values of another Image If all values are the same, then the images must be the same.

7.3 Mapping entity associations

7.3.3 Cascading state

If an entity state change can be cascaded across an association to another entity, you need fewer lines of code to manage relationships. The following code creates a new Item and a new Bid and then links them:

Item someItem = new Item("Some Item");

Bid someBid = new Bid(new BigDecimal("123.00"), someItem); someItem.getBids().add(someBid);

Listing 7.15 Item has a collection of Bid references

PATH: /model/src/main/java/org/jpwh/model/associations/onetomany/ bidirectional/Item.java Required for bidirectional association Default Don’t forget! Item 0..* Bid Figure 7.15 Bidirectional association between Item and Bid

You have to consider both sides of this relationship: the Bid constructor accepts an item, used to populate Bid#item. To maintain integrity of the instances in memory, you need to add the bid to Item#bids. Now the link is complete from the perspective of your Java code; all references are set. If you aren’t sure why you need this code, please see section 3.2.4.

Let’s save the item and its bids in the database, first without and then with transi- tive persistence.

ENABLINGTRANSITIVEPERSISTENCE

With the current mapping of @ManyToOne and @OneToMany, you need the following code to save a new Item and several Bid instances.

Item someItem = new Item("Some Item"); em.persist(someItem);

Bid someBid = new Bid(new BigDecimal("123.00"), someItem); someItem.getBids().add(someBid);

em.persist(someBid);

Bid secondBid = new Bid(new BigDecimal("456.00"), someItem); someItem.getBids().add(secondBid);

em.persist(secondBid); tx.commit();

When you create several bids, calling persist() on each seems redundant. New instances are transient and have to be made persistent. The relationship between Bid and Item doesn’t influence their life cycle. If Bid were to be a value type, the state of a Bid would be automatically the same as the owning Item. In this case, however, Bid has its own completely independent state.

We said earlier that fine-grained control is sometimes necessary to express the dependencies between associated entity classes; this is such a case. The mechanism for this in JPA is the cascade option. For example, to save all bids when the item is saved, map the collection as shown next.

@Entity

public class Item {

@OneToMany(mappedBy = "item", cascade = CascadeType.PERSIST) protected Set<Bid> bids = new HashSet<>();

// ...

}

Listing 7.16 Managing independent entity instances separately

PATH: /examples/src/test/java/org/jpwh/test/associations/ OneToManyBidirectional.java

Listing 7.17 Cascading persistent state from Item to all bids

PATH: /model/src/main/java/org/jpwh/model/associations/onetomany/ cascadepersist/Item.java

Don’t forget!

Cascading options are per operation you’d like to be transitive, so you use Cascade- Type.PERSIST for the EntityManager#persist() operation. You can now simplify the code that links items and bids and then saves them.

Item someItem = new Item("Some Item"); em.persist(someItem);

Bid someBid = new Bid(new BigDecimal("123.00"), someItem); someItem.getBids().add(someBid);

Bid secondBid = new Bid(new BigDecimal("456.00"), someItem); someItem.getBids().add(secondBid);

tx.commit();

At commit time, Hibernate examines the managed/persistent Item instance and looks into the bids collection. It then calls persist() internally on each of the refer- enced Bid instances, saving them as well. The value stored in the column BID#ITEM_ID is taken from each Bid by inspecting the Bid#item property. The foreign key column is “mapped by” with @ManyToOne on that property.

The @ManyToOne annotation also has the cascade option. You won’t use this often. For example, we can’t really say “when the bid is saved, also save the item”. The item has to exist beforehand; otherwise, the bid won’t be valid in the database. Think about another possible @ManyToOne: the Item#seller property. The User has to exist before they can sell an Item.

Transitive persistence is a simple concept, frequently useful with @OneToMany or @ManyToMany mappings. On the other hand, you have to apply transitive deletion carefully.

CASCADINGDELETION

It seems reasonable that deletion of an item implies deletion of all the bids for the item, because they’re no longer relevant alone. This is what the composition (the filled-out diamond) in the UML diagram means. With the current cascading options, you have to write the following code to delete an item:

Item item = em.find(Item.class, ITEM_ID);

for (Bid bid : item.getBids()) { em.remove(bid);

}

em.remove(item);

Listing 7.18 All referenced bids are automatically made persistent

PATH: /examples/src/test/java/org/jpwh/test/associations/ OneToManyCascadePersist.java

PATH: /examples/src/test/java/org/jpwh/test/associations/ OneToManyCascadePersist.java

Saves the bids automatically (later, at flush time)

Dirty checking; SQL execution Removes bids

B

Removes owner

C

First you remove the bids B, and then you remove the owner: the Item C. The deletion order is important. If you remove the Item first, you’ll get a foreign key–constraint vio- lation, because SQL operations are queued in the order of your remove() calls. First the row(s) in the BID table have to be deleted, and then the row in the ITEM table.

JPA offers a cascading option to help with this. The persistence engine can remove an associated entity instance automatically.

@Entity

public class Item {

@OneToMany(mappedBy = "item",

cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) protected Set<Bid> bids = new HashSet<>();

// ...

}

Just as before with PERSIST, Hibernate now cascades the remove() operation on this association. If you call EntityManager#remove() on an Item, Hibernate loads the bids collection elements and internally calls remove() on each instance:

Item item = em.find(Item.class, ITEM_ID); em.remove(item);

The collection must be loaded because each Bid is an independent entity instance and has to go through the regular life cycle. If there is an @PreRemove callback method present on the Bid class, Hibernate has to execute it. You’ll see more on object states and callbacks in chapter 13.

This deletion process is inefficient: Hibernate must always load the collection and delete each Bid individually. A single SQL statement would have the same effect on the database: delete from BID where ITEM_ID = ?.

You know this because nobody in the database has a foreign key reference on the BID table. Hibernate doesn’t know this and can’t search the whole database for any row that might have a BID_ID.

If Item#bids was instead a collection of embeddable components, someItem .getBids().clear() would execute a single SQL DELETE. With a collection of value types, Hibernate assumes that nobody can possibly hold a reference to the bids, and removing only the reference from the collection makes it orphan removable data.

Listing 7.19 Cascading removal from Item to all bids

PATH: /model/src/main/java/org/jpwh/model/associations/onetomany/ cascaderemove/Item.java

PATH: /examples/src/test/java/org/jpwh/test/associations/ OneToManyCascadeRemove.java

Deletes bids one by one after loading them

ENABLINGORPHANREMOVAL

JPA offers a (questionable) flag that enables the same behavior for @OneToMany (and only @OneToMany) entity associations.

@Entity

public class Item {

@OneToMany(mappedBy = "item",

cascade = CascadeType.PERSIST, orphanRemoval = true)

protected Set<Bid> bids = new HashSet<>();

// ...

}

The orphanRemoval=true argument tells Hibernate that you want to permanently remove a Bid when it’s removed from the collection. Here is an example of deleting a single Bid:

Item item = em.find(Item.class, ITEM_ID);

Bid firstBid = item.getBids().iterator().next(); item.getBids().remove(firstBid);

Hibernate monitors the collection and on transaction commit will notice that you removed an element from the collection. Hibernate now considers the Bid to be orphaned. You guarantee that nobody else had a reference to it; the only reference was the one you just removed from the collection. Hibernate automatically executes an SQL DELETE to remove the Bid instance in the database.

You still won’t get the clear() one-shot DELETE as with a collection of components. Hibernate respects the regular entity-state transitions, and the bids are all loaded and removed individually.

Why is orphan removal questionable? Well, it’s fine in this example case. There is so far no other table in the database with a foreign key reference on BID. There are no con- sequences to deleting a row from the BID table; the only in-memory references to bids are in Item#bids. As long as all of this is true, there is no problem with enabling orphan removal. It’s a convenient option, for example, when your presentation layer can remove an element from a collection to delete something; you only work with domain model instances, and you don’t need to call a service to perform this operation.

Consider what happens when you create a User#bids collection mapping—another @OneToMany, as shown in figure 7.16. This is a good time to test your knowledge of Hibernate: what will the tables and schema look like after this change? (Answer: The BID table has a BIDDER_ID foreign key column, referencing USERS.)

Listing 7.20 Enabling orphan removal on a @OneToMany collection

PATH: /model/src/main/java/org/jpwh/model/associations/onetomany/ orphanremoval/Item.java Path: /examples/src/test/java/org/jpwh/test/associations/ OneToManyOrphanRemoval.java Includes CascadeType.REMOVE

The test shown in the following listing won’t pass.

User user = em.find(User.class, USER_ID); assertEquals(user.getBids().size(), 2);

Item item = em.find(Item.class, ITEM_ID);

Bid firstBid = item.getBids().iterator().next(); item.getBids().remove(firstBid);

// FAILURE!

// assertEquals(user.getBids().size(), 1);

assertEquals(user.getBids().size(), 2);

Hibernate thinks the removed Bid is orphaned and deletable; it will be deleted auto- matically in the database, but you still hold a reference to it in the other collection, User#bids. The database state is fine when this transaction commits; the deleted row of the BID table contained both foreign keys, ITEM_ID and BIDDER_ID. You have an inconsistency in memory, because saying, “Remove the entity instance when the refer- ence is removed from the collection” naturally conflicts with shared references.

Instead of orphan removal, or even CascadeType.REMOVE, always consider a sim- pler mapping. Here, Item#bids would be fine as a collection of components, mapped with @ElementCollection. The Bid would be @Embeddable and have an @ManyToOne bidder property, referencing a User. (Embeddable components can own unidirec- tional associations to entities.)

This would provide the life cycle you’re looking for: a full dependency on the own- ing entity. You have to avoid shared references; the UML diagram (figure 7.16) makes the association from Bid to User unidirectional. Drop the User#bids collection; you don’t need this @OneToMany. If you need all the bids made by a user, write a query: select b from Bid b where b.bidder = :userParameter. (In the next chapter, you’ll complete this mapping with an @ManyToOne in an embeddable component.)

ENABLING ON DELETE CASCADE ONTHEFOREIGNKEY

All the removal operations we’ve shown are inefficient; bids have to be loaded into memory, and many SQL DELETEs are necessary. SQL databases support a more efficient foreign key feature: the ON DELETE option. In DDL, it looks like this: foreign key (ITEM_ID) references ITEM on delete cascade for the BID table.

Listing 7.21 Hibernate doesn’t clean up in-memory references after database removal

PATH: /examples/src/test/java/org/jpwh/test/associations/ OneToManyOrphanRemoval.java