Wednesday, July 20, 2016

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.

No comments:

Post a Comment