Reference by Identity in JPA
In a previous post, I mentioned that I opted to reference other aggregates by their primary key, and not by type. I usually use this approach (a.k.a. disconnected domain model) when working with large or complex domain models. In this post, let me try to explain further how it can be done in JPA. Note that the resulting DDL scripts will not create a foreign key constraint (unlike the one shown in the previous post).
Reference by Identity
In most JPA examples, every entity references another entity, or is being referenced by another entity. This results into an object model that allows traversal from one entity to any other entity. This can cause unwanted traversals (and unwanted cascade of persistence operations). As such, it would be good to prevent this, by referencing other entities by ID (and not by type).
The code below shows how OrderItem
references a Product
entity by its primary key (and not by type).
@Entity public class Product { @Id private Long id; // ... } @Entity public class Order { // ... @OneToMany(mappedBy="order") private Collection<OrderItem> items; } @Entity public class OrderItem { // ... @ManyToOne private Order order; // @ManyToOne // private Product product; private Long productId; // ... }
There are several ways to get the associated Product
entities. One way is to use a repository to find products given the IDs (ProductRepository
with a findByIdIn(List<Long> ids)
method). As mentioned in previous comments, please be careful not to end up with the N+1 selects problem.
Custom identity types can also be used. The example below uses ProductId
. It is a value object. And because of JPA, we needed to add a zero-arguments constructor.
@Embeddable public class ProductId { private Long id; public ProductId(long id) { this.id = id; } public long getValue() { return id; } // equals and hashCode protected ProductId() { /* as required by JPA */ } } @Entity public class Product { @EmbeddedId private ProductId id; // ... } @Entity public class Order { // ... @OneToMany(mappedBy="order") private Collection<OrderItem> items; } @Entity public class OrderItem { // ... @ManyToOne private Order order; // @ManyToOne // private Product product; @Embedded private ProductId productId; // ... }
But this will not work if you’re using generated values for IDs. Fortunately, starting with JPA 2.0, there are some tricks around this, which I’ll share in the next section.
Generated IDs
In JPA, when using non-@Basic
types as @Id
, we can no longer use @GeneratedValue
. But using a mix of property and field access, we can still use generated value and ProductId
.
@Embeddable @Access(AccessType.FIELD) public class ProductId {...} @Entity @Access(AccessType.FIELD) public class Product { @Transient private ProductId id; public ProductId getId() { return id; } // ... private Long id_; @Id @GeneratedValue(strategy=...) @Access(AccessType.PROPERTY) protected Long getId_() { return id_; } protected void setId_(Long id_) { this.id_ = id_; this.id = new ProductId(this.id_); } } @Entity public class Order { // ... @OneToMany(mappedBy="order") private Collection<OrderItem> items; } @Entity public class OrderItem { // ... @ManyToOne private Order order; // @ManyToOne // private Product product; @Embedded private ProductId productId; // ... }
The trick involves using property access for the generated ID value (while keeping the rest with field access). This causes JPA to use the setter method. And in it, we initialize the ProductId
field. Note that the ProductId
field is not persisted (marked as @Transient
).
Hope this helps.
Reference: | Reference by Identity in JPA from our JCG partner Lorenzo Dee at the Adapting and Learning blog. |