Mapping collections in Entity Views

Introduction to collection mappings in Blaze-Persistence Entity Views

By Christian Beikov on 09 May 2017

In the last post I have shown an introductory example of subviews to map singular associations. This time I will show how to map collections and how you can combine the powers of collections and subviews.

It is quite often necessary to load collections of data associated to an element. With plain JPA the normal way of doing this is to query the entity type and to do a JOIN FETCH of the *ToMany association. This works fine in the beginning, but quickly falls apart when more collections or sub-associations of a collection are needed for similar use cases. By trying to avoid code duplication, developers start adding JOIN FETCH clauses to an existing query just so they can "reuse" the base query logic for other areas of an application.

Sometimes they forget to add a JOIN FETCH and that results either in a LazyInitializationException or in the N + 1 queries problems depending on the scope of the EntityManager. JPAs answer to the code reuse problem is the Entity Graphs feature which was introduced in JPA 2.1. That still doesn’t help with a developer forgetting to actually add a subgraph for a use case that can lead to the previously mentioned problems.

A clean separation between the persistence model and the actual use-case specific read model a.k.a. DTO is required in order to hide the data that is not not used already at compile time. This will help prevent errors and allow for more efficient queries as a DTO will only contain the data that is absolutely necessary.

Entity model

This time I am going to use a post and comment data model which should be easy to understand but allows to show some features of Entity Views.

@Entity
public class User {
    @Id
    @GeneratedValue
    private Integer id;
    private String userName;
    private String email;
    private Date registrationDate;
    // Other attributes
}

@Entity
public class Post {
    @Id
    @GeneratedValue
    private Integer id;
    private String text;
	@ManyToOne(fetch = LAZY, optional = false)
    private User poster;
    @ElementCollection
	@OrderColumn
    private List<Comment> comments = new ArrayList<>();
}

@Embeddable
public class Comment {
	private String comment;
	@ManyToOne(fetch = LAZY, optional = false)
	private User commenter;
}

I intentionally added some irrelevant attributes to the User entity to make a point.

Entity View model

Let’s assume we have 2 use cases that we want to model. The first use case is to display a (paginated) list of posts and the second is to display a post with all comments.

To model the first use case, we introduce the following entity views.

@EntityView(User.class)
public interface UserNameView {
    String getUserName();
}

@EntityView(Post.class)
public interface PostListView {
    String getText();
    UserNameView getPoster();
}

It is only necessary to display the name of the user in the UI so we only declare the username attribute in the entity view. The entity view for the post in the (paginated) list will only have the text and the poster so we are done here.

Since we want to reuse code when possible, the entity views for the second use case will extend the existing entity views.

@EntityView(Comment.class)
public interface CommentView {
    String getComment();
    UserNameView getCommenter();
}

@EntityView(Post.class)
public interface PostDetailView extends PostListView {
    List<CommentView> getComments();
}

Which brings us to the mapping of the comments collection in the entity view. In the entity model the attribute comments refers to an element collection with the element type Comment. In PostDetailView the comments attribute maps to a collection with the element type CommentView which is perfectly fine as that entity view is specified as being a projection of the embeddable type Comment.

The good thing about the CommentView is, that it makes use of the pre-existing UserNameView which will massively reduce the amount of data transfer and at the same time increase the performance by only fetching the necessary data userName instead of all the properties of a user.

Using the entity views works just as always.

CriteriaBuilder<Post> cb = criteriaBuilderFactory.create(entityManager, Post.class);
cb.from(Post.class, "thePost");

EntityViewSetting<PostListView, CriteriaBuilder<PostListView>> setting = EntityViewSetting.create(PostListView.class);
List<PostListView> list = entityViewManager
                        .applySetting(setting, cb)
                        .getResultList();

Not only do you get efficient queries when using entity views as I will show in a moment, but you also benefit from better maintainability. Imagine how much work it would normally take to add a new attribute that is needed in your UI.

  1. Add select item to the backing query

  2. Add optional join to the backing query

  3. Add getter and setter or constructor parameter to DTO

  4. Pass-through value into DTO

Now if you have multiple queries that feed the model this is going to be very painful.

With entity views, you just add the attribute to the interface with an appropriate mapping and you are done!

Behind the scenes

When querying the PostListView like above you will get a JPQL query that approximately looks like this.

SELECT
    thePost.text,
    poster_1.userName
FROM
    Post thePost
JOIN
    thePost.poster poster_1

Nothing special apart from this being a very efficient query for the use case compared to a plain entity query. The interesting stuff happens when querying the PostDetailView

SELECT
    thePost.text,
    poster_1.userName,
    INDEX(comments_1),
    comments_1.comment,
    commenter_1.userName
FROM
    Post thePost
JOIN
    thePost.poster poster_1
LEFT JOIN
    thePost.comments comments_1
LEFT JOIN
    comments_1.commenter commenter_1

The first thing that you might have noticed is that Blaze-Persistence used an INNER JOIN for the non-optional association poster and in case of the commenter correctly made use of a LEFT JOIN. Thanks to a feature called model-aware joins it is possible to infer that the poster relation is safe to INNER JOIN which might improve performance. The commenter association, although being optional = false must use LEFT JOIN as it’s join parent, the comments association doesn’t use an INNER JOIN.

The next thing you might notice is that the query also selects the INDEX of the joined comments which happens because of multiple reasons

  1. The comments association in the entity model has an @OrderColumn that defines the order of elements within the list

  2. The entity view attribute comments is mapped as list which reuses an order column index if possible.

All in all, the query is again, quite optimized for the use case as opposed to a entity type query with JOIN FETCH statements.

Note that it is also possible to map the user name directly into the PostListView which will emit the same query.

@EntityView(Post.class)
public interface PostListView {
    String getText();
    @Mapping("poster.userName")
    String getPosterName();
}

Similarly, it is also possible to map the comment directly into a collection as opposed to using a subview.

@EntityView(Post.class)
public interface PostDetailView extends PostListView {
	@Mapping("comments.comment")
    List<String> getComments();
}

Just be aware that it is not possible to map the same collection multiple times. So the following would be illegal.

@EntityView(Post.class)
public interface PostDetailView extends PostListView {
	@Mapping("comments.comment")
    List<String> getComments();
	@Mapping("comments.commenter.name")
    List<String> getCommenterNames();
}

Last but not least, it is also possible to use the JPA types for the mapping. I generally do not recommend the use since you will have to worry about the usual LazyInitializationException or N + 1 queries problems again, but sometimes there is no other way.

@EntityView(Post.class)
public interface PostDetailView extends PostListView {
	@Mapping(fetches = {"commenter"})
    List<Comment> getComments();
}

Querying this entity view will fetch join the commenter association instead of just joining and selecting the name.

SELECT
    thePost.text,
    poster_1.userName,
    INDEX(comments_1),
    comments_1
FROM
    Post thePost
JOIN
    thePost.poster poster_1
LEFT JOIN
    thePost.comments comments_1
LEFT JOIN FETCH
    comments_1.commenter commenter_1

This is not very efficient as the SQL will load all properties of the user. Also note that there are some bugs related to element collections that were only fixed in recent Hibernate versions so you might have to replace the @ElementCollection mapping in the entity model with a @OneToMany mapping to be able to test the presented code.

Conclusion

With collection mappings and subviews you can already implement many use cases with very little effort. In a future article I will come back to subviews and present some other features, but in the next article I am going to focus on making use of correlated subqueries in entity views for doing things like counting elements or finding maximum elements.

Stay tuned!

blaze-persistence jpa entity-view collection subview
comments powered by Disqus