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.
-
Add select item to the backing query
-
Add optional join to the backing query
-
Add getter and setter or constructor parameter to DTO
-
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
-
The
comments
association in the entity model has an@OrderColumn
that defines the order of elements within the list -
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!