Implementing Aggregate Root In JPA: @OneToMany or @ElementCollection?


In domain driven design, an aggregate root contains one or more entities that represent a bounded context. Those entities should be only manipulated from their aggregate root. In UML class diagram, this is represented as composition (a filled diamond in relationship). Note that UML class diagram also has a concept of aggregation (a hollow diamond in a relationship). Despite similarity in the name, the contained part in UML aggregation can exists without its container. Thus, it is not something like DDD’s aggregate root in which the contained part should not exist without their container.

For example, the following is an UML class diagram with composition:

Composition in UML Class Diagram

Composition in UML Class Diagram

Invoice is the root aggregate that manages LineItem. Every LineItem is a value object. No one should be able to add or delete LineItem directly without obtaining an Invoice first. Because instances of LineItem are value objects, they don’t have a global identity. In the other side, instances of Invoice class are entities so they can be searched by a global identity (for example: invoice number). Each LineItem is associated with a Product entity. This is valid in domain driven design though some people will recommend using value object instead. The value object will store the Product identity (for example: product number). See this article for more information: http://dddcommunity.org/wp-content/uploads/files/pdf_articles/Vernon_2011_2.pdf.

The question is how to implement the classes in our diagram using JPA? Well, there are several possibilities with surprising caveats. The recommended way for @OneToMany relationship is bidirectional with owner on the many side. But this violates our aggregate root rules. No one should select existing LineItem or add new LineItem directly! They must manipulate LineItem from Invoice only. This can be solved by using unidirectional @OneToMany with @JoinColumn. But it is still not a containment. To implement the real containment, use @ElementCollection and mark all value objects as @Embeddable. Note that @ElementCollection is introduced in JPA 2.

For example, this is an implementation using @OneToMany and @JoinColumn in Groovy + Hibernate +simple-jpa:

import ...

@DomainClass @Entity @Canonical
class Invoice {

    @NotEmpty
    String number

    @OneToMany @JoinColumn
    List<LineItem> lineItems = []

    public void add(LineItem lineItem) {
        lineItems << lineItem
    }

}


@DomainClass @Entity @Canonical
class LineItem {

    @NotNull @ManyToOne
    Product product

    @NotNull
    Integer quantity

}


@DomainClass @Entity @Canonical
class Product {

    @NotEmpty
    String name

}

The code above will produce the following database tables:

Tables for @OneToMany with @JoinColumn

Tables for @OneToMany with @JoinColumn

Table for LineItem has an identity. This primary key is required for one to many relationships. In our case, it is pretty useless because LineItem should only be identified with their Invoice. The identity of LineItem has no meaning in global context.

This code will create several objects based on our domain classes:

def productA = new Product('Product A')
persist(productA)
def productB = new Product('Product B')
persist(productB)

def invoice = new Invoice('Invoice-01')
invoice.add(new LineItem(productA, 10))
invoice.add(new LineItem(productB, 20))
persist(invoice)

But if you execute the code above, you will get org.hibernate.TransientObjectException!! Every single LineItem must be persisted before persisting Invoice. This is a bit annoying. It doesn’t show that our Invoice is the boss – the aggregate root. To solve this problem, you will need to add @OneToMany(cascade=CascadeType.ALL) to Invoice.lineItems:

...
@OneToMany(cascade=CascadeType.ALL) @JoinColumn
List<LineItem> lineItems = []
...

Now if you run the code, Hibernate will actually perform the following SQL queries:

insert into Product (createdDate, deleted, modifiedDate, name, id) values (?, ?, ?, ?, ?)
insert into Product (createdDate, deleted, modifiedDate, name, id) values (?, ?, ?, ?, ?)
insert into Invoice (createdDate, deleted, modifiedDate, number, id) values (?, ?, ?, ?, ?)
insert into LineItem (createdDate, deleted, modifiedDate, product_id, quantity, id) values (?, ?, ?, ?, ?, ?)
insert into LineItem (createdDate, deleted, modifiedDate, product_id, quantity, id) values (?, ?, ?, ?, ?, ?)
update LineItem set lineItems_id=? where id=?
update LineItem set lineItems_id=? where id=?

Note that Hibernate will issue both insert and update query for every LineItem. So, if I insert 10 LineItem objects, Hibernate will issue 20 queries: 10 insert queries and another 10 update queries. Isn’t this a bit overwhelming?

What happened if I update existing LineItem or insert new LineItem such as shown in the following code:

def invoice = findInvoiceByNumberFetchComplete('Invoice-01')
invoice.lineItems[0].product = findProductByName('Product B')
invoice.lineItems[0].quantity = 999
invoice.lineItems.remove(1)
merge(invoice)

