By Christian Beikov on 29 November 2018
Introduction
This time we will take a little detour from presenting the different mapping types like in the last post and instead take a look at the filtering and sorting mechanism provided by Blaze-Persistence Entity Views.
When presenting data to an end-user you often try to present a denormalized view of the data that looks natural. Imagine you have a User
type that has firstname
, lastname
and username
attributes. Such a model is nicely normalized and for UIs that display just users, it’s probably a good idea to display the data in the normalized form.
When presenting the user as part of a different object i.e. as author of a post, we usually want to display the values in an aggregate/denormalized form. Imagine that you want to display posts in an overview table and also want to show the author of the post, choosing a normalized form might not be a good idea.
Title | Publish date | Author firstname | Author lastname | Author username | Text |
---|---|---|---|---|---|
Post 1 |
2018-11-01 |
Christian |
Beikov |
cbeikov |
Lorem impsum… |
It’s easy to understand for a human what the firstname and the lastname is when presenting it as aggregate, there is no need to label the parts explicitly. Let’s say we decide to encode the author as firstname + " " + lastname + " (" username + ")"
and just use a single column in the UI.
Title | Publish date | Author | Text |
---|---|---|---|
Post 1 |
2018-11-01 |
Christian Beikov (cbeikov) |
Lorem impsum… |
This already looks much nicer. Now at some point a user would like to filter and sort posts based on the author name, so you have to decide how to implement that filter. You could provide the user the possibility to filter based on firstname, lastname and username separately, but that would be cumbersome. Wouldn’t it be much nicer to let the user filter based on the value that is actually visible?
This is where attribute filters and sorters for entity views show their strength!
The source code for the following examples can be found on GitHub, so you can play around with it.
Entity model
Here is the post and comment data model with the newly added firstname and lastname attributes that we are going to use for this example.
@Entity public class User { @Id @GeneratedValue Integer id; String firstname; String lastname; String userName; String email; @Temporal(TemporalType.DATE) Date registrationDate; // Other attributes } @Entity public class Post { @Id @GeneratedValue Integer id; @Temporal(TemporalType.DATE) Date publishDate; String title; String text; @ManyToOne(fetch = LAZY, optional = false) User poster; @ElementCollection @OrderColumn List<Comment> comments = new ArrayList<>(); } @Embeddable public class Comment { String comment; @ManyToOne(fetch = LAZY, optional = false) User commenter; }
Nothing special, just a simple entity model.
Entity View model
The entity view model essentially is a subset of the entity model, except for two transformations
-
We only want the first 100 characters of the text
-
We format the author name with a plain JPQL expression
@EntityView(Post.class) public interface NormalPostView { @IdMapping Integer getId(); String getTitle(); Date getPublishDate(); SimpleUserView getPoster(); @Mapping("SUBSTRING(text, 1, 100)") String getText(); } @EntityView(User.class) public interface SimpleUserView { @IdMapping Integer getId(); @AttributeFilter(ContainsIgnoreCaseFilter.class) (1) @Mapping("CONCAT(firstname, ' ', lastname, ' (', userName, ')')") String getName(); }
1 | We declare the default filter to be used for the attribute |
The default filter definition itself doesn’t cause anything yet, the filter still has to be activated and provided with a filter value which is done through the EntityViewSetting
object.
EntityViewSetting<NormalPostView, CriteriaBuilder<NormalPostView>> setting = EntityViewSetting.create(NormalPostView.class); setting.addAttributeFilter("poster.name", "joe");
We created a setting to fetch NormalPostView
instances with a filter on the name
attribute of the poster
and specified the filter value "joe"
. When retrieving instances with that setting, the contains ignore case filter will be applied on the mapping expression as defined by the attribute. In this case, a contains ignore case filter is rendered as UPPER(CONCAT(poster.firstname, ' ', poster.lastname, ' (', poster.userName, ')')) LIKE UPPER('%joe%')
.
To implement filters based on attributes, you don’t have to repeat the mapping, just declare an appropriate filter on that attribute and activate the filter with a filter value. Imagine you would implement a DTO approach with plain JPQL and wanted to support such filters or sorters. You would have to duplicate the expression manually, making your query unreadable. The same also works for sorters! Enabling the sorter is a matter of defining the desired sort order for the attribute.
EntityViewSetting<NormalPostView, CriteriaBuilder<NormalPostView>> setting = EntityViewSetting.create(NormalPostView.class); setting.addAttributeSorter("poster.name", Sorters.ascending());
This will produce an ORDER BY
clause that uses the select alias of the mapping expression i.e. ORDER BY NormalPostView_poster_name ASC
.
Behind the scenes
There are quite a few default filters available that can handle the most common filtering use cases, but sometimes that’s not enough which is why attribute filters are extendable.
An attribute filter is defined as an implementation of the abstract class AttributeFilterProvider
. The class may have a constructor that accepts the attribute type as Class
and the filter value as Object
. The access to the WhereBuilder
API within the AttributeFilterProvider
implementation make it possible to do anything necessary to implement a filter. A common requirement is a range filter which has a few edge cases and thus doesn’t have an out of the box implementation yet. A custom implementation could look like this:
@EntityView(Post.class) public interface NormalPostView { @AttributeFilter(MyCustomFilter.class) Date getPublishDate(); public static class MyCustomFilter extends AttributeFilterProvider { private final Range range; public MyCustomFilter(Object value) { this.value = (Range) value; } protected <T> T apply(RestrictionBuilder<T> restrictionBuilder) { return restrictionBuilder.between(range.lower).and(range.upper); } public static class Range { private final Date lower; private final Date upper; public Range(Date lower, Date upper) { this.lower = lower; this.upper = upper; } } } }
To enable the filter use setting.addAttributeFilter("publishDate", new Range(lower, upper))
. For further information on custom filters you can take a look at the range filter implementation example shown in the documentation.
To implement attribute independent filters one can also make use of the ViewFilterProvider
interface to encapsulate common filters.
@EntityView(Post.class) @ViewFilter(name = "publishedPosts", value = PublishedPostsFilterProvider.class) public interface NormalPostView { class PublishedPostsFilterProvider implements ViewFilterProvider { @Override public <T extends WhereBuilder<T>> T apply(T whereBuilder) { return whereBuilder.where("publishDate").ltExpression("CURRENT_TIMESTAMP"); } } // ... }
Enabling this filter with setting.addViewFilter("publishedPosts")
will ensure only posts with a publish date in the past are visible.
Conclusion
Although this was only a short introduction, we have already seen how attribute and view filters provide benefits. Contrary to the Hibernate filter concept, attribute and view filters work on the JPQL/entity level rather than the SQL level.
That’s it for this detour. The next time I’ll show you how you can use @MappingSingular
to map a DBMS array as collection into entity views.
Stay tuned!