Use Java 14 records as entity views

Use Java 14 records as entity views

By Christian Beikov on 11 June 2020

Introduction

In the last few weeks I saw a few blog posts and twitter statuses advertising that libraries just added support for Java 14 records or stated that they have been supporting it all along because there is nothing special to it. I have wanted to support custom entity view class implementations for a while and I finally managed to get around adding support for that. The best thing about it? You can now also use Java 14 records as entity views!

Java 14 introduces a new preview language feature called "records" which allows to eliminate language ceremony for immutable data holder types. Brian Goetz, the Java language architect, refers to records as being nominal tuples that are shallowly immutable.

What this means is that records …​

  • expose their state i.e. the record components through accessor methods

  • are immutable and can not have any additional state

  • define a canonical constructor through which the record can be constructed based on component values

In addition, records also implement the equals, hashCode and toString methods based on the record component state.

Records and custom classes as Entity Views

A record can be used in many different scenarios, but the DTO use case is particularly interesting in relation to Blaze-Persistence Entity-Views. Using entity views as immutable DTOs is the most basic use case but so far, entity views could only be declared as abstract classes or interfaces. The entity view implementation was either generated at compile time through the annotation processor that was introduced in version 1.5.0-Alpha2 or at runtime.

As of version 1.5.0-Alpha3 we also support to declare custom classes as entity views. We naturally support records as well as they are just special classes with reduced language ceremony. Let’s take a look at a simple example!

Here is the entity model that we are going to use:

@Entity
public class User {
    @Id
    @GeneratedValue
    private Integer id;
    private String userName;
    private String email;
    @Temporal(TemporalType.DATE)
    private Date registrationDate;
    // Other attributes
}

And here comes the record as entity view

@EntityView(User.class)
public record UserRecord(@IdMapping Integer id, String userName) {
}

That’s pretty compact, and you can use it almost like any other entity view! You could have also declared the entity view as full blown class, or even as Lombok data class.

@Data
@EntityView(User.class)
public class UserData {
    @IdMapping
    private final Integer id;
    private final String userName;
}

which roughly translates to

@EntityView(User.class)
public class UserData {
    @IdMapping
    private final Integer id;
    private final String userName;

    public UserData(Integer id, String userName) {
        this.id = id;
        this.userName = userName;
    }

    public Integer getId() {
        return id;
    }

    public String getUserName() {
        return userName;
    }

    // equals, hashCode, toString
}

If you want to be in full control of your classes this is definitely something worth considering, but beware that there are a few limitations:

  • You must define a canonical constructor where each field is initialized by a parameter

  • Custom classes can only be used for immutable or read-only entity views, not for @UpdatableEntityView or @CreatableEntityView

  • Currently, no static metamodel is generated for these custom classes

The need for the canonical constructor is trivially fulfilled by a record, but custom classes that define multiple constructors might need to mark the canonical constructor with @ViewConstructor("init"). It is important that the canonical constructor does not alter the passed parameters before they are assigned to fields. The following would lead to an error during entity view validation.

@EntityView(User.class)
public class UserData {
    @IdMapping
    private final Integer id;
    private final String userName;

    public UserData(Integer id, String userName) {
        this.id = id;
        this.userName = userName.toUpperCase(); // This is disallowed
    }

    // getters, equals, hashCode, toString
}

You can do some checks to assert invariants, but entity views need to assign the parameters directly to field in order to be able to link parameters to fields. The following example shows a validation which is totally fine:

@EntityView(User.class)
public record UserRecord(@IdMapping Integer id, String userName) {
    public UserRecord {
        Objects.requireNonNull(id);
        Objects.requireNonNull(userName);
    }
}

The generation of a static metamodel for custom entity view classes will be done in a future version, because that requires code analysis to detect the attributes of an entity view which is a bit harder to do at compile time than at runtime.

The case for interfaces and abstract classes

Although the use of custom classes or records is a nice addition and will certainly have some use cases, interfaces and abstract classes are still far superior for entity views. Let’s consider the simple record example when modeled as interface:

@EntityView(User.class)
public interface UserInterface {
    @IdMapping
    Integer getId();
    String getUserName();
}

The interface is just as compact as the record. Even if we introduce some validation for invariants:

@EntityView(User.class)
public interface UserInterface {
    @IdMapping
    Integer getId();
    String getUserName();

    @PostLoad
    default void validate() {
        Objects.requireNonNull(getId());
        Objects.requireNonNull(getUserName());
    }
}

Using interfaces to model entity views is definitely one of the best ways to work with because of the support for multiple inheritance and the very compact way of declaring entity view attributes. Thanks to the recently added annotation processor you can also make the implementations statically available so you can construct instances yourself.

Abstract classes need a little boilerplate to model public abstract methods because the defaults for methods are package-private and non-abstract. In interfaces you don’t need to write out the public abstract modifiers because that’s the default. But that’s actually a strength of abstract classes because they let you choose other visibility modifiers like protected, package-private or private for your method declarations.

@EntityView(User.class)
public abstract class UserAbstractClass {
    @IdMapping
    public abstract Integer getId();
    public abstract String getUserName();

    @PostLoad
    private void validate() {
        Objects.requireNonNull(getId());
        Objects.requireNonNull(getUserName());
    }
}

To avoid the clutter, you can also combine interfaces with abstract classes to get the best of both worlds and build rich abstractions.

public interface IdView<ID> {
    @IdMapping
    ID getId();
}
public interface UserNameView {
    String getUserName();
}
@EntityView(User.class)
public abstract class UserAbstractClass implements IdView<Integer>, UserNameView {
    @PostLoad
    private void validate() {
        Objects.requireNonNull(getId());
        Objects.requireNonNull(getUserName());
    }
}

Conclusion

We saw how custom classes like Java 14 records and also Lombok data classes can be used as entity views. Although the support is an interesting addition, we saw that using good old interfaces to model entity views is far superior and at the same time still compact.

The main use case for supporting custom classes is to ease the migration from a custom classes DTO approach to Blaze-Persistence Entity Views. I hope it stands up to the goal and makes it easier for people to migrate!

You can find the examples that were used in this blog post in the following repository: https://github.com/Blazebit/blaze-persistence-blog-examples/tree/master/entity-views-custom-classes

blaze-persistence jpa entity-view java14 records
comments powered by Disqus