If I run the code, Hibernate will execute the following queries:

...
update LineItem set createdDate=?, deleted=?, modifiedDate=?, product_id=?, quantity=? where id=?
update LineItem set lineItems_id=null where lineItems_id=? and id=?
...

Hibernate only issue two update queries! Even when we remove the second LineItem in our Invoice in the source code, Hibernate doesn’t actually remove it from database. Hibernate only set LineItem.lineItems_id to null so in the next select query, we will not see that second item. To force Hibernate to delete the second item, add orphanRemoval=true to @OneToMany as shown in the following code:

...
@OneToMany(cascade=CascadeType.ALL, orphanRemoval=true) @JoinColumn
List<LineItem> lineItems = []
...

The updated mapping will generate the following queries:

update LineItem set createdDate=?, deleted=?, modifiedDate=?, product_id=?, quantity=? where id=?
update LineItem set lineItems_id=null where lineItems_id=? and id=?
delete from LineItem where id=?

While it is possible to implement aggregate roots and their managed objects using @OneToMany, developers can still manipulate objects directly without their aggregate roots. Our LineItem is required to have an identity in the mapping but we know that value objects shouldn’t have a global identity. Now imagine if you have several genius kids in your team who don’t like to follow your domain driven design rules! They code in whatever direction they want because they think they can!! When the system grows larger, some of the genius kids resigned and new kids join your team. They even do a big refactoring. At the end, you may have a big ball of mud. See http://laputan.org/mud/ for more information about this anti pattern.

To create a more restricted implementation, you can use @ElementCollection. One of the possible implementation using @ElementCollection will be:

@DomainClass @Entity @Canonical
class Invoice {

    @NotEmpty
    String number

    @ElementCollection
    List<LineItem> lineItems = []

    public void add(LineItem lineItem) {
        lineItems << lineItem
    }

}

@Embeddable @Canonical
class LineItem {

    @NotNull @ManyToOne
    Product product

    @NotNull
    Integer quantity

}

@DomainClass @Entity @Canonical
class Product {

    @NotEmpty
    String name

}

Note that LineItem is annotated with @Embeddable not @Entity. In JPA, @Embeddable is used for value object. @Embeddable class doesn’t have an identity just like what a value object should be. But this imposes a limitation: an embeddable class can have collections, but if it is embedded in another embeddab class, it can’t have collections. This is an important limitation if you’re going to implement all managed classes as @Embeddable!

The new domain classes will produce the following tables:

Tables for @ElementCollection and @Embeddable

Tables for @ElementCollection and @Embeddable

To insert new objects, I use the same code as in @OneToMany mapping. While it is the same code, Hibernate now generates different queries:

insert into Product (createdDate, deleted, modifiedDate, name, id) values (?, ?, ?, ?, ?)
insert into Product (createdDate, deleted, modifiedDate, name, id) values (?, ?, ?, ?, ?)
insert into Invoice (createdDate, deleted, modifiedDate, number, id) values (?, ?, ?, ?, ?)
insert into Invoice_lineItems (Invoice_id, product_id, quantity) values (?, ?, ?)
insert into Invoice_lineItems (Invoice_id, product_id, quantity) values (?, ?, ?)

Note that there are no more annoying updates like in @OneToMany mapping. How about update and delete? The same code now will generate the following queries:

delete from Invoice_lineItems where Invoice_id=?
insert into Invoice_lineItems (Invoice_id, product_id, quantity) values (?, ?, ?)

Wait, this is a big difference!! Hibernate will always delete all LineItem records from our Invoice before re-inserting old, updated and new records. Hibernate must do this because our LineItem doesn’t have an identity. Hibernate doesn’t know which records to update or delete if there is no identity on the records.
This behavior is acceptable in small collection. But this is not efficient for a large collection because Hibernate will re-create all records even when only one LineItem is changed. To avoid such case, you can use @OrderColumn in the List, such as:

...
@ElementCollection @OrderColumn
List<LineItem> lineItems = []
...

This new mapping will add a new field to Lineitem table that stores index number (remember that List is a number indexed collection).

Tables for @ElementCollection with @OrderColumn

Tables for @ElementCollection with @OrderColumn

With this mapping, the update and delete code will execute the following queries:

delete from Invoice_lineItems where Invoice_id=? and lineItems_ORDER=?
update Invoice_lineItems set product_id=?, quantity=? where Invoice_id=? and lineItems_ORDER=?

Now, Hibernate will not delete all line item but only delete the second line item (because it was deleted in the following code: invoice.lineItems.remove(1)).

Perihal Solid Snake
I'm nothing...

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: