Blazebit.com

Preface

JPA applications that use the entity model in every layer often suffer from the infamous LazyInitializationException or N + 1 queries issues. This is mainly due to the use of a too general model for a use case and is often solved by making use of a specialized DTO and adapting queries to that structure. The use of DTOs normally requires adapting many parts of an application and a lot of boilerplate code which is why people tend to do the wrong thing like making use of the open session in view anti-pattern. Apart from lazy loading issues, also the performance suffers due to selecting unnecessary data that a UI is never displaying.

Blaze Persistence entity views try to solve these and many more problems a developer faces when having to implement efficient model mapping in a JPA application. It allows to define DTOs as interfaces and provides the mappings to the JPA model via annotations. It favors convention-over-configuration by providing smart defaults that allow to omit most mappings. By applying DTOs to a query builder through the ObjectBuilder extension point it is possible to separate query logic from the projections while still enjoying high performance queries.

System requirements

Blaze Persistence entity views require at least Java 1.7 and at least a JPA 2.0 implementation. The entity view module depends on the core module and requires the use of the same versions for both modules.

1. Getting started

This is a step-by-step introduction about how to get started with the entity view module of Blaze Persistence.

The entity view module requires the Blaze Persistence core so if you have not read the getting started guide for the core yet, you might want to start your reading there.

1.1. Setup

As already described in the core module setup, every module depends on the core module. So if you haven’t setup the core module dependencies yet, get back here when you did.

To make use of the entity view module, you require all artifacts from the entity-view directory of the distribution. CDI and Spring users can find integrations in integration/entity-view that give a good foundation for configuring for these environments.

Spring Data users can find a special integration in integration/entity-view which is described in more detail in a later chapter. This integration depends on all artifacts of the jpa-criteria module.

1.1.1. Maven setup

We recommend you introduce a version property for Blaze Persistence which can be used for all artifacts.

<properties>
    <blaze-persistence.version>1.6.15-SNAPSHOT</blaze-persistence.version>
</properties>

The required dependencies for the entity view module are

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-entity-view-api</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-entity-view-impl</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

or if you are using Jakarta JPA

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-entity-view-api-jakarta</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-entity-view-impl-jakarta</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

Depending on the environment, there are some integrations that help you with configuration

Annotation processor

The annotation processor will generate static entity view metamodels, static entity view implementations and also static entity view builders.

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-entity-view-processor</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>provided</scope>
</dependency>

or if you are using Jakarta JPA

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-entity-view-processor-jakarta</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>provided</scope>
</dependency>
CDI integration
<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-entity-view-cdi</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

or if you are using Jakarta JPA

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-entity-view-cdi</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>
Spring integration
<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-entity-view-spring</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

or if you are using Jakarta APIs and Spring 6+

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-entity-view-spring-6.0</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>
Spring Data integration

When you work with Spring Data you can additionally have first class integration by using the following dependencies.

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-spring-data-2.7</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

For Spring-Data version 2.6, 2.5, 2.4, 2.3, 2.2, 2.1, 2.0 or 1.x use the artifact with the respective suffix 2.6, 2.5, 2.4, 2.3, 2.2, 2.1, 2.0, 1.x.

If you are using Jakarta APIs and Spring Framework 6+ / Spring Boot 3+, use this

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-spring-data-3.3</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>
The Spring Data integration depends on the jpa-criteria module
JPA Criteria
<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-jpa-criteria-api</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-jpa-criteria-impl</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

or if you are using Jakarta JPA

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-jpa-criteria-api-jakarta</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-jpa-criteria-impl-jakarta</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

If a JPA provider that does not implement the JPA 2.1 specification like Hibernate 4.2 or OpenJPA is used, the following compatibility dependency is also required.

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-jpa-criteria-jpa-2-compatibility</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>
Spring HATEOAS integration

When you work with Spring HATEOAS you can additionally have first class support for generating keyset pagination aware links by using the following dependency.

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-spring-hateoas-webmvc</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

or if you are using Jakarta APIs and Spring Framework 6+ / Spring Boot 3+ use

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-spring-hateoas-webmvc-jakarta</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

More information about the integration can be found in the Spring HATEOAS chapter.

1.2. Quarkus integration

To use the Quarkus extension you need to add the following Maven dependency to your Quarkus project:

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-quarkus</artifactId>
    <version>${blaze-persistence.version}</version>
</dependency>

1.3. Environments

The entity view module of Blaze Persistence is usable in Java EE, Spring as well as in Java SE environments.

1.3.1. Java SE

In a Java SE environment the EntityViewConfiguration as well as the EntityViewManager must be created manually as follows:

EntityViewConfiguration cfg = EntityViews.createDefaultConfiguration();
cfg.addEntityView(EntityView1.class);
// Add some more
cfg.addEntityView(EntityViewn.class);
EntityViewManager evm = cfg.createEntityViewManager(criteriaBuilderFactory);

As you can see, the EntityViewConfiguration is used to register all the entity view classes that you want to make accessible within the an EntityViewManager.

You may create multiple EntityViewManager instances with potentially different configurations.

1.3.2. Java EE

For usage with CDI the integration module blaze-persistence-integration-entity-view-cdi provides a CDI extension which takes over the task of creating and providing an EntityViewConfiguration from which an EntityViewManager can be created like following example shows.

@Singleton // from javax.ejb
@Startup   // from javax.ejb
public class EntityViewManagerProducer {

    // inject the configuration provided by the cdi integration
    @Inject
    private EntityViewConfiguration config;

    // inject the criteria builder factory which will be used along with the entity view manager
    @Inject
    private CriteriaBuilderFactory criteriaBuilderFactory;

    private EntityViewManager evm;

    @PostConstruct
    public void init() {
        // do some configuration
        evm = config.createEntityViewManager(criteriaBuilderFactory);
    }

    @PreDestroy
    public void closeEvm() {
        evm.close();
    }

    @Produces
    @ApplicationScoped
    public EntityViewManager createEntityViewManager() {
        return evm;
    }
}

The CDI extension collects all the entity views classes and provides a producer for the pre-configured EntityViewConfiguration.

When deploying a WAR file to an application server running on Java 11+ that doesn’t support MR-JARs, it will be necessary to use a special Java 9+ only artifact:

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-entity-view-impl</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
    <!-- Use the 9 classifier to get the Java 9+ only artifact -->
    <classifier>9</classifier>
</dependency>

1.3.3. CDI

If EJBs aren’t available, the EntityViewManager can also be configured in a CDI 1.1 specific way similar to the Java EE way.

@ApplicationScoped
public class EntityViewManagerProducer {

    // inject the configuration provided by the cdi integration
    @Inject
    private EntityViewConfiguration config;

    // inject the criteria builder factory which will be used along with the entity view manager
    @Inject
    private CriteriaBuilderFactory criteriaBuilderFactory;

    private volatile EntityViewManager evm;

    public void init(@Observes @Initialized(ApplicationScoped.class) Object init) {
        // no-op to force eager initialization
    }

    @PostConstruct
    public void init() {
        // do some configuration
        evm = config.createEntityViewManager(criteriaBuilderFactory);
    }

    @PreDestroy
    public void closeEvm() {
        evm.close();
    }

    @Produces
    @ApplicationScoped
    public EntityViewManager createEntityViewManager() {
        return evm;
    }
}

1.3.4. Spring

You have to enable the Spring entity-views integration via annotation based config or XML based config and you can also mix those two types of configuration:

Annotation Config

@Configuration
@EnableEntityViews("my.entityviews.base.package")
public class AppConfig {
}

XML Config

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:ev="http://persistence.blazebit.com/view/spring"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
        http://persistence.blazebit.com/view/spring http://persistence.blazebit.com/view/spring/spring-entity-views-1.2.xsd">

    <ev:entity-views base-package="my.entityviews.base.package"/>

</beans>

The Spring integration collects all the entity views classes in the specified base-package and provides the pre-configured EntityViewConfiguration for injection. This configuration is then used to create a EntityViewManager which should be provided as bean.

@Configuration
public class BlazePersistenceConfiguration {

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    @Lazy(false)
    // inject the criteria builder factory which will be used along with the entity view manager
    public EntityViewManager createEntityViewManager(CriteriaBuilderFactory cbf, EntityViewConfiguration entityViewConfiguration) {
        return entityViewConfiguration.createEntityViewManager(cbf);
    }
}

1.4. Supported Java runtimes

The entity view module like all other modules generally follows what has already been stated in the core moduel documentation.

Automatic module names for modules.

Module Automatic module name

Entity View API

com.blazebit.persistence.view

Entity View Impl

com.blazebit.persistence.view.impl

1.5. Supported environments/libraries

Generally, we support the usage in Java EE 6+ or Spring 4+ applications.

The following table outlines the supported library versions for the integrations.

Module Automatic module name Minimum version Supported versions

CDI integration

com.blazebit.persistence.integration.view.cdi

CDI 1.0

1.0 - 1.2, 2.0, 3.0

Spring integration

com.blazebit.persistence.integration.view.spring

Spring 4.3

4.3, 5.0 - 5.3, 6.0

DeltaSpike Data integration

com.blazebit.persistence.integration.deltaspike.data

DeltaSpike 1.7

1.7 - 1.9

Spring Data integration

com.blazebit.persistence.integration.spring.data

Spring Data 1.11

1.11, 2.0 - 2.7, 3.1

Spring Data Rest integration

com.blazebit.persistence.integration.spring.data.rest

Spring Data 1.11, Spring MVC 4.3

Spring Data 1.11 + Spring MVC 4.3, Spring Data 2.0 - 2.7 + Spring MVC 5.0 - 5.3, Spring Data 3.1 + Spring MVC 6.0

1.6. First entity view query

This section is supposed to give you a first feeling of how to use entity views. For more detailed information, please see the subsequent chapters.

In the following we suppose cbf, em and evm to refer to an instance of CriteriaBuilderFactory, JPA’s EntityManager and EntityViewManager, respectively. Take a look at the environments chapter for how to obtain an EntityViewManager.

An entity view can be thought of as the ORM world’s dual to a database table view. It enables the user to query just a subset of an entity’s fields. This enables developers to only query what they actually need for their current use case, thereby reducing network traffic and improving performance.

Let’s start with a very simple example. Assume that in our application we want to display a list of the names of all the cats in our database. Using entity views we would first define a new view for this purpose:

@EntityView(Cat.class)
public interface CatNameView {

    @IdMapping
    public Long getId();

    public String getName();

}

The usage of the CatNameView could look like this:

CriteriaBuilder<Cat> cb = cbf.create(em, Cat.class);
CriteriaBuilder<CatNameView> catNameBuilder = evm.applySetting(EntityViewSetting.create(CatNameView.class), cb);
List<CatNameView> catNameViews = catNameBuilder.getResultList();

Of course, you can apply further restrictions to your query by CriteriaBuilder means. E.g. you could avoid duplicate names in the above example by calling groupBy() on the CriteriaBuilder at any point after its creation.

By default the abstract getter methods in the view definition map to same named entity fields. So the getName() getter in the above example actually triggers querying of the name field. If we want to use a different name for the getter method we would have to add an additional @Mapping annotation:

@EntityView(Cat.class)
public interface CatNameView {

    @IdMapping
    public Long getId();

    @Mapping("name")
    public String getCatName();

}

Of course, it is also possible to combine various views via inheritance.

@EntityView(Cat.class)
public interface CatKittens {

    @IdMapping
    public Long getId();

    public List<Kitten> getKittens();

}

@EntityView(Cat.class)
public interface CatNameView {

    @IdMapping
    public Long getId();

    @Mapping("name")
    public String getCatName();

}

@EntityView(Cat.class)
public interface CombinedView extends CatKittens, CatNameView {

    @Mapping("SIZE(kittens)")
    public Integer getKittenSize();

}
An entity view does not have to be an interface, it can be any class.

Moreover you can see that it is possible to use whole expressions inside the @Mapping annotations. The allowed expression will be covered in more detail in subsequent chapters.

Another useful feature are subviews which is illustrated in following example.

@EntityView(Landlord.class)
public interface LandlordView {

    @IdMapping
    public Long getId();

    public String getName();

    public Integer getAge();

    @Mapping("ownedProperties")
    public PropertyAddressView getHouses();

}

@EntityView(Property.class)
public interface PropertyAddressView {

    @IdMapping
    public Long getId();

    public String getAddress();

}

The last feature we are going to cover here are filters and sorters in conjunction with EntityViewSetting which allows the dynamic configuration of filters and sorters on your entity view and are also usable together with pagination. This makes them an ideal fit whenever you need to query data for display in a filterable and/or sortable data table. Following example illustrates how this looks like:

@EntityView(Cat.class)
@ViewFilters({
    @ViewFilter(name = "customFilter", value = FilteredDocument.CustomFilter.class)
})
public interface FilteredCatView {

    @AttributeFilter(ContainsFilter.class)
    public String getName();

    public static class CustomFilter extends ViewFilterProvider {
        @Override
        public <T extends WhereBuilder<T>> T apply(T whereBuilder) {
            return whereBuilder.where("doctor.name").like().expression("Julia%").noEscape();
        }
    }
}

In this example we once again define a view on our Cat entity and select the cat’s name only. But in addition we applied a filter on the name attribute. In this case we chose the ContainsFilter, one of the predefined filters. We also defined a custom filter where we check whether the cat’s doctor’s name starts with the string Julia. The next code snippet shows how we dynamically set the actual filter value by which the query should filter and how we paginate the resulting query.

// Base setting
EntityViewSetting<FilteredCatView, PaginatedCriteriaBuilder<FilteredCatView>> setting =
        EntityViewSetting.create(FilteredCatView.class, 0, 10);

// Query
CriteriaBuilder<Cat> cb = cbf.create(em, Cat.class);
setting.addAttributeFilter("name", "Kitty");

PaginatedCriteriaBuilder<FilteredCatView> paginatedCb = evm.applySetting(setting, cb);
PagedList<FilteredCatView> result = paginatedCb.getResultList();

1.7. Summary

If you want to go into more detail, you are now ready to discover the other chapters of the documentation or the API yourself.

2. Architecture

This is just a high level view for those that are interested about how Blaze Persistence entity view works.

2.1. Interfaces

A quick overview that presents the interfaces that are essential for users and how they are related.

Since entity views are mostly annotation driven and are about mapping attributes to entity attributes, there are not that many interfaces. The two most important ones are the EntityViewManager and the EntityViewSetting.

A EntityViewManager is built once on startup during which it analyzes and validates the configured entity views. It is responsible for building implementations for the interfaces and abstract classes from the metamodel and caching object builder instances for entity views.

The EntityViewSetting is a configuration that can be applied on a query builder through an EntityViewManager and contains information about

  • The entity view

  • Pagination

  • Filters and sorters

  • Parameters and properties

2.2. Core module integration

The entity view module builds on top of the ObjectBuilder integration point offered by query builders of the core module. Every entity view is translated into a ObjectBuilder which is then applied on a query builder.

2.3. Object builder pipeline

During startup the metamodel is built which is then used for building an object builder pipeline. For every entity view interface/class a ObjectBuilder template called ViewTypeObjectBuilderTemplate is created which is cached. From these templates a normal ObjectBuilder is built that can be applied on any query builder. Depending on the features a entity view uses, the resulting object builder might actually be a object builder pipeline i.e. it invokes multiple object builders in an ordered manner on tuples.

In general, a object builder for an entity view just takes in the tuple and passes it to the constructor of the entity view implementation. As soon as subviews or collections are involved, it becomes a pipeline. The pipeline has two different forms, the abstract form represented by TupleTransformatorFactory and the concrete form TupleTransformator. When a object builder is created from a template, the concrete form is created from the abstract one which might involve building object builders for subviews. Every collection introduces a new transformation level i.e. elements of a collection must be materialized before the collection can be materialized.

So the result is processed from the leafs(i.e. the elements) upwards(i.e. a collection) until objects of the target entity view type are materialized.

2.4. Updatable entity views

Updatable entity views are still in flux and are not yet fully thought through, but here comes the essential idea.

Similar to the object builder pipeline, a EntityViewUpdater is composed of several possibly nested attribute flushers. A EntityViewUpdater is built once and is responsible for flushing dirty attributes to the persistence context. After flushing, attributes are considered to be non-dirty but they can become dirty again either through a change or a transaction rollback.

Dirty tracking is done either by remembering the initial state and comparing with changed state or not at all. Collections are tracked by using custom collection implementations that do action recording which is then replayed onto a collection of an entity reference.

3. Mappings

As already mentioned in the Getting started section, the entity view module builds up on the core module. Some of the basics like implicit joins and the basic expression structure should be known to understand all of the following mapping examples.

Entity views are to entities in ORM, what table views are to tables in an RDBMS. They represent projections on the entity model. In a sense you can say that entity views are DTOs 2.0 or DTOs done right.

One of the unique features of entity views is that it only imposes a structure and the projections, but the base query defines the data source. Blaze Persistence tried to reduce as much of the boilerplate as possible for defining the structure and the projections by employing a convention over configuration approach.

The result of these efforts is that entity views are defined as interfaces or abstract classes mostly containing just getter definitions that serve as attribute definitions. To declare that an interface or an abstract class as entity view, you have to annotate it and specify the entity class for which this entity view provides projections.

@EntityView(Cat.class)
interface CatView { ... }

So an entity view can be seen as a named wrapper for a bunch of attributes, where every attribute has some kind of mapping that is based on the attributes the entity type offers. An attribute is declared by defining a public abstract method in an entity view i.e. every abstract method is considered to be an attribute.

@EntityView(Cat.class)
interface CatView {
    String getName();
}

Since every method of an interface is abstract and public, you can omit the abstract and public keywords. In this simple example you can see that the CatView has an attribute named name. The implicit mapping for the attribute is the attribute name itself, so name. This means that the entity view attribute name declared by the abstract method getName() is mapped to the entity attribute name.

Since entity views and their mappings are validated during startup against the entity model, you should see any mapping related runtime errors and can be sure it works if it doesn’t fail to start

One of the nice things about using interfaces is that you can have multiple inheritance. If you separate concerns in separate feature interfaces, you can effectively make use of multiple inheritance.

interface NameView {
    String getName();
}

interface AgeView {
    Long getAge();
}

@EntityView(Cat.class)
interface CatView extends NameView, AgeView {
}

In this example CatView has two attributes, name and age. Even though the interfaces are not entity views, they could have custom mappings.

3.1. Mapping types

So far, you have mostly seen basic attribute mappings in entity views, but there is actually support for far many mapping types.

  • Basic mappings - maps basic attributes from entities into entity views

  • Subview mappings - maps a *ToOne relation of an entity to an entity view

  • Flat view mappings - maps an embeddable or association of an entity to a flat entity view

  • Subquery mappings - maps the result of a subquery to a basic attribute into entity views

  • Parameter mappings - maps named query parameters into an entity view

  • Entity mappings - maps *ToOne or *ToMany relations of an entity as is into an entity view

  • Collection mappings - maps *ToMany relations of an entity into an entity view with support for basic, subview and embeddable types

  • Correlated mappings - correlates an entity type by some key and maps it or an attribute of it into an entity view as subview or basic type respectively

In general we do not recommend to make extensive use of entity mappings as it defeats the purpose of entity views and can lead to lazy loading issues

Apart from mapping attributes, it is also possible have a constructor and map parameters when using an abstract class. One of the biggest use cases for this is for doing further transformations on the data that can’t be pushed to the DBMS like e.g. money formatting.

3.2. Mapping defaults

As mentioned before, the entity view module implements a convention over configuration approach and thus has some smart defaults for mappings. Whenever an attribute(getter method) without a mapping annotation is encountered, a default mapping to the same named entity attribute will be created. If there is none, it will obviously report an error.

3.3. Id mappings

Id mappings declare that an attribute represents the identifier i.e. can be used to uniquely identify an entity view object. The id mapping is declared by annotating the desired attribute with @IdMapping and optionally specifying the mapping path. Having an id attribute allows an entity view to map collections, be mapped in collections and gives an entity view object a meaningful identity. If an entity view has no id mapping, it is considered to be a flat view which probably only makes sense for scalar results or embedded objects.

It is generally recommended to always declare an id mapping if possible.

When an id mapping is present, the generated entity view implementation’s equals-hashCode implementation will be based on it. Otherwise it will consider all attributes in the equals-hashCode implementation.

@EntityView(Cat.class)
interface CatView {
    @IdMapping
    Long getId();
}

3.4. Flat view id mappings

A flat view id mapping is given when the type of the id attribute is a flat view type. This is the case when the view type has no id declared. It’s very similar to subview mappings and is mostly used when working with JPA embeddable types. Imagine the following model for illustration purposes.

@EntityView(Cat.class)
interface CatIdView {
    String getName();
}

@EntityView(Cat.class)
interface CatView {
    @IdMapping("this")
    CatIdView getId();
}

This example already makes use of many concepts. It declares the CatIdView as flat view with a basic mapping and the CatView with a flat view id. The mapping for the flat view id in CatView uses to the this expression extension to allow the flat view to be based on the same entity that is backing the CatView.

Since flat view types will consider all attributes in the equals-hashCode implementation, the type shouldn’t contain unnecessary attributes if possible.

3.5. Basic mappings

A basic mapping is declared by annotating the desired attribute with @Mapping and specifying the mapping expression. An attribute that has no mapping annotations is only considered to have a basic mapping if it is of a basic type like e.g. Integer. Without a mapping annotation, the default mapping rules apply. In general, every non-collection and non-managed type is considered to be basic. Managed types are JPA managed types and entity view types.

Although most example only use path expressions for the mapping, it is actually allowed to use any scalar expression that JPQL or Blaze Persistence allows.

@EntityView(Cat.class)
interface CatView {
    @IdMapping
    Long getId();

    @Mapping("UPPER(name)")
    String getUpperName();
}

As you might expect, the expression UPPER(name) will upper-case the name, so getUpperName() will return the upper-cased name. Applying such an entity view on a simple query builder will show what happens behind the scenes.

List<CatView> result = evm.applySetting(
        EntityViewSetting.create(CatView.class),
        cbf.create(em, Cat.class)
    ).getResultList();
SELECT cat.id, UPPER(cat.name)
FROM Cat cat

The expression in the mapping ends up as select item in the query just as expected.

3.6. Subview mappings

Subview and embeddable view mappings are similar to basic mappings in the sense that the same rules apply, except for the allowed mappings. Since these mappings get their data from objects of managed types, only path expressions are allowed for their mappings. Path expressions can have arbitrary depth i.e. multiple de-references like relation.subRelation.otherRelation and path elements can be of the following types:

  • Simple path elements that refer to entity type attributes

  • TREAT expression like TREAT(..).subRelation

  • Qualified expression like KEY(..).subRelation

  • Array expression like relation[:param].subRelation

A subview mapping is given when the type of the attribute is a entity view type. Since a entity view is always declared for a specific entity type, the target type of the subview mapping and the entity view’s entity type must be compatible. This means that you could apply a AnimalView to a Cat if it extends Animal but can’t apply a PersonView since it’s not compatible i.e. Cat is not a subtype of Person.

@EntityView(Person.class)
interface PersonView {
    @IdMapping
    Long getId();

    String getName();
}

@EntityView(Animal.class)
interface AnimalView {
    @IdMapping
    Long getId();

    String getName();
}

@EntityView(Cat.class)
interface CatView {
    @IdMapping
    Long getId();

    AnimalView getFather();
}

As you might imagine, the CatView will additionally select attributes of the father relation since they are requested by the AnimalView. In order to understand the following generated query, you should know what an implicit join does and how entity views make use of such implicit joins.

Behind the scenes, the entity views runtime will apply a select on the criteria builder for the expressions cat.id, father.id and father.name. The expression father.name accesses an entity attribute is only accessible when actually joining the relation. This is why an implicit/default join is generated for the father relation.

SELECT cat.id, father_1.id, father_1.name
FROM Cat cat
LEFT JOIN cat.father father_1

Since the father relation is optional or nullable, a (default) left join is created due to the rules of model awareness in implicit joins. This is a perfect fit for entity views as the subview object will be simply null if a cat has no father. If the implicit join worked like JPQL defines it, an inner join would have to be created. An inner join would mean that cats without a father would get filtered out which is an undesirable effect since we only want a projection on top of a base query.

Subviews can in turn have subviews again, so there is no limitation regarding the depth. The only requirement is that there is no cycle.

3.6.1. Flat view mappings

A flat view mapping is given when the type of the attribute is a flat view type. This is the case when the entity view has no id declared. It’s very similar to subview mappings and is mostly used when working with JPA embeddable types.

Note that a flat view can be used like a normal view except when

  • it is used as view root i.e. the flat view is the entity view type used in EntityViewSetting,

  • it is embedded in a flat view which in turn is the view root i.e. the parent is a flat view that is used in EntityViewSetting

  • or it is used as subview for a non-indexed collection

then the flat view can’t have collection attributes with fetch strategy JOIN. The reason is that the elements of the collection can’t be matched with the flat view as it has no identity it can use for matching.

Imagine the following model for illustration purposes.

@Embeddable
class Name {
    String firstName;
    String lastName;
}

@Entity
class Person {
    @Id
    @GeneratedValue
    Long id;
    @Embedded
    Name name;
}

@EntityView(Name.class)
interface SimpleNameView {
    String getFirstName();
}

@EntityView(Person.class)
interface PersonView {
    @IdMapping
    Long getId();

    SimpleNameView getName();
}

Applying a PersonView would produce a query like

SELECT person.id, person.name.firstName
FROM Person person

Such a flat view can also be used with the this expression which is similar to JPAs @Embedded.

A limitation in Hibernate actually requires the use of flat entity views for mapping of element collections i.e. you can map the element collection 1:1 to the entity view.

Flat views for singular attributes are by default always created, even if all attributes of a flat view are null i.e. the flat view is empty. This can be overridden by annotating the attribute with @EmptyFlatViewCreation(false) or globally by specifying the configuration option CREATE_EMPTY_FLAT_VIEWS. When empty flat view creation is disabled, the attribute value will be set to null instead of an empty flat view.

3.7. Subquery mappings

Subquery mappings allow to map scalar subqueries into entity views and are declared by annotating the desired attribute with @MappingSubquery and specifying a SubqueryProvider. The following example should illustrate the usage:

@EntityView(Cat.class)
interface CatView {
    @IdMapping
    Long getId();

    @MappingSubquery(KittenCountSubqueryProvider.class)
    Long getKittenCount();

    class KittenCountSubqueryProvider implements SubqueryProvider {

        @Override
        public <T> T createSubquery(SubqueryInitiator<T> subqueryBuilder) {
            return subqueryBuilder.from(Cat.class, "subCat")
                    .select("COUNT(*)")
                    .whereOr()
                        .where("subCat.father.id").eqExpression("EMBEDDING_VIEW(id)")
                        .where("subCat.mother.id").eqExpression("EMBEDDING_VIEW(id)")
                    .endOr()
                .end();
        }
    }
}

This entity view already comes into contact with the core API for creating subqueries. It produces just what it defines, a subquery in the select clause.

SELECT
    cat.id,
    (
        SELECT COUNT(*)
        FROM Cat subCat
        WHERE subCat.father.id = cat.id
           OR subCat.mother.id = cat.id
    )
FROM Cat cat

In the subquery provider before you saw the usage of EMBEDDING_VIEW which is gone in the final query. This is because EMBEDDING_VIEW is a way to refer to attributes of the relation of the entity view into which the subquery is embedded without having to refer to the concrete the query alias. For more information on this check out the documentation of the EMBEDDING_VIEW function

The subquery was just used for illustration purposes and could be replaced with a basic mapping SIZE(kittens) which would also generate a more efficient query.

3.8. Parameter mappings

A parameter mapping is a convenient way to inject the values of query parameters or optional parameters into instances of an entity view. Introducing a parameter mapping with @MappingParameter will introduce a fake select item. If a parameter is not used in a query, NULL will be injected into the entity view.

@EntityView(Cat.class)
interface CatView {
    @IdMapping
    Long getId();

    @MappingParameter("myParam")
    String getMyParam();
}
SELECT cat.id, NULLIF(1,1)
FROM Cat cat

Parameter mappings are probably most useful in constructor mappings where they can be used for some transformation logic.

Optional parameters can be configured globally through setOptionalParameter() or for a specific use case through addOptionalParameter().

3.9. Entity mappings

Apart from having custom projections for entity or embeddable types through subviews, you can also map the JPA managed types directly. You can use the @Mapping annotation if desired and map any path expression as singular or plural attribute(i.e. collection) with managed types.

@EntityView(Cat.class)
interface CatView {
    @IdMapping
    Long getId();

    Cat getFather();
}
SELECT cat.id, father_1
FROM Cat cat
LEFT JOIN cat.father father_1

Beware that when using managed types directly, you might run into lazy loading issues when accessing uninitialized/un-fetched properties of the entity.

You can however specify what properties should be fetched for such entity mappings by using the fetches configuration.

@EntityView(Cat.class)
interface CatView {
    @IdMapping
    Long getId();

    @Mapping(fetches = "kittens")
    Cat getFather();
}

This will fetch the kittens of the father.

SELECT cat.id, father_1
FROM Cat cat
LEFT JOIN cat.father father_1
LEFT JOIN FETCH father_1.kittens kittens_1

3.10. Collection mappings

One of the most important features of the Blaze Persistence entity view module is the possibility to map collections. You can map collections defined in the entity model to collections in the entity view model in multiple ways.

3.10.1. Simple 1:1 collection mapping

The simplest possible mapping is a 1:1 mapping of e.g. a *ToMany collection.

@EntityView(Cat.class)
interface CatView {
    @IdMapping
    Long getId();

    Set<Cat> getKittens();
}

This will simply join the kittens collection. During entity view construction the elements are collected and the result is flattened as expected.

SELECT cat.id, kittens_1
FROM Cat cat
LEFT JOIN cat.kittens kittens_1

3.10.2. Subset basic collection mapping

Most of the time, only a subset of the properties of a relation is needed. In case only a single property is required, the use of @Mapping to refer to the property within a collection can be used.

@EntityView(Cat.class)
interface CatView {
    @IdMapping
    Long getId();

    @Mapping("kittens.name")
    Set<String> getKittenNames();
}

This will join the kittens collection and only select their name.

SELECT cat.id, kittens_1.name
FROM Cat cat
LEFT JOIN cat.kittens kittens_1

3.10.3. Subview collection mapping

For the cases when multiple properties of a relation are needed, you can also use subviews.

@EntityView(Cat.class)
interface SimpleCatView {
    @IdMapping
    Long getId();

    String getName();
}

@EntityView(Cat.class)
interface CatView extends SimpleCatView {

    Set<SimpleCatView> getKittens();
}

Applying the CatView entity view will again join the kittens collection but this time select some more properties.

SELECT cat.id, kittens_1.id, kittens_1.name
FROM Cat cat
LEFT JOIN cat.kittens kittens_1

A subview within a collection can have subviews and collections of subviews again i.e. there is no limit to nesting.

3.10.4. Collection type re-mapping

Another nice feature of Blaze Persistence entity views is the ability to re-map a collection to a different collection type. In the entity model one might for example choose to always use a java.util.Set for mapping collections, but to be able to make use of the elements in a UI, you might require e.g. a java.util.List.

Although the kittens relation in the Cat entity uses a Set, you can map the kittens as List in the CatView. As you might expect, the order of the elements will then depend on the order of the query result.

@EntityView(Cat.class)
interface SimpleCatView {
    @IdMapping
    Long getId();

    String getName();
}

@EntityView(Cat.class)
interface CatView extends SimpleCatView {

    List<SimpleCatView> getKittens();
}

By executing the query with a custom ORDER BY clause, the result order can be made deterministic.

List<CatView> result = entityViewManager.applySetting(
        EntityViewSetting.create(CatView.class),
        cb.create(Cat.class)
            .orderByAsc("name")
            .orderByAsc("kittens.name")
    ).getResultList();
SELECT cat.id, kittens_1.id, kittens_1.name
FROM Cat cat
LEFT JOIN cat.kittens kittens_1
ORDER BY cat.name       ASC NULLS LAST,
         kittens_1.name ASC NULLS LAST
We do not recommend to rely on this behavior but instead make use of sorted collection mappings.

3.10.5. Ordered collection mapping

Apart from changing the collection type to e.g. List it is also possible to get ordered results with sets. By specifying ordered = true for the collection via the annotation @CollectionMapping you can force a set implementation that retains the insertion order like a LinkedHashSet.

@EntityView(Cat.class)
interface SimpleCatView {
    @IdMapping
    Long getId();

    String getName();
}

@EntityView(Cat.class)
interface CatView extends SimpleCatView {

    @CollectionMapping(ordered = true)
    Set<SimpleCatView> getKittens();
}

The query doesn’t change, the only thing that does, is the implementation for the collection.

SELECT cat.id, kittens_1.id, kittens_1.name
FROM Cat cat
LEFT JOIN cat.kittens kittens_1

This oviously only makes sense when used along with an ORDER BY clause that orders the result set deterministically.

3.10.6. Sorted collection mapping

In addition to ordering, the following sorted collection types are supported

  • SortedSet and NavigableSet

  • SortedMap and NavigableMap

You can specify the comparator for the collection via the annotation @CollectionMapping

@EntityView(Cat.class)
interface SimpleCatView {
    @IdMapping
    Long getId();

    String getName();

    static class DefaultComparator implements Comparator<SimpleCatView> {

        @Override
        public int compare(SimpleCatView o1, SimpleCatView o2) {
            return String.CASE_INSENSITIVE_ORDER.compare(o1.getName(), o2.getName());
        }
    }
}

@EntityView(Cat.class)
interface CatView extends SimpleCatView {

    @CollectionMapping(comparator = SimpleCatView.DefaultComparator.class)
    SortedSet<SimpleCatView> getKittens();
}

This will ensure the correct ordering of the collection elements regardless of the query ordering. The query stays the same.

SELECT cat.id, kittens_1.id, kittens_1.name
FROM Cat cat
LEFT JOIN cat.kittens kittens_1

3.10.7. Indexed collection re-mapping

Mapping an indexed collection like a java.util.Map or java.util.List with an @OrderColumn can happen in multiple ways. Let’s consider the following model.

@Entity
class Cat {
    @Id
    Long id;

    @OneToMany
    @OrderColumn
    List<Cat> indexedKittens;

    @ManyToMany
    Map<Cat, Cat> kittensBestFriends;
}

@EntityView(Cat.class)
interface SimpleCatView {
    @IdMapping
    Long getId();

    String getName();
}
Indexed mapping

One way is to map the collections indexed again, i.e. a Map in the entity is mapped as Map in the entity view.

@EntityView(Cat.class)
interface CatView extends SimpleCatView {

    List<SimpleCatView> getIndexedKittens();

    Map<SimpleCatView, SimpleCatView> getKittensBestFriends(); (1)
}
1 Careful when mapping the key to a subview. This is only supported in the latest JPA provider versions
SELECT
    cat.id,
    cat.name,

    INDEX(indexedKittens_1),
    indexedKittens_1.id,
    indexedKittens_1.name

    KEY(kittensBestFriends_1).id,
    KEY(kittensBestFriends_1).name,

    kittensBestFriends_1.id,
    kittensBestFriends_1.name
FROM Cat cat
LEFT JOIN cat.indexedKittens indexedKittens_1
LEFT JOIN cat.kittensBestFriends kittensBestFriends_1
Map-Key only mapping

By using the qualified expression KEY() you can map the keys of a map to a collection by using @Mapping

@EntityView(Cat.class)
interface CatView extends SimpleCatView {

    @Mapping("KEY(kittensBestFriends)")
    List<SimpleCatView> getKittens();
}
SELECT cat.id, cat.name, KEY(kittensBestFriends_1).id, KEY(kittensBestFriends_1).name
FROM Cat cat
LEFT JOIN cat.kittensBestFriends kittensBestFriends_1
Map-Value only mapping

Simply mapping a path expression for a Map to a normal collection, will result in only fetching the map values.

@EntityView(Cat.class)
interface CatView extends SimpleCatView {

    @Mapping("kittensBestFriends")
    List<SimpleCatView> getBestFriends();
}
SELECT cat.id, cat.name, kittensBestFriends_1.id, kittensBestFriends_1.name
FROM Cat cat
LEFT JOIN cat.kittensBestFriends kittensBestFriends_1
List-Value only mapping

Sometimes it might be required to ignore the index of an indexed List when mapping it to a List again. To do so use ignoreIndex on @CollectionMapping

@EntityView(Cat.class)
interface CatView extends SimpleCatView {

    @Mapping("indexedKittens")
    @CollectionMapping(ignoreIndex = true)
    List<SimpleCatView> getKittens();
}
SELECT cat.id, cat.name, indexedKittens_1.id, indexedKittens_1.name
FROM Cat cat
LEFT JOIN cat.indexedKittens indexedKittens_1

3.10.8. Custom indexed collection mapping

Mapping an indexed collection like a java.util.Map or java.util.List in entity views does not necessarily require that the source collection must be of the same type. A custom index mapping can be specified by annotating the attribute with @MappingIndex. The index mapping is relative to the target mapping. Let’s consider the following model.

@Entity
class Cat {
    @Id
    Long id;

    int age;

    @OneToMany
    Set<Cat> kittens;
}

@EntityView(Cat.class)
interface SimpleCatView {
    @IdMapping
    Long getId();

    String getName();
}
Indexed-List mapping

An indexed List can be mapped by specifying a @MappingIndex that resolves to a 0-based integer of the target mapping.

@EntityView(Cat.class)
interface CatView extends SimpleCatView {

    @MappingIndex("age")
    @Mapping("kittens")
    List<SimpleCatView> getKittensByAge();
}
SELECT
    cat.id,
    cat.name,

    kittens_1.age,
    kittens_1.id,
    kittens_1.name
FROM Cat cat
LEFT JOIN cat.kittens kittens_1

Note that depending on the age values, there can be many null entries in the list. An indexed List is filled up with null entries for missing indexes.

Map mapping

An Map indexed by some value can be mapped by specifying a @MappingIndex relative to the target mapping.

@EntityView(Cat.class)
interface CatView extends SimpleCatView {

    @MappingIndex("age")
    @Mapping("kittens")
    Map<Integer, SimpleCatView> getKittensByAge();
}
SELECT
    cat.id,
    cat.name,

    kittens_1.age,
    kittens_1.id,
    kittens_1.name
FROM Cat cat
LEFT JOIN cat.kittens kittens_1

Since the age might not be unique for the kittens in a collection, some cats could be overwritten, which is prevented by throwing an exception. To avoid the exception and instead collect all kittens grouped by the index value, multi-collections can be used.

Multi-collection mapping

An indexed List or Map can specify a collection value to collect all values grouped by their index value. Valid types for the collections are Collection, Set, SortedSet and List.

@EntityView(Cat.class)
interface CatView extends SimpleCatView {

    @MappingIndex("age")
    @Mapping("kittens")
    Map<Integer, Set<SimpleCatView>> getKittensByAge();
}
SELECT
    cat.id,
    cat.name,

    kittens_1.age,
    kittens_1.id,
    kittens_1.name
FROM Cat cat
LEFT JOIN cat.kittens kittens_1
Subview key mapping

An indexed Map can specify a subview as key as well. Note how the this mapping is used for the index mapping which allows the key view to be based on the target mapping kittens.

@EntityView(Cat.class)
interface CatAgeView {
    int getAge();
}

@EntityView(Cat.class)
interface CatView extends SimpleCatView {

    @MappingIndex("this")
    @Mapping("kittens")
    Map<CatAgeView, Set<SimpleCatView>> getKittensByAge();
}
SELECT
    cat.id,
    cat.name,

    kittens_1.age,
    kittens_1.id,
    kittens_1.name
FROM Cat cat
LEFT JOIN cat.kittens kittens_1
Ordered element collection mapping

By specifying ordered = true for the element collection via the annotation @MultiCollectionMapping you can force a set implementation that retains the insertion order like a LinkedHashSet.

@EntityView(Cat.class)
interface CatAgeView {
    int getAge();
}

@EntityView(Cat.class)
interface CatView extends SimpleCatView {

    @MappingIndex("this")
    @Mapping("kittens")
    @MultiCollectionMapping(ordered = true)
    Map<CatAgeView, Set<SimpleCatView>> getKittensByAge();
}

The query doesn’t change, the only thing that does, is the implementation for the collection.

SELECT
    cat.id,
    cat.name,

    kittens_1.age,
    kittens_1.id,
    kittens_1.name
FROM Cat cat
LEFT JOIN cat.kittens kittens_1

This oviously only makes sense when used along with an ORDER BY clause that orders the result set deterministically.

Sorted element collection mapping

You can specify the comparator for the collection via the annotation @MultiCollectionMapping

@EntityView(Cat.class)
interface CatAgeView {
    int getAge();
}

@EntityView(Cat.class)
interface CatView extends SimpleCatView {

    @MappingIndex("this")
    @Mapping("kittens")
    @MultiCollectionMapping(comparator = SimpleCatView.DefaultComparator.class)
    Map<CatAgeView, Set<SimpleCatView>> getKittensByAge();
}

This will ensure the correct ordering of the element collection elements regardless of the query ordering. The query stays the same.

SELECT
    cat.id,
    cat.name,

    kittens_1.age,
    kittens_1.id,
    kittens_1.name
FROM Cat cat
LEFT JOIN cat.kittens kittens_1

3.11. Singular collection type mappings

There are cases when the entity model defines a collection that is actually a singular entity attribute. This can happen when you use custom type implementations or JPA 2.1 attribute converters that produce collections. A custom type or converter could map a DBMS array, json, xml or any other type to a collection. Since such an entity attribute is not a relation, it can only be a singular attribute. By default Blaze Persistence entity views assume that an entity view attribute with a collection type is a plural attribute and the mapping refers to a plural entity attribute. In order to be able to map such special singular attribute collections, you have to specifically use @MappingSingular.

@Entity
class Cat {

    @Id
    Long id;

    @Basic
    @Convert(converter = StringSetConverter.class)
    Set<String> tags;
}

class StringSetConverter implements AttributeConverter<String, Set<String>> { ... }

@EntityView(Cat.class)
interface CatView {

    @IdMapping
    Long getId();

    @MappingSingular
    Set<String> getTags();
}

Beware that you can’t re-map the collection type in this case although this might soon be possible.

The query will not generate a join but simply select the tags since it’s a singular attribute.

SELECT cat.id, cat.tags
FROM Cat cat

3.12. Limit mapping

Oftentimes it is not necessary to fetch all elements of a collection or correlation but only the top N values. To achieve that, the @Limit annotation can be used like in the following example:

@EntityView(Cat.class)
interface CatView extends SimpleCatView {

    @Mapping("kittens")
    @Limit(limit = "5", order = {"age DESC", "id DESC"})
    List<SimpleCatView> getKittens();
}

This will fetch only the 5 oldest kittens per cat, regardless of the used fetch strategy. A possible query for this view could look like this:

SELECT cat.id, cat.name, kittens.id, kittens.name
FROM Cat cat
LEFT JOIN LATERAL Cat(
    SELECT kitten.age, kitten.father.id, kitten.id, kitten.mother.id, kitten.name
    FROM Cat kitten
    WHERE kitten MEMBER OF cat.kittens
    ORDER BY kitten.age DESC, kitten.id DESC
    LIMIT 5
) kittens(age, father.id, id, mother.id, name) ON 1=1

3.13. Correlated mappings

In some entity models, not every relation between entities might be explicitly mapped. There are multiple possible reasons for that like e.g. not wanting to have explicit dependencies, to keep it simple etc. Apart from unmapped relations, there is sometimes the need to correlate entities based on some criteria with other entities which are more of an ad-hoc nature than explicit relations.

For these cases Blaze Persistence entity views introduces the concept of correlated mappings. These mappings can be used to connect entities through a custom criteria instead of through mapped entity relations. Correlated mappings can be used for any attribute type(basic, entity, subview, collection) although singular basic attributes can also be implemented as normal subqueries.

A correlation mapping is declared by annotating the desired attribute with @MappingCorrelated or @MappingCorrelatedSimple.

3.13.1. General correlated mappings

In order to map the correlation you need to specify some values

  • correlationBasis - An expression that maps to the so called correlation key

  • correlator - The CorrelationProvider to use for the correlation that introduces a so called correlated entity

By default, the correlated entity type is projected into the view. To map a specific property of the entity type, use the correlationResult attribute. There is also the possibility to specify a fetch strategy that should be used for the correlation. By default, the SELECT strategy is used.

@EntityView(Cat.class)
public interface CatView {

    @IdMapping
    Long getId();

    @MappingCorrelated(
        correlationBasis = "age",
        correlator = PersonAgeCorrelationProvider.class,
        fetch = FetchStrategy.JOIN
    )
    Set<Person> getSameAgedPersons();

    static class PersonAgeCorrelationProvider implements CorrelationProvider {

        @Override
        public void applyCorrelation(CorrelationBuilder builder, String correlationExpression) {
            final String alias = builder.getCorrelationAlias();  (1)
            builder.correlate(Person.class)
                .on(alias + ".age").inExpressions(correlationExpression) (2)
            .end();
        }

    }
}
1 getCorrelationAlias() defines the alias for the correlated entity
2 correlationExpression represents the correlationBasis. We generally recommend to use the IN predicate through inExpressions() to be able to easily switch the fetch strategy

Depending on the fetch strategy multiple other queries might be executed. Check out the different fetch strategies for further information. In this case, the JOIN strategy was used, so the following query is generated.

SELECT cat.id, pers
FROM Cat cat
LEFT JOIN Person correlated_SameAgedPersons (1)
       ON cat.age = correlated_SameAgedPersons.age (2)
1 This makes use of the so called entity join feature which is only available in newer JPA provider versions
2 Note that the IN predicate which was used in the correlation provider was rewritten to a equality predicate

Since entity joins are required for using the JOIN fetch strategy with correlation mappings you have to make sure your JPA provider supports them. If your JPA provider does not support entity joins, you have to use a different fetch strategy instead.

Entity joins are only supported in newer versions of JPA providers(Hibernate 5.1+, EclipseLink 2.4+, DataNucleus 5+)

3.13.2. Simple correlated mappings

Since correlation providers are mostly static, Blaze Persistence also offers a way to define simple correlations in a declarative manner. The @MappingCorrelatedSimple annotation only requires a few values

  • correlationBasis - An expression that maps to the so called correlation key

  • correlated - The correlated entity type

  • correlationExpression - The expression to use for correlating the correlated entity type to the view

@EntityView(Person.class)
public interface PersonView {

    @IdMapping
    Long getId();

    String getName();
}

@EntityView(Cat.class)
public interface CatView {

    @IdMapping
    Long getId();

    @MappingCorrelatedSimple(
        correlationBasis = "age",
        correlated = Person.class,
        correlationExpression = "age IN correlationKey" (1)
        fetch = FetchStrategy.JOIN
    )
    Set<PersonView> getSameAgedPersons(); (2)
}
1 The expression uses the default name for the correlation key but could use a different name by specifying the attribute correlationKeyAlias
2 As you see here, it is obviously also possible to map subviews for correlated entity types

Just like the general correlation, by default, the correlated entity type is projected into the view. To map a specific property of the entity type, use the correlationResult attribute. There is also the possibility to specify a fetch strategy that should be used for the correlation. By default, the SELECT strategy is used.

3.14. Correlation mappings via entity array syntax

The easiest way to correlate an entity is by using the entity array syntax EntityName[predicate] which is explained in detail in the core documentation.

Such a mapping can be used anywhere with all fetch strategies. Correlating with the current view is usually done with the VIEW macro which allows to refer to the current view. This is important because within the brackets of an entity array expression, the implicit root for path expressions is the joined entity itself. The previous example can be simplified to the following.

@EntityView(Person.class)
public interface PersonView {

    @IdMapping
    Long getId();

    String getName();
}

@EntityView(Cat.class)
public interface CatView {

    @IdMapping
    Long getId();

    @Mapping("Person[age IN VIEW(age)]")
    Set<PersonView> getSameAgedPersons();
}

The result is very similar and roughly looks like this

SELECT cat.id, Person__age_IN_VIEW_age__.id, Person__age_IN_VIEW_age__.name
FROM Cat cat
LEFT JOIN Person Person__age_IN_VIEW_age__
       ON Person__age_IN_VIEW_age__.age = cat.age

3.15. Special method attributes

There are some special methods that can be declared abstract in an entity view type which have special runtime support.

3.15.1. EntityViewManager getter

An abstract method that returns EntityViewManager will not be considered to be an attribute. Such a method has special runtime support as it will always return the associated EntityViewManager.

@EntityView(Person.class)
public abstract class PersonView {

    @IdMapping
    public abstract Long getId();

    abstract EntityViewManager getEntityViewManager();

    public void someMethod() {
        // ... use getEntityViewManager()
    }
}

This is especially useful for Updatable Entity Views when a method wants to create a new instance of a subview or get a reference to a subview.

3.16. Mapping expression extensions

Blaze Persistence entity views generally supports the full set of expressions that JPQL and Blaze Persistence core module supports, but in addition to that, also offers some expression extensions.

3.16.1. THIS

Similar to the this expression in Java, in a mapping expression within entity views the this expression can be used to refer to the entity type backing the entity view. The expression can be used to implement embedded objects that are able to refer to the entity type of the entity view.

@EntityView(Cat.class)
interface EmbeddedCatView {

    @IdMapping
    Long getId();

    String getName();
}

@EmbeddableEntityView(Cat.class)
interface ExternalInterfaceView {

    @Mapping("name")
    String getExternalName();
}

@EntityView(Cat.class)
interface CatView {

    @IdMapping
    Long getId();

    @Mapping("this")
    EmbeddedCatView getEmbedded();

    @Mapping("this")
    ExternalInterfaceView getAdapter();
}

Both EmbeddedCatView and ExternalInterfaceView refer to the same Cat as their parent CatView. The query looks as if the types were directly embedded into the entity view.

SELECT
    cat.id,
    cat.id,
    cat.name,
    cat.name
FROM Cat cat

3.16.2. OUTER

In Blaze Persistence core the OUTER function can be used to refer to the query root of a parent query from within a subquery. This is still the same with Blaze Persistence entity views but might lead to unintuitive behavior when the subquery provider uses OUTER and is used in a subview. The following example shows the unintuitive behavior.

@EntityView(Cat.class)
interface CatView {

    @IdMapping
    Long getId();

    Set<KittenCatView> getKittens();
}

@EntityView(Cat.class)
interface KittenCatView {

    @IdMapping
    Long getId();

    @MappingSubquery(KittenCountSubqueryProvider.class)
    Long getKittenCount();

    class KittenCountSubqueryProvider implements SubqueryProvider {

        @Override
        public <T> T createSubquery(SubqueryInitiator<T> subqueryBuilder) {
            return subqueryBuilder.from(Cat.class, "subCat")
                    .select("COUNT(*)")
                    .whereOr()
                        .where("subCat.father.id").eqExpression("OUTER(id)")
                        .where("subCat.mother.id").eqExpression("OUTER(id)")
                    .endOr()
                .end();
        }
    }
}

When applying the KittenCatView directly, everything works as expected.

SELECT
    cat.id,
    (
        SELECT COUNT(*)
        FROM Cat subCat
        WHERE subCat.father.id = cat.id
        OR subCat.mother.id = cat.id
    )
FROM Cat cat

But when using KittenCatView as subview within CatView, it starts to break.

SELECT
    cat.id,
    kittens_1.id,
    (
        SELECT COUNT(*)
        FROM Cat subCat
        WHERE subCat.father.id = cat.id (1)
           OR subCat.mother.id = cat.id
    )
FROM Cat cat
LEFT JOIN cat.kittens kittens_1
1 OUTER resolved to cat instead of kittens_1

The OUTER function doesn’t know about the entity view structure and will remain to refer to the query root. It is often best to make use of the EMBEDDING_VIEW function instead, which refers to the relation of the embedding view.

3.16.3. VIEW

The VIEW function can be used to refer to the relation backed by the current view. Usually this is not necessary as the relation of the current view is the implicit root for path expressions, but within the brackets of an entity array expression the implicit root is the joined entity.

In such a case it is necessary to use the VIEW function to refer to attributes of the relation of the current view in the predicate. For an example usage, go to the entity array expression correlation section.

3.16.4. EMBEDDING_VIEW

The EMBEDDING_VIEW function can be used to refer to the relation backed by the embedding view. In case of a subquery provider, this will refer to the relation of the view, using the subquery provider. In case of a normal subview, this will refer to the relation of the view which contains the subview. One of the main use cases for this function is when using subquery mappings.

@EntityView(Cat.class)
interface CatView {

    @IdMapping
    Long getId();

    Set<KittenCatView> getKittens();
}

@EntityView(Cat.class)
interface KittenCatView {

    @IdMapping
    Long getId();

    @MappingSubquery(KittenCountSubqueryProvider.class)
    Long getKittenCount();

    class KittenCountSubqueryProvider implements SubqueryProvider {

        @Override
        public <T> T createSubquery(SubqueryInitiator<T> subqueryBuilder) {
            return subqueryBuilder.from(Cat.class, "subCat")
                    .select("COUNT(*)")
                    .whereOr()
                        .where("subCat.father.id").eqExpression("EMBEDDING_VIEW(id)")
                        .where("subCat.mother.id").eqExpression("EMBEDDING_VIEW(id)")
                    .endOr()
                .end();
        }
    }
}

When applying the KittenCatView directly, everything works as expected, just like it did before with OUTER.

SELECT
    cat.id,
    (
        SELECT COUNT(*)
        FROM Cat subCat
        WHERE subCat.father.id = cat.id
        OR subCat.mother.id = cat.id
    )
FROM Cat cat

But when using KittenCatView as subview within CatView, EMBEDDING_VIEW plays out it’s unique properties.

SELECT
    cat.id,
    kittens_1.id,
    (
        SELECT COUNT(*)
        FROM Cat subCat
        WHERE subCat.father.id = kittens_1.id (1)
           OR subCat.mother.id = kittens_1.id
    )
FROM Cat cat
LEFT JOIN cat.kittens kittens_1
1 EMBEDDING_VIEW resolved to kittens_1 whereas OUTER would resolve to cat
Make sure you understand the <<anchor-select-fetch-strategy-view-root-or-embedding-view,implication> of the EMBEDDING_VIEW function when using the batched SELECT fetch strategy as this might affect performance.

Note that the use of the EMBEDDING_VIEW function in a top level view will result in an exception since there is no embedding view.

3.16.5. VIEW_ROOT

The VIEW_ROOT function can be used to refer to the relation for which the main entity view is applied. Normally this will resolve to the query root, but beware that the entity view root might not always be the query root. One of the main use cases for this function is when using correlated subview mappings.

For further information on applying a different entity view root take a look into the querying chapter.

The VIEW_ROOT function can be used in a correlation provider to additionally refer to a view root.

@EntityView(Cat.class)
public interface CatView {

    @IdMapping
    Long getId();

    @MappingCorrelated(
        correlationBasis = "age",
        correlator = CatAgeCorrelationProvider.class
    )
    Set<Cat> getSameAgedCats();

    static class CatAgeCorrelationProvider implements CorrelationProvider {

        @Override
        public void applyCorrelation(CorrelationBuilder builder, String correlationExpression) {
            final String correlatedCat = builder.getCorrelationAlias();
            builder.correlate(Cat.class)
                .on(correlatedCat + ".age").inExpressions(correlationExpression)
                .on(correlatedCat + ".id").notInExpressions("VIEW_ROOT(id)") (1)
            .end();
        }

    }
}
1 We generally recommend to use the IN predicate through inExpressions() or notInExpressions() to be able to easily switch the fetch strategy

The VIEW_ROOT function is usable with every fetch strategy. In case of the JOIN fetch strategy the result is just as expected.

SELECT cat.id, correlatedCat
FROM Cat cat
LEFT JOIN Cat correlatedCat
       ON correlatedCat.age = cat.age
      AND correlatedCat.id <> cat.id (1)
1 Again, the IN predicate was rewritten to an equality predicate
Make sure you understand the <<anchor-select-fetch-strategy-view-root-or-embedding-view,implication> of the VIEW_ROOT function when using the batched SELECT fetch strategy as this might affect performance.

3.17. Entity View constructor mapping

So far, all mapping examples used interfaces for entity views, but as outlined in the beginning, Blaze Persistence entity views also has support for abstract classes. There are multiple use cases for using abstract classes for entity views, but in general we recommend to use an interface as often as possible. The biggest advantage of using abstract classes is that you can have a custom constructor which can further apply transformations on data.

3.17.1. Abstract class Entity View with custom equals-hashCode

Abstract classes, contrary to interfaces, can define an implementation for the equals and hashCode methods which is normally generated for the runtime implementations of Entity Views. If you decide to have a custom implementation you have to fulfill the general requirement, that the equals and hashCode methods use

  • Only the attribute mapped with @IdMapping if there is one

  • Otherwise use all attributes of the Entity View

Not following these requirements could lead to unexpected results so it is generally best to rely on the default implementation. For every custom implementation that is detected during the bootstrap a warning message will be logged.

3.17.2. Map external data model with view constructor

One of those use cases for a view constructor is integrating with an existing external data model.

class CatRestDTO {
    private final Long id;
    private final String name;

    public CatRestDTO(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

In general we recommend to use the entity view types directly instead of an external data model, because of the additional boilerplate code needed. Note that the creators of Blaze Persistence are not generally against external data models since it is reasonable to have them e.g. in API projects that shouldn’t expose a library dependency.

@EntityView(Cat.class)
public abstract class CatView extends CatRestDTO {

    public CatView(
        @Mapping("id") Long id,
        @Mapping("name") String name
    ) {
        super(id, name);
    }

}

Now you can use the CatView for efficient querying but still have objects that are an instance of CatRestDTO and can thus be used like normal CatRestDTO instances. To decouple the actual entity view CatView from the data access or service one normally uses method signatures like

interface CatDAO {
    <T> List<T> findAll(Class<T> entityViewClass); (1)

    <T> List<T> findAll(EntityViewSetting<T, CriteriaBuilder<T>> entityViewSetting); (2)
}
1 Create the EntityViewSetting within the implementation
2 Supply a custom EntityViewSetting which can also have filters, sorts, optional parameters and pagination information

By using one of these approaches you can have a projection independent implementation for CatDAO and let the consumer i.e. a REST endpoint decide about the representation.

3.17.3. Additional data transformation in view constructor

Another use case for view constructors is the transformation of data. Sometimes it is just easier to do the transformation in Java code instead of through a JPQL expression, but then there are also times when there is no other way than doing it in Java code.

Let’s assume you want to have an attribute that contains different text based on the age.

@EntityView(Cat.class)
interface CatView {

    @IdMapping
    Long getId();

    @Mapping("CASE WHEN age = 0 THEN 'newborn' WHEN age < 10 THEN 'child' WHEN age < 18 THEN 'teenager' ELSE 'adult' END")
    String getText();

}

As you can see, the CASE WHEN expression can be used to implement that, but if the text is only static, there is no need to use that kind of expression. You can instead just inject the age as constructor parameter and do the mapping to the text in Java code.

@EntityView(Cat.class)
public abstract class CatView {

    private final String text;

    public CatView(@Mapping("age") long age) {
        if (age == 0) {
            this.text = "newborn";
        } else if (age < 10) {
            this.text = "child";
        } else if (age < 18) {
            this.text = "teenager";
        } else {
            this.text = "adult";
        }
    }

    @IdMapping
    public abstract Long getId();

    public String getText() {
        return text;
    }
}

Since that kind of mapping logic is normally externalized, Blaze Persistence entity views also offers a way to inject external services. You can provide services to entity views via optional parameters like

EntityViewSetting<CatView, CriteriaBuilder<CatView>> setting
    = EntityViewSetting.create(CatView.class);

setting.addOptionalParameter("ageMapper", new AgeToTextMapper());
List<CatView> result = entityViewManager.applySetting(setting, cbf.create(em, Cat.class))
    .getResultList();

The services, or optional parameters in general can be consumed either as attributes or as constructor parameters with @MappingParameter. If the parameter is not supplied, null is injected.

@EntityView(Cat.class)
public abstract class CatView {

    private final String text;

    public CatView(
        @Mapping("age") long age,
        @MappingParameter("ageMapper") AgeToTextMapper mapper
    ) {
        this.text = ageMapper.map(age);
    }

    @IdMapping
    public abstract Long getId();

    public String getText() {
        return text;
    }
}

3.17.4. Multiple named constructors

So far, the example always used no or just a single constructor, but it is actually possible to have multiple constructors. Every constructor in an entity view must have a name defined via @ViewConstructor. The default name is init and is used for constructors that have no @ViewConstructor annotation.

@EntityView(Cat.class)
public abstract class CatView {

    private final String text;

    public CatView(
        @Mapping("age") long age,
        @MappingParameter("ageMapper") AgeToTextMapper mapper
    ) {
        this.text = ageMapper.map(age);
    }

    @ViewConstructor("special")
    public CatView(@Mapping("age") long age) {
        this.text = age > 80 ? "oldy" : "normal";
    }

    @IdMapping
    public abstract Long getId();

    public String getText() {
        return text;
    }
}

The constructor name can be chosen when constructing a EntityViewSetting via create().

EntityViewSetting.create(CatView.class, "special");

3.17.5. Using attribute getters in constructor

Since mapping constructor parameters can become very cumbersome and oftentimes you need a value not only in the constructor but also accessible directly via a getter, Blaze Persistence came up with a solution that allows you to use the getters of attributes in the constructor.

It might not be immediately obvious why this is a special thing. Since entity views are declared as abstract classes you can imagine that the runtime has to actually create concrete classes. These concrete classes normally initialize fields after calling the super constructor, thus making it impossible for the super constructor to actually retrieve values by using the attribute getters. The JVM enforces that fields can only be accessed after the super constructor has been called, so normally there is no way that the getter implementations that serve the fields can return non-null values in the super constructor. Fortunately, Blaze Persistence entity views found a way around this limitation of the JVM by making use of the infamous sun.misc.Unsafe to define a class that would normally fail bytecode verification. The trick is, that the implementations that are generated will set the fields before calling the super constructor thus making the values available to the super constructor.

By default, all abstract classes will be defined through sun.misc.Unsafe. If you don’t want that behavior and instead want bytecode verifiable implementations to be generated, you can always disable this strategy by using a configuration property.

@EntityView(Cat.class)
public abstract class CatView {

    private final String text;

    public CatView(@MappingParameter("ageMapper") AgeToTextMapper mapper) {
        this.text = ageMapper.map(getAge()); (1)
    }

    @IdMapping
    public abstract Long getId();

    public abstract Long getAge();

    public String getText() {
        return text;
    }
}
1 If the unsafe proxy is used, getAge() will return the actual value, otherwise it will return null

Note that instead of using this unsafe approach which can’t be used when generating entity view implementations through the annotation processor, one can make use of the @Self annotation to inject a view of the state to a constructor. The previous example defined in a safe way would look like this:

@EntityView(Cat.class)
public abstract class CatView {

    private final String text;

    public CatView(@Self CatView self, @MappingParameter("ageMapper") AgeToTextMapper mapper) {
        this.text = ageMapper.map(self.getAge());
    }

    @IdMapping
    public abstract Long getId();

    public abstract Long getAge();

    public String getText() {
        return text;
    }
}

Instead of calling the getter on the this instance which is not yet initialized, one uses the @Self annotated instance to access the state. The type of the instance is a serializable subclass of the entity view with the only purpose to serve as "self" instance. The construction of the "self" instance will not invoke any user code as it is constructed via deserialization to bypass constructor calls.

3.18. Inheritance mapping

Entity views can have an inheritance relationship to subtypes via an inheritance mapping. This relationship allows instances of an entity view subtype to be materialized when a selection predicate defined by an inheritance mapping is satisfied.

The inheritance feature for an entity view is activated by annotating @EntityViewInheritance on an entity view. By default, all subtypes of the entity view are considered as inheritance subtypes and thus require a so called inheritance mapping.

An inheritance mapping is defined by annotating the subtype with @EntityViewInheritanceMapping and defining a selection predicate that represents the condition on which decides the instantiation of that subtype. The predicate is a normal JPQL predicate expression and can refer to all attributes of the mapped entity type.

Consider the following example

@EntityView(Cat.class)
@EntityViewInheritance
public interface BaseCatView {
    String getName();
}

@EntityView(Cat.class)
@EntityViewInheritanceMapping("age < 18")
public interface YoungCatView extends BaseCatView {
    @Mapping("mother.name")
    String getMotherName();
}

@EntityView(Cat.class)
@EntityViewInheritanceMapping("age > 18")
public interface OldCatView extends BaseCatView {
    @Mapping("kittens.name")
    List<String> getKittenNames();
}

When querying for entity views of the type BaseCatView, the selection predicates age < 18 and age > 18 are merged into a type discriminator expression that returns a type index. The type index refers to the entity view type into which a result should be materialized. The resulting JPQL query for such an entity view looks like the following

SELECT
    CASE
        WHEN age < 18 THEN 1
        WHEN age > 18 THEN 2
        ELSE 0
    END,
    cat.name,
    mother_1.name,
    kittens_1.name
FROM Cat cat
LEFT JOIN cat.mother mother_1
LEFT JOIN cat.kittens kittens_1

The type index 0 refers to the base type BaseCatView, hence instances of BaseCatView are materialized when the age of a result equals 18.

Since it might not be desirable to use all entity view subtypes for the inheritance relationship, it is possible to explicitly declare the subtypes in the @EntityViewInheritance annotation on the super type.

@EntityView(Cat.class)
@EntityViewInheritance({ YoungCatView.class })
public interface BaseCatView {
    String getName();
}

This has the effect, that only BaseCatView or YoungCatView instances are materialized for a result.

3.19. Inheritance subview mapping

Similarly to specifying the entity view inheritance subtypes at the declaration site, i.e. BaseCatView, it is also possible to define subtypes at the use site, i.e. at the subview attribute. By annotating the subview attribute with @MappingInheritance, it is possible to delimit and override the entity view subtype mappings that are considered for materialization from the result. When using the @MappingInheritance annotation, it is required to list all desired subtypes via @MappingInheritanceSubtype annotations that can optionally override the inheritance mapping.

@EntityView(Person.class)
interface PersonView {
    String getName();
    @MappingInheritance({
        @MappingInheritanceSubtype(mapping = "age <= 18", value = YoungCatView.class)
    })
    Set<BaseCatView> getCats();
}

@EntityView(Cat.class)
@EntityViewInheritance
public interface BaseCatView {
    String getName();
}

@EntityView(Cat.class)
@EntityViewInheritanceMapping("age < 18")
public interface YoungCatView extends BaseCatView {
    @Mapping("mother.name")
    String getMotherName();
}

@EntityView(Cat.class)
@EntityViewInheritanceMapping("age > 18")
public interface OldCatView extends BaseCatView {
    @Mapping("kittens.name")
    List<String> getKittenNames();
}

When querying for PersonView, YoungCatView instances will be materialized if the cat’s age is lower or equal to 18 and otherwise instances of BaseCatView will be created. By setting the annotation property onlySubtypes to true, instances of the base type BaseCatView aren’t materialized but a null is propagated. Apart from skipping the base type, it is also possible to define the base type via @MappingInheritanceSubtype which allows to specify the inheritance mapping for the base type.

When no @MappingInheritanceSubtype elements are given, only the base type is materialized which can be used to disable the inheritance feature for an attribute.
It is illegal to set onlySubtypes to true and have an empty set of subtype mappings as that would always result in a null object.

3.19.1. Inheritance mapping with constructors

Entity view inheritance is not limited to interface types but can also be used with custom constructors. If a view constructor is used, all entity view inheritance subtypes must have a view constructor with the same name. In case of just a single constructor the @ViewConstructor does not have to be applied, as the name init is chosen by default as name.

@EntityView(Cat.class)
@EntityViewInheritance
public abstract class BaseCatView {

    private final String parentName;

    public BaseCatView(@Mapping("father.name") String parentName) {
        this.parentName = parentName;
    }

    public abstract String getName();
}

@EntityView(Cat.class)
@EntityViewInheritanceMapping("age < 18")
public abstract class YoungCatView extends BaseCatView {

    public YoungCatView(@Mapping("mother.name") String parentName) {
        super(parentName);
    }

    @Mapping("mother.name")
    public abstract String getMotherName();
}

@EntityView(Cat.class)
@EntityViewInheritanceMapping("age > 18")
public abstract class OldCatView extends BaseCatView {

    public OldCatView() {
        super("None");
    }

    @Mapping("kittens.name")
    public abstract List<String> getKittenNames();
}

3.19.2. Inheritance mapping and JPA inheritance

The most obvious use case for entity view inheritance is mapping JPA entities that use an inheritance relationship. Blaze Persistence supports this and also makes use of defaults for the inheritance mapping in case a entity view subtype uses an entity subtype in the @EntityView annotation.

@EntityView(Animal.class)
@EntityViewInheritance
public interface AnimalView {

    String getName();
}

@EntityView(Cat.class)
public interface CatView extends AnimalView {

    String getKittyName();
}

@EntityView(Dog.class)
public interface DogView extends AnimalView {

    String getDoggyName();
}

The DogView uses the entity Dog and CatView the entity Cat which are both subtypes of Animal. In this case no inheritance mapping needs to be provided as Blaze Persistence will generate a type constraint like TYPE(this) = Dog or TYPE(this) = Cat for the respective entity view subtypes DogView and CatView. The resulting JPQL query when using AnimalView might look like the following

SELECT
    CASE
        WHEN TYPE(animal) = Cat THEN 1
        WHEN TYPE(animal) = Dog THEN 2
        ELSE 0
    END,
    animal.name,
    TREAT(animal AS Cat).kittyName,
    TREAT(animal AS Dog).doggyName
FROM Animal animal

As can be seen, the expressions for the access of the subtype properties rightfully make use of the TREAT operator.

An entity view could also be modelled flat i.e. not mirroring the entity inheritance relationship as entity views, but just put the desired properties on a single entity view type. This can be done by making use of the TREAT operator and the this expression in the entity view mappings just as expected.

@EntityView(Animal.class)
@EntityViewInheritance
public interface MyAnimalView {

    String getName();

    @Mapping("TREAT(this AS Cat).kittyName")
    String getKittyName();

    @Mapping("TREAT(this AS Dog).doggyName")
    String getDoggyName();
}

The generated query looks approximately like this

SELECT
    animal.name,
    TREAT(animal AS Cat).kittyName,
    TREAT(animal AS Dog).doggyName
FROM Animal animal

and in case an animal is not of the treated type, a null value will be produced.

3.20. Using CTEs in entity views

A CTE can be used in an entity view by correlating the CTE entity type, but it is still necessary to define the CTE. This can be done on the underlying Blaze Persistence core CriteriaBuilder before applying an entity view. Doing that is not always ideal e.g. when considering repositories for Spring Data or DeltaSpike Data, there is usually no access to the CriteriaBuilder.

The @With annotation can be applied on an entity view class to register a CTEProvider to the entity view class. When applying an entity view, it’s corresponding registered CTEProvider instances are invoked that can define CTEs

@EntityView(Cat.class)
@With(MyCteProvider.class)
public interface BaseCatView {

    class MyCteProvider implements CTEProvider {
        @Override
        public void applyCtes(CTEBuilder<?> builder, Map<String, Object> optionalParameters) {
            builder.with(MyCTE.class); // ...
        }
    }
}

For more information about CTEs refer to the CTE section in the core documentation.

3.21. Secondary entity view roots

Up until now, mappings were always relative to the entity type of the entity view within which the mappings are defined, except for entity array expression correlations. Although entity array expressions are very mighty, they always imply a left join and there is no way to define a limit on elements to be joined. This is where secondary entity view roots come in.

A secondary entity view root has an name and is defined on the entity view class level through the @EntityViewRoot and @EntityViewRoots annotations. The name is very important as secondary entity view roots are registered on the underlying query builder with the name as alias.

Roughly speaking, secondary entity view roots can be thought of as a way to define joins that are registered and make them available through the defined name to mappings of an entity view.

@EntityView(Cat.class)
@EntityViewRoot(name = "v1", entity = Cat.class, condition = "id = VIEW(id)", joinType = JoinType.INNER)
@EntityViewRoot(name = "v2", expression = "Cat[id = VIEW(id)]", limit = "1", order = "id DESC")
@EntityViewRoot(name = "v3", correlator = CatView.TestCorrelator.class)
public interface CatView {

    @IdMapping
    Long getId();

    String getName();

    @Mapping("v1.name")
    String getV1Name();

    @Mapping("v2.name")
    String getV2Name();

    @Mapping("v3.name")
    String getV3Name();

    class TestCorrelator implements CorrelationProvider {
        @Override
        public void applyCorrelation(CorrelationBuilder correlationBuilder, String correlationExpression) {
            correlationBuilder.correlate(Cat.class)
                    .on(correlationBuilder.getCorrelationAlias()).eqExpression(correlationExpression)
                    .end();
        }
    }
}

The generated query for such an entity view will roughly look like the following:

SELECT
    cat.id
    cat.name,
    v1.name,
    v2.name,
    v3.name
FROM Cat cat
JOIN Cat v1 ON v1.id = cat.id
LEFT JOIN LATERAL Cat(
    SELECT v2_sub.age, v2_sub.father.id, v2_sub.id, v2_sub.mother.id, v2_sub.name
    FROM Cat v2_sub
    WHERE v2_sub.id = cat.id
    ORDER BY v2_sub.id DESC
    LIMIT 1
) v2(age, father.id, id, mother.id, name) ON 1=1
LEFT JOIN Cat v3 ON v3.id = cat.id

The big differences to using entity array expressions directly are:

  • Possibility to specify the join type

  • Possibility to specify a limit/offset with an order to implement a TOP-N per category join

  • Possibility to use subqueries in the ON clause through a CorrelationProvider

A view root can be either defined through an entity class with a condition:

@EntityViewRoot(
    name = "root1",
    entity = Document.class,
    condition = "id = VIEW(documentId)"
)

an expression with an optional condition:

@EntityViewRoot(
    name = "root2",
    expression = "Document[id = VIEW(documentId)]",
    condition = "root2.age > 10"
)

or through a correlator:

@EntityViewRoot(
    name = "root3",
    correlator = MyCorrelationProvider.class
)

Paths that are not fully qualified i.e. relative paths that use no root alias, are prefixed with the entity view root alias.

The entity view root name must be unique across all entity view types that are accessible through attributes.

During boot, every CorrelationProvider is probed to figure out the correlation type. If the CorrelationProvider is dynamic, you can optionally define the type through the entity attribute. NOTE: Using this feature requires a JPA provider that supports entity joins. Using JoinType.INNER can be a workaround as that is emulated through cross joins if needed.

4. Fetch strategies

There are multiple different fetch strategies available for fetching. A fetch strategy can be applied to all kinds of mappings, except for @MappingParameter and @MappingSubquery. These mappings will always use a JOIN strategy, i.e. the mapping will be put into the main query.

Any attribute in an entity view can be fetched separately by specifying a fetch strategy other than JOIN.

Every fetch strategy has some pros and cons but most of the time, the JOIN fetch strategy is a good choice. Unless you can’t use the JOIN strategy because your JPA provider doesn’t support entity joins, you should always stick with it by default and only change the strategy on a case by case basis.

4.1. Join fetch strategy

If your JPA provider supports entity joins, the JOIN strategy usually makes sense most of the time. In case of correlated mappings it will result in a LEFT JOIN entity join of the correlated entity type. The correlation expression created by the CorrelationProvider will be used in the ON condition.

For an example query that is generated by this strategy take a look at the correlation mappings chapter.

Entity joins are only supported in newer versions of JPA providers(Hibernate 5.1+, EclipseLink 2.4+, DataNucleus 5+)

4.2. Select fetch strategy

In general, the SELECT strategy will create a separate query for every attribute that uses it. It will collect up to N distinct correlation basis values and then will execute that query to actually fetch the values for the attributes of the instances. The parameter N is the batch size that can be configured at multiple levels.

Let’s look at an example that shows what happens

@EntityView(Cat.class)
public interface CatView {

    @IdMapping
    Long getId();

    @BatchFetch(20) (1)
    @MappingCorrelated(
        correlationBasis = "age",
        correlator = PersonAgeCorrelationProvider.class,
        fetch = FetchStrategy.SELECT
    )
    Set<Person> getSameAgedPersons();

    static class PersonAgeCorrelationProvider implements CorrelationProvider {

        @Override
        public void applyCorrelation(CorrelationBuilder builder, String correlationExpression) {
            final String pers = builder.getCorrelationAlias();
            builder.correlate(Person.class)
                .on(pers + ".age").inExpressions(correlationExpression)
            .end();
        }

    }
}
1 Defines the batch size to use for loading

When using this entity view, there are 2 queries that are generated.

SELECT cat.id, cat.age
FROM Cat cat

The main query will fetch the correlationBasis and all the other attributes.

SELECT
    correlationParams,
    correlated_SameAgedPersons
FROM Person correlated_SameAgedPersons,
     Long(20 VALUES) correlationParams
WHERE correlated_SameAgedPersons.age = correlationParams

The correlation query on the other hand will select the correlation value and the Person instances with an age matching any of the correlationParams values. What you see here is the use of the VALUES clause for making multiple values available like a table for querying which is required when wanting to select the correlation value. Selecting the actual correlation value via correlationParams along with the Person is necessary to be able to correlate the instances to the instance of the main query.

Depending on how many different values for age there are(cardinality), the correlation query might get executed multiple times. In general, the runtime will collect up to batch size different values and then execute the correlation query for these values. Results for a correlation value are cached during the querying to avoid querying the same correlation values multiple times in different batches.

This strategy works best when the cardinality of the correlationBasis is low i.e. there are only a few distinct values. If the cardinality is high and the batch size is too low, this can lead to something similar as an N + 1 select known from lazy loading of collection elements. You could theoretically choose a very big batch size to be able to handle more correlation values per query, but beware that there are limits to the efficiency of this approach. Also beware that the amount of possible parameters might be limited by the DBMS. A value of 1000 for the batch size shouldn’t generally be a problem for a DBMS, but before you configure such a high value, look into the subselect strategy which might be more appropriate for higher cardinalities.

4.2.1. Select fetch strategy with batching

Apart from using the @BatchFetch annotation, there are some other ways to define a batch size for fetching of an attribute.

Batch size default per entity view

A default batch size can be defined by setting the property com.blazebit.persistence.view.batch_size via EntityViewSetting.setProperty(). The value serves as default value and can be overridden on a per attribute basis.

Batch size per entity view attribute

The batch size for a specific attribute can be defined either by using the @BatchFetch annotation or by setting the com.blazebit.persistence.view.batch_size property suffixed with the attribute name. In order to set the batch size for an attribute named someAttribute you have to set the property com.blazebit.persistence.view.batch_size.someAttribute via EntityViewSetting.setProperty(). The path to the attribute is based on the entity view which is queried and can also be deep i.e. someSubview.someAttribute.

4.2.2. Select fetch strategy with VIEW_ROOT or EMBEDDING_VIEW

One possible problem with this strategy might arise when using the VIEW_ROOT or EMBEDDING_VIEW function. The use of two correlation keys i.e. the view root or embedding view and the correlation basis, will affect the way the batching can be done. Before querying for the correlated date, the runtime will determine the cardinality of the view ids and the correlation basis values. After that, it will group the values with higher cardinality by the values with lower cardinality to be able to do efficient batching.

Let’s see what that means

@EntityView(Cat.class)
public interface CatView {

    @IdMapping
    Long getId();

    Set<KittenCatView> getKittens();

}

@EntityView(Cat.class)
public interface KittenCatView {

    @IdMapping
    Long getId();

    @BatchFetch(20)
    @MappingCorrelated(
        correlationBasis = "age",
        correlator = CatAgeCorrelationProvider.class,
        fetch = FetchStrategy.SELECT
    )
    Set<Cat> getSameAgedCats();

    static class CatAgeCorrelationProvider implements CorrelationProvider {

        @Override
        public void applyCorrelation(CorrelationBuilder builder, String correlationExpression) {
            final String correlatedCat = builder.getCorrelationAlias();
            builder.correlate(Cat.class)
                .on(correlatedCat + ".age").inExpressions(correlationExpression)
                .on(correlatedCat + ".id").notInExpressions("VIEW_ROOT(id)")
            .end();
        }

    }
}

In this example the batching might happen either for view roots or correlation basis values depending on the data. If the number of distinct view root ids is lower than the number of distinct correlation basis values, the correlation basis values are grouped by view root ids. The runtime will then execute a batched query for every view root id.

The good thing is, the runtime will adapt based on the data to minimize the number of queries, but still, if the cardinality is high, this can result in many queries being executed.

Batching expectation fine tuning

By default the runtime assumes that neither the VIEW_ROOT nor the EMBEDDING_VIEW function are used and generates a query that batches correlation basis values. If this assumption fails because the VIEW_ROOT or EMBEDDING_VIEW function is used and the batching is done based on view root or embedding view ids, a new query has to be built.

The way the VIEW_ROOT and EMBEDDING_VIEW functions are implemented, it is required to invoke the CorrelationProvider again for building the new query.

To avoid this unnecessary rebuilding of the query, you can specify the batch expectation for all attributes by setting the property com.blazebit.persistence.view.batch_mode via EntityViewSetting.setProperty() to view_roots if batching is expected to be done on a view root id basis or embedding_views if batching is expected to be done on a embedding view id basis. The value serves as default value and can be overridden on a per attribute basis by suffixing the property name with the attribute name. In order to set the batch expectation for an attribute named someAttribute you have to set the property com.blazebit.persistence.view.batch_mode.someAttribute via EntityViewSetting.setProperty(). The path to the attribute is based on the entity view which is queried and can also be deep i.e. someSubview.someAttribute.

4.3. Subselect fetch strategy

The SUBSELECT strategy will create one query for every attribute that uses it and is especially efficient for bigger collections. It creates a separate query based on the outer query and applies the CorrelationProvider to it.

Let’s look at an example that shows what happens

@EntityView(Cat.class)
public interface CatView {

    @IdMapping
    Long getId();

    @MappingCorrelated(
        correlationBasis = "age",
        correlator = PersonAgeCorrelationProvider.class,
        correlationResult = "pers",
        fetch = FetchStrategy.SUBSELECT
    )
    Set<Person> getSameAgedPersons();

    static class PersonAgeCorrelationProvider implements CorrelationProvider {

        @Override
        public void applyCorrelation(CorrelationBuilder builder, String correlationExpression) {
            final String pers = builder.getCorrelationAlias();
            builder.correlate(Person.class)
                .on(pers + ".age").inExpressions(correlationExpression)
            .end();
        }

    }
}

When using this entity view, there are 2 queries that are generated.

SELECT cat.id, cat.age
FROM Cat cat

The main query will fetch the correlationBasis and all the other attributes.

SELECT
    cat.age,
    correlated_SameAgedPersons
FROM Cat cat,
     Person correlated_SameAgedPersons
WHERE correlated_SameAgedPersons.age = cat.age

The correlation query looks very similar since it’s based on the main query, but has a custom select clause. It selects the correlation key as well as the attributes for the target representation in the main entity view.

4.4. Multiset fetch strategy

The MULTISET strategy will use the TO_MULTISET function which aggregates tuples to a e.g. JSON/XML which is very efficient for big collections and wide rows. Note that using this strategy puts some restrictions on the attributes contained in the view types of the MULTISET fetched attribute:

  • The types of the attributes all must have a BasicUserTypeStringSupport implementation which is the case for most basic types

  • Entity types are not allowed because a BasicUserTypeStringSupport implementation is not possible for such types

Let’s look at an example that shows what happens

@EntityView(Cat.class)
public interface CatNameView {

    @IdMapping
    Long getId();

    String getName();
}

@EntityView(Cat.class)
public interface CatView extends CatNameView {

    @Mapping(fetch = FetchStrategy.MULTISET)
    Set<CatNameView> getKittens();

}

When using this entity view, only one query is generated.

SELECT
    cat.id,
    cat.name,
    TO_MULTISET((
        SELECT kittens_1.id, kittens_1.name
        FROM cat.kittens kittens_1
    ))
FROM Cat cat

Behind the scenes, depending on the DBMS support, this will use JSON/XML functions to aggregate the subquery rows to a CLOB-like value. This aggregation comes with a certain cost, so this strategy is not perfect.

This strategy outperforms the JOIN strategy only when the rows are very wide(i.e. take a lot of space) or there are nested collections. Wide rows are a problem for JOIN because these rows have to be duplicated for every collection element which is a problem from the network bandwidth and Java memory consumption perspective.

Using JOIN fetching when selecting a list of 10 elements, each having a collection of 10 sub-elements will result in 100 rows being produced in a JDBC result. When each of the sub-elements have one or more collection again with overall 20 elements, 2000 rows are being produced in a JDBC result. Fetching 2000 rows is not a big deal for most DBMS and is usually pretty fast, but if the rows are very wide e.g. row size > 1kB network bandwidth and memory usage might slowly become a problem. With MULTISET fetching of the collection of the sub-elements, the JDBC result size will go down to 100 rows again and save a lot of bandwidth and memory because tuples don’t have to be duplicated. Unfortunately, the aggregation is not as efficient as fetching the collection separately. Overall, the MULTISET strategy will still mostly outperforms the SELECT and SUBSELECT fetch strategy due to the reduced latency and fewer query executions.

5. Filter and Sorter API

Apart from mapping projections, Blaze Persistence entity views also provides support for filtering and sorting on attribute-level. Implementing the filtering and sorting based on attributes allows to completely encapsulate the entity model behind an entity view. The structure of an entity view is driven by the consumer and basing the filtering and sorting aspects on that very same structure is only natural for a consumer.

The filter and sorter API is provided via com.blazebit.persistence.view.EntityViewSetting and allows filtering and sorting to be applied to entity views dynamically. Dynamic in this context means that the filters and sorters can be added/enabled without the need to explicitly modify the entity view type itself or the criteria builder which the entity view is based on.

Let’s consider the following data access method for an example

<V, C extends CriteriaBuilder<V>> getHungryCats(EntityViewSetting<V, C> settings) { ... }

It implements the basic business logic of how to obtain all hungry cats via a CriteriaBuilder from the database. The method supports entity views to allow fetching only the fields which are needed for a concrete use cases. For example when displaying the cats in a dropdown, their names might be sufficient but when displaying them in a table it might be desirable to include more details.

Likewise, one might want to retrieve the cats sorted by name or by age depending on the use case. Having to introduce 2 new methods for this purpose would be painful:

<V, C extends CriteriaBuilder<V>> getHungryCatsSortedByName(EntityViewSetting<V, C> settings);

<V, C extends CriteriaBuilder<V>> getHungryCatsSortedByAge(EntityViewSetting<V, C> settings);

The above approach does not even account for different sort orders so in reality we might rather go for parameterizing the original method which is painful nevertheless:

<V, C extends CriteriaBuilder<V>> getHungryCats(EntityViewSetting<V, C> settings, String sortField, String sortOrder);

Instead it is possible to apply the sorting to the EntityViewSetting instance that is passed to your data access layer:

settings.addAttributeSorter("name", com.blazebit.persistence.view.Sorters.ascending());
dataAccess.getHungryCats(settings);
All attribute names specified using the filter or sorter API refer to the entity view attribute names rather than entity attribute names.

5.1. Filter API

The filter API allows to enable and parameterize a filter for entity view attributes.

Entity view filters are defined by annotating respective entity view attributes with @AttributeFilter or @AttributeFilters for multiple named filters.

In the annotation you can supply an optional filter name and a filter provider class which needs to extend AttributeFilterProvider. An attribute filter’s name must be unique for the attribute it is annotated on. The attribute filter without a filter name is the default filter. Only a single default attribute filter per attribute is allowed.

Example:

@EntityView(Cat.class)
public interface CatView {
    @IdMapping
    Integer getId();

    @AttributeFilters({
        AttributeFilter(ContainsIgnoreCaseFilter.class),
        AttributeFilter(name = "containsCaseSensitive", value = ContainsFilter.class)
    })
    String getName();
}

The supplied object values are used by the filter provider to append the appropriate restrictions to the query builder.

EntityViewSetting<CatView, CriteriaBuilder<CatView>> setting = EntityViewSetting.create(CatView.class);
setting.addAttributeFilter("name", "kitty"); (1)
setting.addAttributeFilter("name", "containsCaseSensitive", "kitty"); (2)
1 Enables the default filter ContainsIgnoreCaseFilter, so e.g. KITTY matches
2 Enables the named filter ContainsFilter, so e.g. KITTY doesn’t match

Blaze Persistence provides a number of built-in filter providers in the com.blazebit.persistence.view.filter package:

Built-in filters Supported filter value types

GreaterOrEqualFilter

Number, Date, String

LessOrEqualFilter

Number, Date, String

GreaterThanFilter

Number, Date, String

LessThanFilter

Number, Date, String

BetweenFilter

Range<?> or Object[] with: Number, Date, String

EqualFilter

Any

StartsWithFilter

String

EndsWithFilter

String

ContainsFilter

String

StartsWithIgnoreCaseFilter

String

EndsWithIgnoreCaseFilter

String

ContainsIgnoreCaseFilter

String

NullFilter

Boolean - true includes NULLs, false excludes NULLs

It is also possible to filter by subview attributes. The following example illustrates this:

@EntityView(Cat.class)
public interface CatView {
    @IdMapping
    Integer getId();

    ChildCatView getChild();
}

@EntityView(Cat.class)
public interface ChildCatView {
    @IdMapping
    Integer getId();

    @AttributeFilter(LessOrEqualFilter.class)
    Integer getAge();
}

CriteriaBuilderFactory cbf = ...;
EntityViewManager evm = ...;
EntityViewSetting<CatView, CriteriaBuilder<CatView>> setting = EntityViewSetting.create(CatView.class);
// by adding this filter, only cats with a child of age <= 10 will be selected
setting.addAttributeFilter("child.age", "10");
Currently there is no support for collection filters like "has at least one" semantics. This is planned for a future version. When applying an attribute filter on a collection attribute or a subview attribute contained in a collection, the collection’s elements will currently be filtered. In the meantime, collection filters can be implemented by creating a custom attribute filter, applying restrictions directly on the entity view’s base query or by using a view filter.

5.1.1. View filters

View filters allow filtering based on attributes of the view-backing entity as opposed to attribute filters which relate to entity view attributes.

For example, the following entity view uses a view filter to filter by the age entity attribute of the Cat entity without this attribute being mapped in the entity view.

@EntityView(Cat.class)
@ViewFilter(name = "ageFilter", value = AgeFilterProvider.class)
public interface CatView {
    @IdMapping
    Integer getId();

    String getName();

    class AgeFilterProvider extends ViewFilterProvider {
        @Override
        public <T extends WhereBuilder<T>> T apply(T whereBuilder) {
            return whereBuilder.where("age").gt(2L);
        }
    }
}

View filters need to be activated via the EntityViewSetting:

setting.addViewFilter("ageFilter");

5.1.2. Custom filters

If the built-in filters do not satisfy your requirements you are free to implement custom attribute filters by extending AttributeFilterProvider with either one constructor accepting

  • Class<?> - The attribute type

  • Object - The filter value

  • Class<?> and Object - The attribute type and the filter value

Have a look at how a range filter could be implemented:

public 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 Number lower;
        private final Number upper;

        public Range(Number lower, Number upper) {
            this.lower = lower;
            this.upper = upper;
        }
    }
}

The filter implementation only uses the filter value in the constructor and assumes it to be of the Range type. By accepting the attribute type, a string to object conversion for the filter value can be implemented.

5.2. Sorter API

The sorter API allows to sort entity views by their attributes. A sorter can be applied for an attribute by invoking addAttributeSorter(String attributeName, Sorter sorter)

For an example of how to use the sorter API refer to the introductory example.

Blaze Persistence provides default sorters via the static methods in the Sorters class. These methods allow to easily create any combination of ascending/descending and nulls-first/nulls-last sorter.

At most one attribute sorter can be enabled per attribute.
Sorting by subquery attributes (see ??) is problematic for some DBs.
Currently, sorting by correlated attribute mappings (see ??) is also not fully supported.

5.2.1. Custom sorter

If the built-in sorters do not satisfy your requirements you are free to create a custom sorter by implementing the Sorter interface.

An example for a custom sorter might be a case insensitive sorter

public class MySorter implements com.blazebit.persistence.view.Sorter {

    private final Sorter sorter;

    private MySorter(Sorter sorter) {
        this.sorter = sorter;
    }

    public static Sorter asc() {
        return new MySorter(Sorters.ascending());
    }

    public static Sorter desc() {
        return new MySorter(Sorters.descending());
    }

    public <T extends OrderByBuilder<T>> T apply(T sortable, String expression) {
        return sorter.apply(sortable, "UPPER(" + expression + ")");
    }
}

6. Querying and Pagination API

The main entry point to entity views is via the EntityViewSetting.create() API. There are multiple different variants of the static create() method that allow to construct a EntityViewSetting.

create(Class<?> entityViewClass)

Creates a simple entity view setting without pagination.

create(Class<T> entityViewClass, int firstResult, int maxResults)

Creates a entity view setting that will apply pagination to a CriteriaBuilder via page(int firstResult, int maxResults)

create(Class<T> entityViewClass, Object entityId, int maxRows)

Creates a entity view setting that will apply pagination to a CriteriaBuilder via pageAndNavigate(Object entityId, int maxResults)

Every of the variants also has an overload that additionally accepts a viewConstructorName to be able to construct entity views via named constructors.

A EntityViewSetting essentially is configuration that can be applied to a CriteriaBuilder and contains the following aspects

  • Projection and DTO construction based on the entity view class

  • Entity view attribute based filtering

  • Entity view attribute based sorting

  • Query pagination

Allowing the actual data consumer i.e. the UI to specify these aspects is essential for efficient and easy to maintain data retrieval.

For a simple lookup by id there is also a convenience EntityViewManager.find() method available that allows you to skip some of the CriteriaBuilder ceremony and that works analogous to how EntityManager.find() works, but with entity views.

CatView cat = entityViewManager.find(entityManager, CatView.class, catId);

To get just a reference to an entity view similar to what an entity reference retrieved via EntityManager.getReference() represents, it is possible to use EntityViewManager.getReference(). Note that the returned object will only have the identifier set, all other attributes will have their default values. This is usually useful when wanting to compare a list of elements with some entity view type against an entity id or also for setting *ToOne relationships.

CatView cat = entityViewManager.getReference(CatView.class, catId);

To get a reference to an entity form an entity view one can use EntityViewManager.getEntityReference() which will return the entity reference object retrieved via EntityManager.getReference() for the given entity view object.

Cat cat = entityViewManager.<Cat>getEntityReference(entityManager, catView);

6.1. Querying entity views

Code in the presentation layer is intended to create an EntityViewSetting via the create() API and pass the entity view setting to a data access method. The data access method then applies the setting onto a CriteriaBuilder instance which it created to build a query.

We know that the current state of the EntityViewSetting API requires some verbose generics and we are going to fix that in 2.0. For further information also see #371

6.1.1. Normal CriteriaBuilder use

Depending on the need for pagination, an EntityViewSetting object is normally created like this

EntityViewSetting<CatView, CriteriaBuilder<CatView>> setting;
// Use this if no pagination is required
setting = EntityViewSetting.create(CatView.class);
// Apply filters and sorters on setting
List<CatView> list = catDataAccess.findAll(setting);

The implementation of the catDataAccess is quite simple. It creates a query with the CriteriaBuilder API as usual, and finally applies the setting on the builder through the EntityViewManager.applySetting() method.

// Inject these somehow
CriteriaBuilderFactory criteriaBuilderFactory;
EntityViewManager entityViewManager;

public <V, Q extends CriteriaBuilder<V>> List<V> findAll(EntityViewSetting<V, Q> setting) {
    CriteriaBuilder<Cat> criteriaBuilder = criteriaBuilderFactory.create(Cat.class);

    // Apply business logic filters
    criteriaBuilder.where("deleted").eq(false);

    return entityViewManager.applySetting(setting, criteriaBuilder)
                .getResultList();
}

6.1.2. Paginating entity view results

When data pagination is required, the firstResult and maxResults parameters are required to be specified when creating the EntityViewSetting object

EntityViewSetting<CatView, PaginatedCriteriaBuilder<CatView>> setting;
// Paginate and show only the 10 first records by doing this
setting = EntityViewSetting.create(CatView.class, 0, 10);
// Apply filters and sorters on setting
PagedList<CatView> list = catDataAccess.findAll(setting);

To actually be able to get the PagedList instead of a normal list, the following data access implementation is required

// Inject these somehow
CriteriaBuilderFactory criteriaBuilderFactory;
EntityViewManager entityViewManager;

public <V, Q extends PaginatedCriteriaBuilder<V>> PagedList<V> findAll(EntityViewSetting<V, Q> setting) {
    CriteriaBuilder<Cat> criteriaBuilder = criteriaBuilderFactory.create(Cat.class);

    // Apply business logic filters
    criteriaBuilder.where("deleted").eq(false);

    return entityViewManager.applySetting(setting, criteriaBuilder)
                .getResultList();
}

The only difference to the former implementation is that this method uses the PaginatedCriteriaBuilder as upper bound for the type variable and a different return type. By using a different type variable bound, the EntityViewManager.applySetting() will return an instance of PaginatedCriteriaBuilder. It’s getResultList() returns a PagedList instead of a normal list.

6.1.3. Keyset pagination with entity views

The EntityViewSetting API also comes with an integration with the keyset pagination feature.

A EntityViewSetting that serves for normal offset based pagination, can be additionally enriched with a KeysetPage by invoking withKeysetPage(KeysetPage keysetPage). Supplying a keyset page allows the runtime to choose keyset pagination instead of offset pagination based on the requested page and the supplied keyset page.

To be able to use keyset pagination, it is required to remember the last known keyset page. When using a server side UI technology, this can be done very easily by simply saving the keyset page in the HTTP session. With e.g. CDI the KeysetPage could simply be declared as field of a session-like scoped bean.

EntityViewSetting<CatView, PaginatedCriteriaBuilder<CatView>> setting;

int maxResults = ...; // elements per page
int firstResult = ...; // (pageNumber - 1) * elementsPerPage

setting = EntityViewSetting.create(CatView.class, firstResult, maxResults);
// Apply filters and sorters on setting

setting.withKeysetPage(previousKeysetPage);

PagedList<CatView> list = catDataAccess.findAll(setting);
previousKeysetPage = list.getKeysetPage();

When using a more stateless approach like it is often the case with RESTful backends, the keyset page has to be serialized to the client and deserialized back when reading from the client. Depending on your requirements, you can serialize the KeysetPage directly into e.g. a JSON object and should be able to deserialize it with the most common serialization libraries. Another possible way to integrate this, is to generate URLs that contain the keyset in some custom format which should then be used by the client to navigate to the next or previous page.

Any of these approaches will require custom implementations of the KeysetPage and Keyset interfaces.

The GraphQL, JAX-RS and Spring MVC, as well as Spring WebFlux integrations have proper implementations for serializing and deserializing entity view types.

6.1.4. Entity page navigation with entity views

Sometimes it is necessary to navigate to a specific entry with a specific id. When required to also display the entry in a paginated table marked as selected, it is necessary to determine the page at which an entry with an id is located. This feature is implemented by the navigate to entity page feature and can be used by creating an EntityViewSetting via create(Class<T> entityViewClass, Object entityId, int maxResults).

EntityViewSetting<CatView, PaginatedCriteriaBuilder<CatView>> setting;

setting = EntityViewSetting.create(CatView.class, catId, maxResults);
// Apply filters and sorters on setting

// Use this to activate keyset pagination
setting.withKeysetPage(null);

PagedList<CatView> list = catDataAccess.findAll(setting);
previousKeysetPage = list.getKeysetPage();

6.2. Optional parameters and configuration

Apart from the already presented aspects, a EntityViewSetting also contains so called optional parameters and configuration properties.

Optional parameters are set on a query if no value is set and also injected into entity views if requested by a parameter mapping and are a very good integration point for dependency injection into entity views. They can be set with the addOptionalParameter(String parameterName, Object value) method.

Configuration properties denoted as being always applicable can be set via setProperty(String propertyName, Object value) and allow to override or fine tune configuration time behavior for a single query.

6.3. Applying entity views on specific relations

Up until now, an entity view setting has always been applied on the query root of a CriteriaBuilder which might not always be doable because of the way relations are mapped or how the query is done. Fortunately, Blaze Persistence entity views also allow to apply a setting on a relation of the query root via EntityViewManager.applySetting(EntityViewSetting setting, CriteriaBuilder criteriaBuilder, String entityViewRoot).

Let’s consider the following example.

@EntityView(Cat.class)
interface CatView {
    @IdMapping
    Long getId();

    String getName();
}

Mapping this entity view on e.g. the father relation like

CriteriaBuilderFactory criteriaBuilderFactory = ...;
EntityViewManager entityViewManager = ...;

CriteriaBuilder<Cat> criteriaBuilder = criteriaBuilderFactory.create(Cat.class);
criteriaBuilder.where("father").isNotNull();

List<CatView> list = entityViewManager.applySetting(
    EntityViewSetting.create(CatView.class),
    criteriaBuilder,
    "father"
);

This will map all fathers of cats to the CatView and roughly produce a query like the following

SELECT father_1.id, father_1.name
FROM Cat cat
LEFT JOIN cat.father father_1
WHERE father_1 IS NOT NULL

6.4. Fetching a data subset

Although an entity view already represents a significantly state-reduced version of an entity, it might still be desirable to reduce the state even further. Imagine an UI that allows to configure visible columns in a table where entity view data is presented. Wouldn’t it be great if data that isn’t shown is not fetched at all? On a EntityViewSetting you can specify entity view attributes that you would like to fetch via the fetch(String path) method.

As soon as you call it once, you will have to specify all attributes that you want to be fetched.

Here a simple example:

@EntityView(Cat.class)
interface CatView {
    @IdMapping
    Long getId();

    String getName();

    PersonView getOwner();

    @EntityView(Person.class)
    interface PersonView {

        @IdMapping
        Long getId();

        String getName();

        @Mapping("cats.id")
        Set<Long> getCatIds();
    }
}

Normally, when you fetch this, you will get a query like the following

SELECT cat.id, cat.name, owner_1.id, owner_1.name, cats_1.id
FROM Cat cat
JOIN cat.owner owner_1
LEFT JOIN owner_1.cats cats_1

But when you use the following settings instead

CriteriaBuilderFactory criteriaBuilderFactory = ...;
EntityViewManager entityViewManager = ...;
CriteriaBuilder<Cat> criteriaBuilder = ...;
EntityViewSetting<CatView, CriteriaBuilder<CatView>> setting;
setting = EntityViewSetting.create(CatView.class);
setting.fetch("name");
setting.fetch("owner.name");
List<CatView> list = entityViewManager.applySetting(setting, criteriaBuilder);

You will instead only get the mentioned attributes and the identifiers by which the objects are reachable

SELECT cat.id, cat.name, owner_1.id, owner_1.name, NULL
FROM Cat cat
JOIN cat.owner owner_1

Even the join was omitted because of this change. You still get the same CatView objects returned, but the getOwner().getCatIds() is simply empty.

7. Updatable Entity Views

Updatable entity views represent DTOs for the write concern of an application. Updatable entity views are like a normal entity views, except that changes to attributes are tracked and can be inspected through the Change Model API or flushed to the backing data store.

Updatable entity views are also a lot like normal entities and can be thought of being similar to what is sometimes referred to as sub-entities. The main idea is to model use-case specific representations with a limited scope of attributes that can change. Usually, when using an entity type, many more attributes are exposed as being changable to the consumer of the type, although they might not even need to be always changeable. Updatable entity views allow for perfect reuse of attribute declarations thanks to it’s use of interfaces but also brings a lot more to the table than using plain entities.

Apart from a concept for updating existing objects, Blaze Persistence also has a notion for creating new objects. With only JPA, a developer is often left with some open question like e.g. how to implement equals-hashCode for entities. Thanks to the first class notion of creatable entity views, this question and others can be easily answered as discussed below.

7.1. Update mapping

To declare an entity view as being updatable, it is required to additionally annotate it with @UpdatableEntityView. By default an updatable entity view will do full updates i.e. always update all (owned) updatable attributes if at least one (owned) attribute is dirty. Owned attributes are the ones that belong to the backing entity type like e.g. basic typed attributes. Collections or inverse attributes aren’t owned in this sense and are thus independent. This behavior can be configured by setting the mode attribute on the @UpdatableEntityView:

  • PARTIAL - The mode will only flush values of actually changed attributes

  • LAZY - The default, will flush all updatable values if at least one attribute is dirty

  • FULL - Always flushes all updatable attributes, regardless of dirtyness

The flushing, by default, is done by executing JPQL DML statements, but can be configured to use entities instead by setting the strategy attribute on the @UpdatableEntityView:

  • QUERY - The default, will flush changes by executing JPQL DML statements. Falling back to entity flushing if necessary

  • ENTITY - Will flush changes by loading the dirty entity graph and applying changes onto it

7.2. Create mapping

To declare an entity view as being creatable, it is required to additionally annotate it with @CreatableEntityView. Note that updatable entity views for embeddable types are implicitly also creatable, yet the @CreatableEntityView annotation can still be applied for further configuration. By default, a creatable entity view is validated against the backing model regarding it’s persistability i.e. it is checked if an instance could be successfully persisted regarding the non-null constraints of the entity model. This allows to catch errors early that occur when adding new attributes to the entity model but forgetting to do so in the entity view. The validation can be disabled by setting the validatePersistability attribute on the @CreatableEntityView to false but can also be controlled in a fine grained manner by excluding specific entity attributes from the validation via the excludedEntityAttributes attribute. The latter is useful for attributes that are known to be set on the entity model through inverse relationship, entity listeners or entity view listeners.

Creatable views are converted to their context specific declaration type after persisting. This mean that if a creatable entity view is used as value for an attribute of an updatable entity view, the instance is replaced by an equivalent instance of the type that is declared for the attribute. Consider the following example model for illustration.

@UpdatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    @UpdatableMapping(cascade = CascadeType.PERSIST)
    OwnerView getOwner();
    void setOwner(OwnerView owner);
}
@EntityView(Person.class)
interface OwnerView {
    @IdMapping
    Long getId();

    String getName();
}
@CreatableEntityView
@EntityView(Person.class)
interface OwnerCreateView extends OwnerView {
    void setName(String name);
}

When flushing an instance of the type CatUpdateView that contains an owner of the creatable entity view type OwnerCreateView the following happens

  1. A Person entity is created with the defined properties of the OwnerCreateView

  2. The Person entity is persisted via EntityManager.persist()

  3. The generated identifier is set on the OwnerCreateView object

  4. The OwnerCreateView object is converted to the context specific declared type OwnerView

  5. The OwnerCreateView object is replaced by the OwnerView object

The same replacing happens for creatable entity views that are contained in a collection, thus developers don’t need to think about possible problems related to primary key based equals-hashCode implementations. Since the object is properly replaced, the assignment of a generated primary key, which would change the object regarding equals-hashCode, is not problematic. Still, the object can safely make use of the primary key based equals-hashCode implementation that is generated for all entity views by default.

7.3. API usage

An updatable as well as an creatable entity view is flushed by invoking EntityViewManager.save(EntityManager em, Object view) or one of its variants and will flush changes according to the flush strategy and mode. Changes to collections are flushed depending on the collection mapping, flush strategy and JPA provider support.

The query flush strategy requires support for collection DML queries which is currently only provided with Hibernate.

If the provider doesn’t support collection DML, or you choose to do entity flushing, the owning entity is loaded and changes are applied to that. For collections that are not owned by the containing entity i.e. use a mappedBy, changes will be applied by creating/updating/deleting the target entities.

INFO: Blaze Persistence will manage inverse relationships automatically and even update the parent object in the child object if mapped.

Creatable entity views are constructed via EntityViewManager.create(Class type) and always result in a persist when being flushed directly or through an updatable attribute having the CascadeType.PERSIST enabled.

Deletion of entities through view types works either by supplying an existing view object to EntityViewManager.remove(EntityManager em, Object view) or by entity id via EntityViewManager.remove(EntityManager em, Class viewType, Object id).

The big advantage of using the remove APIs is that Blaze Persistence will reduce the amount of queries significantly, especially if the a view object is passed that already provides information about the object graph.

7.4. Lifecycle and listeners

An entity view, similar to a JPA entity, also has something like a lifecycle, though within entity views, the states correspond to different entity view java types, rather than a transaction state. There are essentially 3 different kinds of entity views:

new

An instance of a creatable entity view type(@CreatableEntityView) that is created via EntityViewManager.create(Class). After flushing of such an instance, the instance transitions to the updatable state if the entity view java type is also updatable(@UpdatableEntityView) otherwise to the read-only state. If it is used within an updatable view, it is then converted to the context specific type which replaces the creatable entity view instance.

read-only

A normal entity view without updatable or creatable configuration(@UpdatableEntityView, @CreatableEntityView).

updatable

An entity view with updatable configuration(@UpdatableEntityView).

load

An entity view is loaded by applying an EntityViewSetting to a CriteriaBuilder which also happens implicitly when using EntityViewManager.find(). Another way to load is to get a reference for an entity view via EntityViewManager.getReference() but note that this does not invoke the @PostLoad lifecycle listener.

remove

Removing is done explicitly by calling EntityViewManager.remove() or implicitly when delete cascading or orphan removal is activated.

create

Creating of entity view instances is done by calling EntityViewManager.create().

save

Flushing/Updating happens when invoking EntityViewManager.save()/EntityViewManager.saveTo()/EntityViewManager.saveWith()/EntityViewManager.saveWithTo() or EntityViewManager.saveFull()/EntityViewManager.saveFullTo()/EntityViewManager.saveFullWith()/EntityViewManager.saveFullWithTo() as well as implicitly for CascadeType.UPDATE enabled attributes.

convert

Conversion happens when calling EntityViewManager.convert() which implicitly happens for creatable entity views within a context after persisting.

For most of the operations it is possible to register a listener which is invoked before or after an operation. The listeners can react to specific events but in some cases also alter the state of the corresponding object.

A listener can be defined within an entity view class but within a class hierarchy there may only be one listener of a kind. If multiple listeners of a single kind from e.g. super interfaces are inherited, the entity view type must declare a listener to disambiguate the situation. The listener then can invoke other parent listener methods or skip them.

Most listeners can be defined for a specific update or remove operation to react to change events in a particular manner for a specific use case, but it is also possible to register listeners globally. The globally registered listeners can be used to implement cross cutting concerns like soft-deletion, auditing, etc. Global listeners are registered via one of the EntityViewConfiguration.addEntityViewListener methods or discovered in a CDI or Spring environment if the classes are annotated with @EntityViewListener or @EntityViewListeners.

A global listener must implement one or more of the following interfaces:

7.4.1. Post create listener

Within an entity view type a concrete method annotated with @PostCreate is considered to be a post create listener. It may optionally define a parameter of the type EntityViewManager and must have a return type of void.

Such a listener is usually used for creatable entity view types to setup default values.

enum LifeState {
    ALIVE,
    DEAD;
}

@CreatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    String getName();
    void setName(String name);

    LifeState getState();
    void setState(LifeState state);

    @PostCreate
    default void init() {
        setState(LifeState.ALIVE);
    }
}

7.4.2. Post convert listener

Within an entity view type a concrete method annotated with @PostConvert is considered to be a post convert listener. It may optionally define a parameter of the type EntityViewManager and of the type Object for the source view and must have a return type of void.

Such a listener is usually used to transfer transient state from a previous object.

@CreatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    String getName();
    void setName(String name);

    @MappingParameter("source")
    CatUpdateView getSource();
    void setSource(CatUpdateView source);

    @PostConvert
    default void postConvert(Object source) {
        setSource((CatUpdateView) source);
    }
}

7.4.3. Post load listener

Within an entity view type a concrete method annotated with @PostLoad is considered to be a post load listener. It may optionally define a parameter of the type EntityViewManager and must have a return type of void.

Such a listener is usually used for computing values based on the entity view state.

@EntityView(Cat.class)
abstract class CatUpdateView {

    private String shortName;

    @IdMapping
    public abstract Long getId();

    public abstract String getName();

    public String getShortName() {
        return shortName;
    }

    @PostLoad
    void init() {
        this.shortName = getName().substring(0, 10) + "...";
    }
}

7.4.4. Pre remove listener

Within an entity view type a concrete method annotated with @PreRemove is considered to be a pre remove listener. It may optionally define a parameter of the type EntityViewManager and of the type EntityManager and may have a return type of boolean or void. When the method returns true, the element is going to be removed. By returning false the removal can be cancelled. When the removal is cancelled, the view will be saved by calling EntityViewManager.save.

Such a listener is usually used for implementing soft-deletion by cancelling the actual removal and instead doing an update.

enum LifeState {
    ALIVE,
    DEAD;
}

@UpdatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    String getName();
    void setName(String name);

    LifeState getState();
    void setState(LifeState state);

    @PreRemove
    default boolean preRemove() {
        setState(LifeState.DEAD);
        return false;
    }
}

Additional listeners can be attached for an save or remove operation by using the EntityViewManager.saveWith(EntityManager em, Object view) or EntityViewManager.removeWith(EntityManager em, Object view) methods.

CatUpdateView view = //...
entityViewManager.removeWith(em, view)
    .onPreRemove(CatUpdateView.class, new PreRemoveListener<CatUpdateView>() {
        public boolean preRemove(EntityViewManager evm, EntityManager em, CatUpdateView view) {
            view.setState(LifeState.DEAD);
            return false;
        }
    })
    .flush();
}

7.4.5. Post remove listener

Within an entity view type a concrete method annotated with @PostRemove is considered to be a post remove listener. It may optionally define a parameter of the type EntityViewManager and of the type EntityManager and must have a return type of void.

Such a listener is usually used for doing cleanups on e.g. external systems.

@UpdatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    String getName();
    void setName(String name);

    @PostRemove
    default void postRemove(EntityManager em) {
        em.persist(new ClearResourcesJob(getId()));
    }
}

Additional listeners can be attached for an save or remove operation by using the EntityViewManager.saveWith(EntityManager em, Object view) or EntityViewManager.removeWith(EntityManager em, Object view) methods.

CatUpdateView view = //...
entityViewManager.removeWith(em, view)
    .onPostRemove(CatUpdateView.class, new PostRemoveListener<CatUpdateView>() {
        public void postRemove(EntityViewManager evm, EntityManager em, CatUpdateView view) {
            em.persist(new ClearResourcesJob(view.getId()));
        }
    })
    .flush();
}

7.4.6. Pre persist listener

Within an entity view type a concrete method annotated with @PrePersist is considered to be a pre persist listener. It may optionally define parameters of the type EntityViewManager, of the type EntityManager or the entity type of the entity view and must have a return type of void.

Such a listener is usually used for implementing setting default values or unmapped entity attributes that should only be set during creation.

@CreatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    String getName();
    void setName(String name);

    Calendar getCreationDate();
    void setCreationDate(Calendar creationDate);

    @PrePersist
    default void prePersist(Cat c) {
        c.setAge(1);
        setCreationDate(Calendar.getInstance());
    }
}

Additional listeners can be attached for an save operation by using the EntityViewManager.saveWith(EntityManager em, Object view) method.

CatUpdateView view = //...
entityViewManager.saveWith(em, view)
    .onPrePersist(CatUpdateView.class, new PrePersistListener<CatUpdateView>() {
        public void prePersist(EntityViewManager evm, EntityManager em, CatUpdateView view) {
            view.setCreationDate(Calendar.getInstance());
        }
    })
    .flush();
}

Next to this entity view only pre persist listener there is also a variation of the listener type that allows to update the entity object. There is no annotation that can be used to create such a listener method within the entity view type as that would expose the JPA model to a method signature.

CatUpdateView view = //...
entityViewManager.saveWith(em, view)
    .onPrePersist(CatUpdateView.class, Cat.class, new PrePersistEntityListener<CatUpdateView, Cat>() {
        public void prePersist(EntityViewManager evm, EntityManager em, CatUpdateView view, Cat entity) {
            entity.setCreationDate(Calendar.getInstance());
        }
    })
    .flush();
}

Such a listener is usually used for setting attributes on an entity that shouldn’t be exposed through an entity view like e.g. a tenant.

7.4.7. Post persist listener

Within an entity view type a concrete method annotated with @PostPersist is considered to be a post persist listener. It may optionally define parameters of the type EntityViewManager, of the type EntityManager or the entity type of the entity view and must have a return type of void.

Such a listener is usually used for calling external systems.

@CreatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    String getName();
    void setName(String name);

    @PostPersist
    default void postPersist(EntityManager em) {
        em.persist(new ReplicationJob(view.getId()));
    }
}

Additional listeners can be attached for an save operation by using the EntityViewManager.saveWith(EntityManager em, Object view) method.

CatUpdateView view = //...
entityViewManager.saveWith(em, view)
    .onPostPersist(CatUpdateView.class, new PostPersistListener<CatUpdateView>() {
        public void postPersist(EntityViewManager evm, EntityManager em, CatUpdateView view) {
            em.persist(new ReplicationJob(view.getId()));
        }
    })
    .flush();
}

7.4.8. Pre update listener

Within an entity view type a concrete method annotated with @PreUpdate is considered to be a pre update listener. It may optionally define a parameter of the type EntityViewManager and of the type EntityManager and must have a return type of void.

Such a listener is usually used for implementing automatic setting of e.g. modification dates.

@UpdatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    String getName();
    void setName(String name);

    Calendar getModificationDate();
    void setModificationDate(Calendar creationDate);

    @PreUpdate
    default void preUpdate() {
        setModificationDate(Calendar.getInstance());
    }
}

Additional listeners can be attached for an save operation by using the EntityViewManager.saveWith(EntityManager em, Object view) method.

CatUpdateView view = //...
entityViewManager.saveWith(em, view)
    .onPreUpdate(CatUpdateView.class, new PreUpdateListener<CatUpdateView>() {
        public void preUpdate(EntityViewManager evm, EntityManager em, CatUpdateView view) {
            view.setState(LifeState.DEAD);
        }
    })
    .flush();
}

7.4.9. Post update listener

Within an entity view type a concrete method annotated with @PostUpdate is considered to be a post update listener. It may optionally define a parameter of the type EntityViewManager and of the type EntityManager and must have a return type of void.

Such a listener is usually used for calling external systems.

@UpdatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    String getName();
    void setName(String name);

    @PostUpdate
    default void postUpdate(EntityManager em) {
        em.persist(new ReplicationJob(view.getId()));
    }
}

Additional listeners can be attached for an save operation by using the EntityViewManager.saveWith(EntityManager em, Object view) method.

CatUpdateView view = //...
entityViewManager.saveWith(em, view)
    .onPostUpdate(CatUpdateView.class, new PostUpdateListener<CatUpdateView>() {
        public void postUpdate(EntityViewManager evm, EntityManager em, CatUpdateView view) {
            em.persist(new ReplicationJob(view.getId()));
        }
    })
    .flush();
}

7.4.10. Post commit listener

Within an entity view type a concrete method annotated with @PostCommit is considered to be a post commit listener. It may optionally define parameters of the type EntityViewManager, of the type EntityManager or the type ViewTransition and must have a return type of void. The @PostCommit annotation can define the view transitions(PERSIST, UPDATE, REMOVE) for which the listener should be invoked.

Such a listener is usually used for calling external systems.

@UpdatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    String getName();
    void setName(String name);

    @PostCommit(transitions = ViewTransition.UPDATE)
    default void postCommit(EntityManager em) {
        em.persist(new ReplicationJob(view.getId()));
    }
}

Additional listeners can be attached for an save operation by using the EntityViewManager.saveWith(EntityManager em, Object view) method.

CatUpdateView view = //...
entityViewManager.saveWith(em, view)
    .onPostCommit(CatUpdateView.class, new PostCommitListener<CatUpdateView>() {
        public void postCommit(EntityViewManager evm, EntityManager em, CatUpdateView view, ViewTransition transition) {
            em.persist(new ReplicationJob(view.getId()));
        }
    })
    .flush();
}

There are various short-had variants to register post commit listeners for specific view transitions like e.g. onPostCommitPersist().

7.4.11. Post rollback listener

Within an entity view type a concrete method annotated with @PostRollback is considered to be a post rollback listener. It may optionally define parameters of the type EntityViewManager, of the type EntityManager or the type ViewTransition and must have a return type of void. The @PostRollback annotation can define the view transitions(PERSIST, UPDATE, REMOVE) for which the listener should be invoked.

Such a listener is usually used for calling external systems or resetting state.

@UpdatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    String getName();
    void setName(String name);

    boolean getDone();
    void setDone(boolean done);

    @PostRollback(transitions = ViewTransition.UPDATE)
    default void postRollback() {
        setDone(false);
    }
}

Additional listeners can be attached for an save operation by using the EntityViewManager.saveWith(EntityManager em, Object view) method.

CatUpdateView view = //...
entityViewManager.saveWith(em, view)
    .onPostRollback(CatUpdateView.class, new PostRollbackListener<CatUpdateView>() {
        public void postRollback(EntityViewManager evm, EntityManager em, CatUpdateView view, ViewTransition transition) {
            view.setDone(false);
        }
    })
    .flush();
}

There are various short-had variants to register post rollback listeners for specific view transitions like e.g. onPostRollbackPersist().

7.5. Attribute mappings

When an entity view has @UpdatableEntityView annotated, every attribute for which a setter method exists, is considered to be updatable. For an attribute to be updatable means that changes done to the attribute of an entity view, can be flushed to the entity attribute they map to. There is also a notion of mutable attributes which means that an attribute is updatable and/or the type of the attribute’s value might be mutable.

An unknown type is mutable by default and needs to be configured by registering a basic user type. Entity view types are only considered being mutable if they are updatable(@UpdatableEntityView) or creatable(@CreatableEntityView). Entity types are always considered to be mutable.

The mappings for updatable attributes must follow some rules

  • May not use complex expressions like arithmetic or functions

  • May not access elements or attributes of elements through a collection e.g. kittens.name

The general understanding is that mappings should be bi-directional i.e. it should be possible to map a value back to a specific entity attribute.

To prevent an attribute being considered updatable, it can be annotated with @UpdatableMapping(updatable = false). Sometimes, it’s also useful to annotate plural attributes i.e. collection attributes with @UpdatableMapping(updatable = true) when a setter is inappropriate.

Note that updatable and creatable entity view types require an id mapping to work properly, which is validated during the building of the metamodel. The getters and setters of abstract entity view classes may use the protected or default visibility setting which allows to encapsulate the access to these attributes properly.

7.5.1. Basic type mappings

Singular attributes with a basic type(all types except entity view types, entity types or collection types) do not have a nested domain structure since they are basic. Values of such types usually change by setting a different value, though there are some mutable types as well. Basic types in general are handled by registered basic user types and define the necessary means to safely handle values of such types.

Values set for a basic type entity view attribute are only flushed to the entity attribute it refers to, if the entity view attribute is updatable. This means that even if the type is mutable, a basic type attribute is never considered to be updatable as long as there is no setter or an explicit @UpdatableMapping(updatable = true) present. If a type is immutable, an attribute with such a type obviously needs a setter to be considered updatable as there would otherwise be no way to change a value.

@UpdatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    String getName();
    void setName(String name);
}

Changes made via calls to e.g. setName() can be flushed later in a different persistence context. The following shows a simple example

// Load the updatable entity view
CatUpdateView view = entityViewManager.find(entityManager, CatUpdateView.class, catId);

// Update the name of the view
view.setName("newName");

// Flush the changes to the persistence context
eventityViewManager.save(entityManager, view);

Depending on the configured flush strategy, this will either load the Cat entity and apply changes to it or create an update query that set’s the updatable attributes.

UPDATE Cat cat
SET cat.name = :name
WHERE cat.id = :id

7.5.2. Subview mappings

Just like *ToOne relationships can be mapped in entities, it is possible to map these relationships as subviews.

In general, Blaze Persistence distinguishes between two concepts regarding updatability

  • Updatability of the relationship role i.e. the attribute owner or more specifically the owner_id column

  • Updatability of the relation type represented by the entity view PersonView or more specifically the row in the person table

The following example illustrates a case where the relation type PersonView is not updatable, but the relationship represented by the attribute owner is updatable.

@EntityView(Person.class)
interface PersonView {
    @IdMapping
    Long getId();

    String getName();
}

@UpdatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    String getName();

    PersonView getOwner();
    void setOwner(PersonView owner);
}

Even if the PersonView had a setName() method, changes done to that attribute would not be flushed, since PersonView is not updatable(@UpdatableEntityView).

Having only an updatable relationship role is very common, because it is rarely necessary to do cascading updates. It is so common, that by default, the subview types used for owned *ToOne relationships are not allowed to be updatable i.e. annotated with @UpdatableEntityView as that would break the idea of updatable views per use-case.

An owned ToOne relationship is a link to an existing object which shouldn’t normally be altered as part of the object owning the *ToOne relationship. To illustrate this, let’s consider the entities Cat and Person. A Cat might have a @ManyToOne relationship called *owner that refers to Person. When considering the use case of editing a cat, one would normally expect to be able to change attributes like the name or age, maybe even the owner link, but never any attributes of the linked owner. Changing attributes of linked objects is usually a separate use case which deserves a separate model.

One can always convert from one entity view model to another with EntityViewManager.convert(Class, Object, ConvertOption…​), so it is not necessary to reload the data to be able to initiate another use case. The important part is that the altering of *ToOne linked objects is another use case and is by default not allowed to be done within an updatable entity view.

An inverse OneToOne relationship is not owned and thus not linked which is why it is possible to have an updatable subview type for these relationships. As there might be models out there, that do not fit this requirement, it is possible to disable this strict check via @AllowUpdatableEntityViews on a per-attribute level or globally via the configuration property.

Note that it is also possible to just make the entity view type PersonView updatable(annotate @UpdatableEntityView) without the setter setOwner(). That way, the relationship role wouldn’t be allowed to change, but the changes to the underlying Person would be cascaded.

When the subview type is updatable(@UpdatableEntityView), updates are by default cascaded. If the subview type is also creatable(@CreatableEntityView), persists are also cascaded. To disable or fine tune this behavior, it is possible to annotate the attribute getter with @UpdatableMapping and specify the cascade attribute. Apart from defining which CascadeType is enabled, it is also possible to restrict the allowed subtypes via the attributes subtypes, persistSubtypes and updateSubtypes. By default, instances of the declared type i.e. the compile time attribute type, are allowed to be set as attribute values. Subtypes that are non-updatable and non-creatable are also allowed. If the attribute defines UPDATE cascading or the declared type is updatable(@UpdatableEntityView), all updatable subtypes that don’t introduce a cycle are also allowed. If the attribute defines PERSIST cascading or the declared type is creatable(@CreatableEntityView), all creatable subtypes that don’t introduce a cycle are also allowed.

When using immutable/non-updatable subview types the method EntityViewManager.getReference(Class viewType, Object id) might come in handy. This method allows to retrieve an instance of the given view type having the defined identifier. This is very useful for cases when just a relationship role like e.g. owner should be set without the need to query PersonView objects. A common use case might be to set the tenant which owns an object. There is no need to query the tenant as the information is unnecessary for simply setting the relationship role, but the tenant’s identity is known.

To be able to encapsulate the creation of subviews or the access to references for subviews it is recommended to make use of the special EntityViewManager getter method. The idea is to define an abstract getter method with protected or default visibility returning an EntityViewManager. Methods that create subviews or want a reference to a subview by id can then invoke the getter to get access to the EntityViewManager.

The following encapsulated updatable entity views illustrate the usage:

@EntityView(Person.class)
interface PersonView {
    @IdMapping
    Long getId();

    String getName();
}

@UpdatableEntityView
@EntityView(Cat.class)
abstract class CatUpdateView {
    @IdMapping
    public abstract Long getId();

    public abstract String getName();

    @Mapping("owner")
    protected abstract PersonView getOwnerInternal();
    protected abstract void setOwnerInternal(PersonView owner);
    protected abstract EntityViewManager evm();

    public PersonView getOwner() {
        return getOwnerInternal();
    }
    public void setOwner(PersonView owner) {
        setOwnerInternal(evm().convert(PersonView.class, owner));
    }
    public void setOwnerId(Long id) {
        setOwnerInternal(evm().getReference(PersonView.class, id));
    }
}

7.5.3. Flat view mappings

Updatable flat view mappings are currently only supported for embeddable types. An updatable flat view type is also always creatable. Flat views are always flushed as whole objects, which means that an updatable flat view should always at least map all attributes as read-only. Read-only i.e. non-updatable attributes are passed-through to the embeddable object when recreating it.

Apart from that, a flat view is just like a normal subview.

7.5.4. Subquery & parameter mappings

Since subqueries and parameter mappings aren’t bidirectional, attributes using these kinds of mappings are never considered to be updatable.

7.5.5. Entity mappings

Entity types are similar to subview types as they have an identity and are specially handled when loading and merging data. Since entity types are mutable by design, PERSIST and UPDATE cascading are by default enabled for attributes that use entity types. The cascading can be overridden by defining the cascade type via a @UpdatableMapping annotation on the attribute. Note that the handling of entity types can be fine tuned by registering a basic user type.

@UpdatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    @UpdatableMapping(cascade = { CascadeType.UPDATE }) (1)
    Cat getFather();
    void setFather(Cat father);
}
1 Defines that only updates are cascaded. Unknown i.e. new Cat instances aren’t persisted

Changes that are done via setFather() will update the father attribute in the entity model when flushed. If query flushing is configured, a query like the following will be generated when updating the father relation.

UPDATE Cat cat
SET cat.father = :father
WHERE cat.id = :id
Since dirty tracking heavily relies on the equals and hashCode implementations, we recommend you implement equals and hashCode of your entity types based on the primary key.

7.5.6. Collection mappings

Updatable collection mappings must be simple paths referring to a collection of the backing entity type. Paths to a nested collection like e.g. owner.kittens are not allowed. Currently, a collection attribute is considered to be updatable if a setter for the attribute exists, or @UpdatableMapping is declared on the getter method of an attribute.

At this point, collections can not be remapped automatically yet, so you have to use the same collection type as in the entity model.
@UpdatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    Set<Cat> getKittens();
    void setKittens(Set<Cat> kittens);
}

Any modification done to a collection

CatUpdateView view = ...;

// Update the view
Cat newKitten = entityManager.find(Cat.class, 2L);
view.getKittens().add(newKitten);

// Flush the changes to the persistence context
entityViewManager.save(entityManager, view);

Will be applied on the collection of an entity reference during EntityViewManager.save() as if the following was done.

CatUpdateView view = ...;
// Actually a query that loads the graph being dirty is issued
Cat cat = entityManager.find(Cat.class, view.getId());

cat.getKittens().add(newKitten);

Since the kittens collection is dirty i.e. a new kitten was added and the collection is owned by the Cat entity, the collection will be loaded along with the Cat when using the entity flush strategy.

WIth query flushing, instead of loading and adding, the new kitten will be added via a collection DML statement

INSERT INTO Cat.kittens(id, kittens.id)
SELECT :ownerId, :kittenId
FROM Integer(1 VALUES)

If kittens were an inverse collection, it wouldn’t need loading during flushing even with the entity flush strategy as adding the new kitten would be a matter of issuing an update query or persisting an entity.

7.5.7. Inverse mappings

Changes to inverse relations like OneToOne’s and *ToMany collections are flushed by persisting, updating or removing the inverse relation objects. There is no special mapping required. If the entity model defines that an attribute is an inverse mapping by specifying a mappedBy, updatable entity view attributes mapping to such attributes automatically discover the mappedBy configuration and will cause the attribute being maintained by managing inverse relation objects.

There are several strategies that can be configured to handle the removal of elements via the removeStrategy attribute of @MappingInverse

  • IGNORE - Ignores elements that have been removed i.e. does not maintain the relationship automatically.

  • REMOVE - Removes the inverse relation object when determined to be removed from the inverse relationship.

  • SET_NULL - The default. Sets the mappedBy attribute to NULL on the inverse relation object when found to be removed from the inverse relationship.

@UpdatableEntityView
@EntityView(Person.class)
interface PersonUpdateView {
    @IdMapping
    Long getId();

    // mappedBy = "owner"
    @MappingInverse(removeStrategy = InverseRemoveStrategy.REMOVE)
    Set<Cat> getKittens();
    void setKittens(Set<Cat> kittens);
}

A modification of the kittens collection…​

PersonUpdateView view = ...;

// Update the view
view.getKittens().remove(someKitten);

// Flush the changes to the persistence context
entityViewManager.save(entityManager, view);

will cause the Cat someKitten to be removed.

DELETE Cat c WHERE c.id = :someKittenId

If the SET_NULL strategy were used, the owner would be set to NULL

UPDATE Cat c SET owner = NULL WHERE c.id = :someKittenId

7.5.8. Correlated mappings

The only difference between correlated mappings and other mappings is that there is no relationship that is updated. Cascading will happen the same way for entities, updatable and creatable entity views.

Although there is no relationship to update for correlation mappings, adding or removing elements to a correlated attribute with updatable types, will be constrained by updatability like normal mappings. If a correlated attribute isn’t updatable by means of @UpdatableMapping(updatable = false), setting a value or adding/removing to a collection will fail.

Consider the following simple example.

@UpdatableEntityView
@EntityView(Person.class)
interface PersonView {
    @IdMapping
    Long getId();

    String getName();
    void setName(String name);
}

@UpdatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    String getName();

    @MappingCorrelatedSimple(
        correlated = Person.class,
        correlationBasis = "owner.id",
        correlationExpression = "id IN correlationKey"
    )
    PersonView getOwner();
    void setOwner(PersonView owner);
}

When changing the name of a correlated owner

CatUpdateView view = ...;

// Update the view
view.getOwner().setName("newName");

// Flush the changes to the persistence context
entityViewManager.save(entityManager, view);

The update of the CatUpdateView will cascade to the correlated object.

UPDATE Person p SET p.name = :name WHERE p.id = :personId

Note that a future version might allow to treat correlated mappings as custom inverse mappings.

7.5.9. Updatable mapping defaults

The default mappings follow the concept of what you see is what you get. If the type of an attribute is a @UpdatableEntityView, changes done to that object will be flushed during an update. Unsupported configurations will fail during boot.

Basic types are either simple value types like Integer, String or JPA managed types i.e. entities or embeddables. Unless an immutable user type was registered via the BasicUserType SPI, a basic type is by default considered to be mutable. A JPA entity type has identity which makes updatability independent from update cascading. Types without identity are either both updatable and update cascaded or immutable.

An attribute does update cascading if changes done to an instance reached through that attribute are flushed during update. An attribute does persist cascading if a new object reached through that attribute is persisted during update.

The following tables should help illustrate the defaults and are also a good reference.

Basic simple type Relationship updatable Update cascaded Persist cascaded
@EntityView(Entity.class)
interface View {
    String getName();
}

no

no

no

@EntityView(Entity.class)
interface View {
    String getName();
    void setName(String name);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    String getName();
    void setName(String name);
}

yes

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    java.util.Date getDate(); // Mutable
}

yes

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    String getName();
    void setName(String name);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    java.util.Date getDate(); // Mutable
}

no

no

no

Using a JPA embeddable type Embeddable

Basic JPA embeddable type Relationship updatable Update cascaded Persist cascaded
@EntityView(Entity.class)
interface View {
    Embeddable getEmbeddable();
}

no

no

no

@EntityView(Entity.class)
interface View {
    Embeddable getEmbeddable();
    void setEmbeddable(Embeddable embeddable);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    Embeddable getEmbeddable();
}

yes

yes

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    Embeddable getEmbeddable();
    void setEmbeddable(Embeddable embeddable);
}

yes

yes

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    Embeddable getEmbeddable();
    void setEmbeddable(Embeddable embeddable);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    Embeddable getEmbeddable();
}

no

no

no

Using a JPA entity type Entity2

Basic JPA entity type Relationship updatable Update cascaded Persist cascaded
@EntityView(Entity.class)
interface View {
    Entity2 getEntity2();
}

no

no

no

@EntityView(Entity.class)
interface View {
    Entity2 getEntity2();
    void setEntity2(Entity2 entity2);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    Entity2 getEntity2();
}

no

yes

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    Entity2 getEntity2();
    void setEntity2(Entity2 entity2);
}

yes

yes

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    Entity2 getEntity2();
    void setEntity2(Entity2 entity2);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    Entity2 getEntity2();
}

no

no

no

Using a read-only entity view type View2 that looks like

@EntityView(Entity2.class)
interface View2 {
    @IdMapping
    Integer getId();
    String getName();
    void setName(String name);
}

results in the following default behavior

View type Relationship updatable Update cascaded Persist cascaded
@EntityView(Entity.class)
interface View {
    View2 getView2();
}

no

no

no

@EntityView(Entity.class)
interface View {
    View2 getView2();
    void setView2(View2 view2);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    View2 getView2();
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    View2 getView2();
    void setView2(View2 view2);
}

yes

no [1]


1. If a subtype of View2 that is an updatable view type exists and is set, updates will cascade

no [2]


2. If a subtype of View2 that is a creatable view type exists and is set, the object will be persisted
@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    View2 getView2();
    void setView2(View2 view2);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    View2 getView2();
}

no

no

no

A type that isn’t allowed for whatever reason will not be allowed to be set on the attribute.

Using an updatable entity view type View2 that looks like

@EntityView(Entity2.class)
@UpdatableEntityView
interface View2 {
    @IdMapping
    Integer getId();
    String getName();
    void setName(String name);
}
View type Relationship updatable Update cascaded Persist cascaded
@EntityView(Entity.class)
interface View {
    View2 getView2();
}

no

no

no

@EntityView(Entity.class)
interface View {
    View2 getView2();
    void setView2(View2 view2);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    View2 getView2();
}

no

yes

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    View2 getView2();
    void setView2(View2 view2);
}

yes

yes

no [3]


3. If a subtype of View2 that is a creatable view type exists and is set, the object will be persisted
@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    View2 getView2();
    void setView2(View2 view2);
}

no

yes

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    View2 getView2();
}

no

yes

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(cascade = {})
    View2 getView2();
}

no

no

no

7.5.10. Updatable collection mapping defaults

Collections are different, as they are mutable by default. Since it is rarely necessary to make the relationship updatable, collections aren’t updatable by default just because they are mutable by design. In order for a collection relationship to be considered updatable, it must have a setter, be annotated with @UpdatableMapping(updatable = true) or have an element type that is @CreatableEntityView.

Basic simple type Relationship updatable Update cascaded Persist cascaded
@EntityView(Entity.class)
interface View {
    Set<String> getNames();
}

no

no

no

@EntityView(Entity.class)
interface View {
    Set<String> getNames();
    void setNames(Set<String> names);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    Set<String> getNames();
    void setNames(Set<String> names);
}

yes

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    Set<java.util.Date> getDates(); // Mutable
}

yes

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    Set<String> getNames();
    void setNames(Set<String> names);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    Set<java.util.Date> getDates(); // Mutable
}

no

no

no

Using a JPA embeddable type Embeddable

Basic JPA embeddable type Relationship updatable Update cascaded Persist cascaded
@EntityView(Entity.class)
interface View {
    Set<Embeddable> getEmbeddables();
}

no

no

no

@EntityView(Entity.class)
interface View {
    Set<Embeddable> getEmbeddables();
    void setEmbeddable(Set<Embeddable> set);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    Set<Embeddable> getEmbeddables();
}

yes

yes

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    Set<Embeddable> getEmbeddables();
    void setEmbeddable(Set<Embeddable> set);
}

yes

yes

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    Set<Embeddable> getEmbeddables();
    void setEmbeddable(Set<Embeddable> set);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    Set<Embeddable> getEmbeddables();
}

no

no

no

Using a JPA entity type Entity2

Basic JPA entity type Relationship updatable Update cascaded Persist cascaded
@EntityView(Entity.class)
interface View {
    Set<Entity2> getEntity2();
}

no

no

no

@EntityView(Entity.class)
interface View {
    Set<Entity2> getEntity2();
    void setEntity2(Set<Entity2> entity2);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    Set<Entity2> getEntity2();
}

no

yes

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    Set<Entity2> getEntity2();
    void setEntity2(Set<Entity2> entity2);
}

yes

yes

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    Set<Entity2> getEntity2();
    void setEntity2(Set<Entity2> entity2);
}

no

yes

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    Set<Entity2> getEntity2();
}

no

no

no

Using a read-only entity view type View2 that looks like

@EntityView(Entity2.class)
interface View2 {
    @IdMapping
    Integer getId();
    String getName();
    void setName(String name);
}
View type Relationship updatable Update cascaded Persist cascaded
@EntityView(Entity.class)
interface View {
    Set<View2> getView2();
}

no

no

no

@EntityView(Entity.class)
interface View {
    Set<View2> getView2();
    void setView2(Set<View2> view2);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    Set<View2> getView2();
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    Set<View2> getView2();
    void setView2(Set<View2> view2);
}

yes

no [4]


4. If a subtype of View2 that is an updatable view type exists and is added, updates will cascade

no [5]


5. If a subtype of View2 that is a creatable view type exists and is added, the object will be persisted
@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    Set<View2> getView2();
    void setView2(Set<View2> view2);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    Set<View2> getView2();
}

no

no

no

A type that isn’t allowed for whatever reason will not be allowed to be added on the attribute.

Using an updatable entity view type View2 that looks like

@EntityView(Entity2.class)
@UpdatableEntityView
interface View2 {
    @IdMapping
    Integer getId();
    String getName();
    void setName(String name);
}
View type Relationship updatable Update cascaded Persist cascaded
@EntityView(Entity.class)
interface View {
    Set<View2> getView2();
}

no

no

no

@EntityView(Entity.class)
interface View {
    Set<View2> getView2();
    void setView2(Set<View2> view2);
}

no

no

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    Set<View2> getView2();
}

no

yes

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    Set<View2> getView2();
    void setView2(Set<View2> view2);
}

yes

yes

no [6]


6. If a subtype of View2 that is a creatable view type exists and is added, the object will be persisted
@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    Set<View2> getView2();
    void setView2(Set<View2> view2);
}

no

yes

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(updatable = false)
    Set<View2> getView2();
}

no

yes

no

@EntityView(Entity.class)
@UpdatableEntityView
interface View {
    @UpdatableMapping(cascade = {})
    Set<View2> getView2();
}

no

no

no

7.6. Locking support

Blaze Persistence entity views by default automatically makes use of a version field mapped in the entity type for optimistic locking. This is controlled by the lockMode attribute on the @UpdatableEntityView annotation which by default is set to AUTO.

  • LockMode.AUTO - The default. Uses the version field of the entity type the entity view is referring to for optimistic locking

  • LockMode.OPTIMISTIC - Forces the use of optimistic locking based on the entity version field

  • LockMode.PESSIMISTIC_READ - Acquires a JPA PESSIMISTIC_READ lock when reading the entity view

  • LockMode.PESSIMISTIC_WRITE - Acquires a JPA PESSIMISTIC_WRITE lock when reading the entity view

  • LockMode.NONE - Don’t use any locking even if a version attribute is available

By default, all updatable attributes in an entity view are protected by optimistic locking. This means that if the value of an attribute was changed, the change will be flushed with the optimistic lock condition. Attribute changes that should be excluded from optimistic locking can be annotated with @OptimisticLock(exclude = true) to prevent the optimistic lock condition when only such attributes are changed.

The entity type for which the optimistic lock condition is asserted is called the lock owner. If the entity type of an entity view does not have a version field and the LockMode.AUTO is used, the parent entity view type is considered being the lock owner. If the parent has no version field, it’s parent is considered and so forth. If no lock owner can be found, no optimistic locking is done.

When specifying a lock mode other than LockMode.AUTO, the entity object for an entity view becomes the lock owner. By annotating @LockOwner on an updatable entity view type, a custom lock owner can be defined.

This is still in development, so not all features might be available yet. Also see https://github.com/Blazebit/blaze-persistence/issues/439 and https://github.com/Blazebit/blaze-persistence/issues/438 for more information.

7.7. Persist and Update cascading

The cascade types defined in Blaze Persistence entity views have different semantics than what JPA offers and should not be mixed up. JPA defines cascade types for logical operations whereas Blaze Persistence entity views defines cascade types for state changes. In a JPA entity, one can define for which operations the changes done to an attribute should be flushed. For example the JPA CascadeType.PERSIST will cause a flush of an attribute’s affected values only if the owning entity is about to be persisted.

Blaze Persistence entity views cascade types define whether a value of an attribute may do a specific state transition. If an attribute defines CascadeType.PERSIST, it means that new objects i.e. the ones created via EntityViewManager.create(), are allowed to be used as values and that these object should be persisted during flushing. Updates done to mutable values of an attribute are only flushed if the CascadeType.UPDATE is enabled.

Normally, the update or persist cascading is enabled for all subtypes of the declared attribute type, but can be restricted by specifying specific subtypes for which to allow updates or persists. This can be done via the subtypes attribute of the @UpdatableMapping or the updateSubtypes or persistSubtypes attributes for the corresponding cascade types.

7.8. Cascading deletes and orphan removal

Delete cascading and orphan removal have the same semantics as in JPA. If you delete an entity A that refers to entity B through an attribute that defines delete cascading, entity B is going to be deleted as well. When removing a reference from entity A to entity B through an attribute that defines orphan removal, entity B is going to be deleted. Orphan removal also implies delete cascading, so entity B is also deleted when deleting entity A.

Most JPA implementations only support cascading deletes and orphan removal for managed entities whereas DML statements for the entity types do not consider this configuration. Blaze Persistence respects the settings all the way and strives to avoid data loading even for the removal by id action done via EntityViewManager.remove(EntityManager, Class, Object). When an entity graph for an entity view type has an arbitrary depth relationship, Blaze Persistence still has to do some entity data loading, but it tries to reduce the executed statements as much as possible.

At some point, DML statements might be grouped together via Updatable CTEs for DBMS that support that. For more information about that, see https://github.com/Blazebit/blaze-persistence/issues/500

To enable delete cascading for an attribute, the CascadeType.DELETE has to be added to the cascade attribute of a @UpdatableMapping

@UpdatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    @UpdatableMapping(cascade = { CascadeType.DELETE })
    Person getOwner();
}

When deleting a Cat like the following

entityViewManager.remove(entityManager, CatUpdateView.class, catId);

the owner is going to be deleted along with the Cat. The delete cascading even works for attributes that are only defined to do delete cascading in the entity. Assuming Cat does not have the arbitrary depth relationship kittens, the removal might trigger the following logical JPQL statements.

DELETE Cat(nickNames) cat WHERE cat.id = :catId
DELETE Cat cat WHERE cat.id = :catId RETURNING owner.id
DELETE Person person WHERE person.id = :ownerId

First, the cascading delete enabled collections like e.g. the nickNames collection is deleted. Then the Cat is deleted and while doing that, the ids of the *ToOne relations with enabled cascading deletes like e.g. the owner’s id are returned. For DBMS not supporting the RETURNING clause for DML statements, a SELECT statement is issued before the DELETE to extract the ids of the *ToOne relations. Finally, the cascading deletes for the *ToOne relations are done e.g. the Person is deleted.

A future strategy for deletion might facilitate temporary tables if the DBMS supports it rather than selecting. For more information see https://github.com/Blazebit/blaze-persistence/issues/220

If the entity type for an updatable entity view uses delete cascading or orphan removal for an attribute, an updatable mapping for that attribute must use these configurations as well. So if the entity type uses delete cascading for the owner of Cat, it would be an error to omit the delete cascading configuration.

@UpdatableEntityView
@EntityView(Cat.class)
interface CatUpdateView {
    @IdMapping
    Long getId();

    @UpdatableMapping(cascade = { }) (1)
    Person getOwner();
}
1 Can’t omit delete cascading if entity attribute uses delete cascading

The same goes for orphan removal and the idea behind this is, that it makes delete cascading and orphan removal configurations visible in every updatable view, thus making it less surprising. It would make no sense to allow disabling delete cascading or orphan removal configurations because then the entity flush strategy would produce different results than the query flush strategy. Obviously the other way around i.e. enabling delete cascading or orphan removal if the entity attribute does not use these configurations, is very valid. Sometimes there are cases where delete cascading or orphan removal shouldn’t be done which means the cascading can’t be configured on the entity type attributes. This where Blaze Persistence entity views show their strength as they allow to control these configurations on a per-use case basis.

7.9. Conversion support

As explained in the beginning, the vision for updatable entity views is to support the modelling of use case specific write models. Although most of the data that is generally updatable is mostly loaded already when starting a use case it is rarely necessary to make it updatable right away. Some use cases might require only a subset of the data to be updatable, while others require a different subset. To support modelling this appropriately it is possible to convert between entity views types.

Imagine the following model for illustration purposes.

@EntityView(Cat.class)
interface KittenView {
    @IdMapping
    Long getId();
}

@EntityView(Cat.class)
interface CatBaseView extends KittenView {
    PersonView getOwner();

    Set<KittenView> getKittens();
}

@UpdatableEntityView
@EntityView(Cat.class)
interface CatOwnerUpdateView extends CatBaseView {
    @UpdatableMapping
    PersonView getOwner();
    void setOwner(PersonView owner);
}

@UpdatableEntityView
@EntityView(Cat.class)
interface CatKittenUpdateView extends CatBaseView {
    @UpdatableMapping
    Set<KittenView> getKittens();
}

When navigating to e.g. a detail UI for a Cat the CatBaseView would be loaded. If the UI had a special action to initiate a transfer to a different owner, doing that action would lead to the conversion of the CatBaseView to the CatOwnerUpdateView.

CatBaseView catBaseView = //...
CatOwnerUpdateView catOwnerUpdate = entityViewManager.convert(CatOwnerUpdateView.class, catBaseView);

After setting the new owner and flushing the changes via EntityViewManager.save(EntityManager, Object) the view is converted back to the base view by invoking EntityViewManager.convert(Class, Object, ConvertOption…​) again.

CatOwnerUpdateView catBaseView = //...
catBaseView = entityViewManager.convert(CatBaseView.class, catBaseView);

When initiating the kitten update action the conversion would be done to CatKittenUpdateView.

Keep in mind that most UIs do not necessarily work this way and that the added complexity might not be beneficial in all cases. Although this mechanism enables a clear separation for use cases, it might just as well be the case, that use cases are so small that it is better to have just a single write model. In some special cases like e.g. when simply changing a status of an object, it might not even be necessary to have an explicit write model. For such cases it is often more appropriate to have a specialized service method or event publishing of some sorts.

Note that internally, the conversion feature is used for converting successfully persisted creatable entity views to their context specific declaration type.

There are of course other possible use cases for this feature like e.g. conversion from a more detailed view to a view containing only a subset of the information.

A very interesting use case is duplicating data. Such a use case requires to partially copy existing data such that it can be saved with a new identity. This is where the control over sub-attributes through EntityViewManager.convertWith(Class, Object, ConvertOption…​) comes in handy. It allows to exclude attributes through ConvertOperationBuilder.excludeAttributes(String... attributes) and also convert specific attributes to a specific subtype with custom convert options through ConvertOperationBuilder.convertAttribute(String attributePath, Class<?> attributeViewClass, ConvertOption... convertOptions).

Imagine the following model for illustration purposes.

@EntityView(Cat.class)
interface KittenView {
    @IdMapping
    Long getId();
}

@EntityView(Cat.class)
interface CatView extends KittenView {
    PersonView getOwner();

    Set<KittenView> getKittens();
}

@CreatableEntityView
@EntityView(Cat.class)
interface CatCloneView extends CatView {
    void setOwner(PersonView owner);
    @UpdatableMapping(cascade = PERSIST)
    Set<KittenView> getKittens();
}

A clone of a cat and kittens could be done by using

CatView catView = //...
catBaseView = entityViewManager.convertWith(CatCloneView.class, catView, ConvertOption.CREATE_NEW)
    .excludeAttribute("id")
    .excludeAttribute("kittens.id")
    .convertAttribute("kittens", CatCloneView.class, ConvertOption.CREATE_NEW)
    .convert();

The resulting object could then be persisted via EntityViewManager.save() which represents a duplicate of the original.

It is also possible to convert an entity to a view

Cat cat = //...
catView = entityViewManager.convert(cat, CatView.class)

8. BasicUserType SPI

Just like JPA providers offer an SPI to make use of custom types for basic values, Blaze Persistence also does. For read models, the type isn’t very important as the JPA provider handles the construction of the values and only provides entity views with object instance. Since write models need to handle change detection and mutability aspects of basic types i.e. non-subview type, the BasicUserType interface SPI is needed.

8.1. Supported types

There are several well known types registered out of the box.

  • boolean, java.lang.Boolean

  • char, java.lang.Character

  • byte, java.lang.Byte

  • short, java.lang.Short

  • int, java.lang.Integer

  • long, java.lang.Long

  • float, java.lang.Float

  • double, java.lang.Double

  • java.lang.String

  • java.math.BigInteger, java.math.BigDecimal

  • java.util.Date, java.sql.Time, java.sql.Date, java.sql.Timestamp

  • java.util.Calendar, java.util.GregorianCalendar

  • java.util.TimeZone, java.lang.Class

  • java.util.UUID, java.net.URL

  • java.util.Locale, java.util.Currency

  • byte[], java.lang.Byte[]

  • char[], java.lang.Character[]

  • java.io.InputStream, java.sql.Blob

  • java.sql.Clob, java.sql.NClob

If found on the classpath, types for the following classes are registered

  • java.time.LocalDate, java.time.LocalDateTime, java.time.LocalTime

  • java.time.OffsetTime, java.time.OffsetDateTime, java.time.ZonedDateTime

  • java.time.Duration, java.time.Instant

  • java.time.MonthDay, java.time.Year, java.time.YearMonth, java.time.Period

  • java.time.ZoneId, java.time.ZoneOffset

8.2. Type support for MULTISET fetching

One of the reasons why a custom basic user type might be desirable is the support for MULTISET fetching. When an entity view attribute defines MULTISET fetching, the basic user types of all types connected through that attribute must support MULTISET fetching. Normally this is not a problem, because well known user types are fully supported. Using embeddable types, custom composite types or JPA converted types in entity views is problematic though, because there is no way Blaze Persistence can figure out how to decompose the attribute to a string representation or construct the type from a string representation. This is where a custom basic user type implementation is desirable.

Depending on whether the type is mutable or not, you can extend the com.blazebit.persistence.view.spi.type.AbstractMutableBasicUserType or com.blazebit.persistence.view.spi.type.ImmutableBasicUserType. An example implementation for an embeddable type composed of 2 attributes might look like the following:

public class QuantityBasicUserType extends com.blazebit.persistence.view.spi.type.AbstractMutableBasicUserType<Quantity> {

    @Override
    public Quantity deepClone(Quantity object) {
        // Clone the object if it is mutable. For immutable types,
        // you can extend com.blazebit.persistence.view.spi.type.ImmutableBasicUserType and don't need this method
        return object == null ? null : new Quantity(object);
    }

    @Override
    public String toStringExpression(String expression) {
        // A JPQL expression that produces a string format which is then parsed
        return "CONCAT(" + expression + ".value, '/', " + expression + ".unit)";
    }

    @Override
    public Quantity fromString(CharSequence sequence) {
        // The CharSequence has the format as defined through toStringExpression
        // Now it must be de-serialized to a Quantity
        String s = sequence.toString();
        String[] parts = s.split("\\/");
        return new Quantity(new BigDecimal(parts[0]), parts[1]);
    }
}

Note that the deepClone method is only relevant for mutable types. Don’t forget to register the basic user type via EntityViewConfiguration.registerBasicUserType(Class type, BasicUserType userType).

EntityViewConfiguration configuration = ...
configuration.registerBasicUserType(Quantity.class, new QuantityBasicUserType());

8.3. Type support for write models

When a basic type is used in a write model, it is very important that an appropriate BasicUserType is registered. If no basic user type is registered for a type, by default the com.blazebit.persistence.view.spi.type.MutableBasicUserType is used. This basic user type assume the type is mutable which will cause values of that type to always be assumed being dirty. Updatable entity views containing values of such a type are thus always considered being dirty which has the effect, that every call to EntityViewManager.save(EntityManager em, Object view) will cause a flush of attributes containing that value. The updatable-entity-view-change-model-api is also affected of this by always reporting such attributes as being dirty.

Immutable types, like e.g. java.lang.String already does, can use the basic user type implementation com.blazebit.persistence.view.spi.type.ImmutableBasicUserType which assumes objects of the type are immutable.

A proper basic user type implementation for mutable types, when based on the provided type com.blazebit.persistence.view.spi.type.AbstractMutableBasicUserType only needs an implementation for cloning a value. The cloned value is used to e.g. keep the initial state so that later changes can be detected by checking equality.

8.4. Type support for JPA managed types

JPA managed types are also considered mutable by default, and since no dirty tracking information is available by default, objects of that such types are always considered dirty thus also always flushed. An integration with the native dirty tracking mechanism of the JPA provider might improve performance and will be considered in future versions. Entity types that handle change tracking manually, can implement a custom basic user type to improve the performance for usages of that entity type within updatable entity views, but are generally recommended to switch to subviews instead.

For further information on the possible SPI methods consult the JavaDoc of the BasicUserType interface

8.5. Optimistic locking version type support

To allow an attribute to be used as version for optimistic locking, the registered basic type also needs to implement the com.blazebit.persistence.view.spi.type.VersionBasicUserType interface. This type additionally requires to provide an implementation for returning the next version based on a given current version.

9. TypeConverter API

The TypeConverter API is similar to the JPA AttributeConverter API as it allows to convert between an entity view model type and an underlying type. This is similar to the BasicUserType SPI but can also be used to convert view types to custom types. All this might sound very generic, but it is the foundation for the support of wrapping a type in a java.util.Optional.

A TypeConverter is responsible for figuring out the actual underlying type of an entity view attribute type. In case of an attribute like e.g. Optional<Integer> getId() the TypeConverter for the java.util.Optional support determines the underlying type which is Integer. Apart from this, the TypeConverter must also implement the conversion from the view type to the underlying type and the other way around.

9.1. Builtin TypeConverters

There are several TypeConverters registered out of the box.

  • Converters for java.sql.Blob, java.sql.Clob, java.sql.NClob to implement dirty tracking in coordination with a custom BasicUserType.

If found on the classpath, TypeConverters for the following types are registered

  • java.util.Optional for all Object types

  • java.util.OptionalInt for java.lang.Integer

  • java.util.OptionalLong for java.lang.Long

  • java.util.OptionalDouble for java.lang.Double

  • java.time.LocalDate for entity types

    • java.util.Date

    • java.sql.Date

    • java.sql.Timestamp

    • java.util.Calendar

    • java.util.GregorianCalendar

  • java.time.LocalDateTime for entity types

    • java.util.Date

    • java.sql.Timestamp

    • java.util.Calendar

    • java.util.GregorianCalendar

  • java.time.Instant for entity types

    • java.util.Date

    • java.sql.Timestamp

    • java.util.Calendar

    • java.util.GregorianCalendar

  • java.time.LocalTime for java.sql.Time

  • java.util.GregorianCalendar for entity types

    • java.util.Date

    • java.sql.Timestamp

  • java.util.Calendar for entity types

    • java.util.Date

    • java.sql.Timestamp

  • java.util.Date for entity types

    • java.util.Calendar

    • java.util.GregorianCalendar

10. Updatable Entity View Change Model

Updatable entity views are not only better write model DTOs, but also allow to retrieving logical changes via the ChangeModel API. Using updatable entity views allows the persistence model to be efficiently updated, but the cost for doing that is hiding the persistent/initial state from the user. Oftentimes part of the persistent/initial state is compared with values that are about to be written to detect logical changes. Since updatable entity views handle the persistent state behind the scenes, such a manual comparison isn’t possible. Thanks to the ChangeModel API it is unnecessary.

The ChangeModel API entry point is EntityViewManager.getChangeModel(Object view) and returns the change model for a given updatable entity view. A change model instance provides access to the initial and current state of an object and the ChangeKind of a change model. Singular change models also give access to the change models of the respective attributes of an entity view type. Plural change models additionally give access to the added, removed and mutated element change models. A map change model also allows to distinguish between element and key change models.

10.1. Change Model API overview

To detect if a model or one of it’s child models is dirty, one can use the ChangeModel.isDirty() method. The actual change models of dirty elements within a SingularChangeModel can be retrieved via SingularChangeModel.getDirtyChanges(). Only attributes of the queried object are reported as change models i.e. only a single level.

The singular change models allow access to the attributes change models either via attribute path or via the metamodel attribute objects by using one of the overloaded SingularChangeModel.get(String attributePath) methods. The term path implicates a nested attribute access is possible, which is the case, but beware that accessing attributes of collection elements will result in an exception unless the SingularChangeModel.getAll(String attributePath) variant is used which returns a list of change models instead of a single one.

Another notable feature the singular change model provides is the checking for dirtyness of a specific attribute path. Instead of materializing every change model along the path, the SingularChangeModel.isDirty(String attributePath) method only reports the dirtyness of the object accessible through the given attribute path. A variant of this method SingularChangeModel.isChanged(String attributePath) will return early if one of the parent attributes was updated i.e. the identity was changed.

The plural change model is similar in the respect that it provides analogous methods that simply return a list of change models instead of a single one. It also allows to access the change models of the added, removed or mutated elements separately. To access all dirty changes similar to what is possible with SingularChangeModel#getDirtyChanges(), plural change models provide the method PluralChangeModel.getElementChanges() for doing the same.

The map change model additionally allows to differentiate between changes to key objects and element objects. It offers methods to access the key changes as well as the overall object changes with analogously named methods getAddedObjects(), getAddedKeys() etc.

10.2. Transaction support

The change model implementation gains it’s insights by inspecting the dirty tracking information of the actual objects. Since a transaction commit will flush dirty changes i.e. the dirtyness is resetted, change model objects won’t report any dirty changes after a commit. If information about the change models should be retained after a transaction commit, it must be serialized with a custom mechanism. When a rollback occurs, the dirtyness is restored to be able to commit again after doing further changes which also means that change models will work as expected.

10.3. User type support

The Change Model API builds on top of the BasicUserType foundation and it is thus essential to have a correct implementation for the type.

Unknown types are considered mutable which has the effect, that objects of that type are always considered dirty. Provide a deepClone implementation or mark the type as immutable to avoid this.

11. Entity View Builder API

The entity view builder API allows to build entity view objects through a builder API. You can assign attributes individually by using the various with methods on EntityViewBuilderBase and finally build a fully functional entity view object.

The entity view builder API entry point is EntityViewManager.createBuilder(Class<X> entityViewClass) and returns a builder for the given entity view class. It is also possible to create a builder and copy the state from an existing view to a builder via EntityViewManager.createBuilder(X entityView).

12. Spring Data integration

Apart from a plain Spring integration which is handy for configuring and providing an EntityViewManager for injection, there is also a Spring Data integration module which tries to make using entity views with Spring Data as convenient as using entities.

12.1. Setup

To setup the project for Spring Data you have to add dependencies as described in the Setup section and make beans available for CriteriaBuilderFactory and EntityViewManager instances as laid out in the Spring environment section.

In short, the following Maven dependencies are required

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-spring-data-2.7</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-hibernate-5.6</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

For Spring-Data version 2.6, 2.5, 2.4, 2.3, 2.2, 2.1, 2.0 or 1.x use the blaze-persistence-integration-spring-data artifact with the respective suffix 2.6, 2.5 2.4, 2.3, 2.2, 2.1, 2.0, 1.x.

If you are using Jakarta APIs and Spring Framework 6+ / Spring Boot 3+, use this

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-spring-data-3.3</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-hibernate-6.2</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

The dependencies for other JPA providers or other versions can be found in the core module setup section.

A possible bean configuration for the required beans CriteriaBuilderFactory and EntityViewManager in short might look like this.

@Configuration
public class BlazePersistenceConfiguration {

    @PersistenceUnit
    private EntityManagerFactory entityManagerFactory;

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    @Lazy(false)
    public CriteriaBuilderFactory createCriteriaBuilderFactory() {
        CriteriaBuilderConfiguration config = Criteria.getDefault();
        // do some configuration
        return config.createCriteriaBuilderFactory(entityManagerFactory);
    }
}
@Configuration
public class BlazePersistenceConfiguration {

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    @Lazy(false)
    // inject the criteria builder factory which will be used along with the entity view manager
    public EntityViewManager createEntityViewManager(CriteriaBuilderFactory cbf, EntityViewConfiguration entityViewConfiguration) {
        return entityViewConfiguration.createEntityViewManager(cbf);
    }
}

To enabling Blaze JPA repositories, annotate your configuration or application class with @EnableBlazeRepositories. Optionally specify a custom basePackage for repository class scanning and a custom entityManagerFactoryRef.

@EnableBlazeRepositories

12.1.1. Spring Boot Devtools

It is important to note that Blaze Persistence currently does not integrate with Spring Boot Devtools (org.springframework.boot:spring-boot-devtools). Spring Boot Devtools uses a separate RestartClassloader to load classes that frequently change to allow for faster application restarts. You may experience issues with active Spring Boot Devtools when using Blaze Persistence Entity-Views because the entity classes in the JPA metamodel are loaded by the RestartClassloader whereas the entity classes you annotate your entity views with are loaded by the base application classloader. This can lead to errors at startup time like:

The entity class '<your-entity>' used for the entity view '<your-entity-view>' could not be found in the persistence unit!

To work around this issue you either need to completely disable Spring Boot Devtools or alternatively, exclude your entity classes from the RestartClassloader by adding properties prefixed with restart.exclude to your META-INF/spring-devtools.properties.

12.2. Features

The integration comes with a convenience base interface com.blazebit.persistence.spring.data.repository.EntityViewRepository that you can use for your repository definitions.

Assume we have the following entity view:

@EntityView(Cat.class)
public interface SimpleCatView {

    @IdMapping
    public getId();

    String getName();

    @Mapping("LOWER(name)")
    String getLowerCaseName();

    Integer getAge();
}

A very simple repository might look like this:

@Transactional(readOnly = true)
public interface SimpleCatViewRepository extends EntityViewRepository<SimpleCatView, Long> {

    List<SimpleCatView> findByLowerCaseName(String lowerCaseName);
}

Since we use EntityViewRepository as a base interface we inherit the most commonly used repository methods. You can now use this repository as any other Spring Data repository:

@Controller
public class MyCatController {

    @Autowired
    private SimpleCatViewRepository simpleCatViewRepository;

    public Iterable<SimpleCatView> getCatDataForDisplay() {
        return simpleCatViewRepository.findAll();
    }

    public SimpleCatView findCatByName(String name) {
        return simpleCatViewRepository.findByLowerCaseName(name.toLowerCase());
    }
}

Spring Data Specifications can be used without restrictions. There is also the convenience base interface com.blazebit.persistence.spring.data.repository.EntityViewSpecificationExecutor that can be extended from.

@Transactional(readOnly = true)
public interface SimpleCatViewRepository extends EntityViewRepository<SimpleCatView, Long>, EntityViewSpecificationExecutor<SimpleCatView, Cat> {
}

@Controller
public class MyCatController {

    @Autowired
    private SimpleCatViewRepository simpleCatViewRepository;

    public Iterable<SimpleCatView> getCatDataForDisplay(final int minAge) {
        return simpleCatViewRepository.findAll(new Specification<Cat>() {
            @Override
            public Predicate toPredicate(Root<Cat> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                return criteriaBuilder.ge(root.<Integer>get("age"), minAge);
            }
        });
    }

Because Spring Data Specifications work on a JPA criteria builder we provide BlazeSpecification as an alternative that accepts a Blaze Persistence criteria builder but works analogously apart from that.

@Transactional(readOnly = true)
public interface SimpleCatViewRepository extends Repository<Cat, Long> {

    Iterable<SimpleCatView> findAll(BlazeSpecification specification);
}

@Controller
public class MyCatController {

    @Autowired
    private SimpleCatViewRepository simpleCatViewRepository;

    public Iterable<SimpleCatView> getCatDataForDisplay(final int minAge) {
        return simpleCatViewRepository.findAll(new BlazeSpecification() {
            @Override
            public void applySpecification(String rootAlias, CriteriaBuilder<?> builder) {
                builder.where("age").ge(minAge);
            }
        });
    }

The integration handles ad-hoc uses of @EntityGraph by adapting the query generation through call of CriteriaBuilder.fetch() rather than passing the entity graphs as hints.

Another notable feature the integration provides is the support for the return type KeysetAwarePage as a replacement for Page. By using KeysetAwarePage the keyset pagination feature is enabled for the repository method.

@Transactional(readOnly = true)
public interface KeysetAwareCatViewRepository extends Repository<Cat, Long> {

    KeysetAwarePage<SimpleCatView> findAll(Pageable pageable);
}

Note that the Pageable should be an instance of KeysetPageable if keyset pagination should be used. A KeysetPageable can be retrieved through the KeysetAwarePage or manually by constructing a KeysetPageRequest. Note that constructing a KeysetPageRequest or actually the contained KeysetPage manually is not recommended. When working with Spring WebMvc, the Spring Data WebMvc or WebFlux integrations might come in handy. For stateful server side frameworks, it’s best to put the KeysetAwarePage into a session like storage to be able to use the previousOrFirst() and next() methods for retrieving KeysetPageable objects.

When using parameters in an entity view, these parameters are usually passed in as optional parameters to an EntityViewSetting rather than normal query parameters. You can customize the EntityViewSetting object that is used by providing a EntityViewSettingProcessor like so.

@Transactional(readOnly = true)
public interface SimpleCatViewRepository extends Repository<Cat, Long> {
    List<SimpleCatView> findAll(EntityViewSettingProcessor<SimpleCatView> processor);
}
simpleCatViewRepository.findAll(setting -> setting.withOptionalParameter("language", Locale.US));

To just pass optional parameters, one can also annotate a parameter with @OptionalParam to designate it as being an optional parameter and to be included in the generated EntityViewSetting.

@Transactional(readOnly = true)
public interface SimpleCatViewRepository extends Repository<Cat, Long> {
    List<SimpleCatView> findAll(@OptionalParam("language") Locale language);
}

All other Spring Data repository features like restrictions, pagination, slices and ordering are supported as usual. Please consult the Spring Data documentation for further information.

12.3. Spring Data WebMvc integration

The Spring Data WebMvc integration offers similar pagination features for keyset pagination to what Spring Data WebMvc integration already offers for normal offset pagination.

12.3.1. Setup

To setup the project for Spring Data WebMvc you have to add the following additional dependency.

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-spring-data-webmvc</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

or if you are using Jakarta APIs and Spring 6+

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-spring-data-webmvc-jakarta</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

12.3.2. Usage

First, a keyset pagination enabled repository is needed which can be done by using KeysetAwarePage as return type.

@Transactional(readOnly = true)
public interface KeysetAwareCatViewRepository extends Repository<Cat, Long> {

    KeysetAwarePage<SimpleCatView> findAll(Pageable pageable);
}

A controller can then use this repository like the following:

@RestController
public class MyCatController {

    @Autowired
    private KeysetAwareCatViewRepository simpleCatViewRepository;

    @RequestMapping(path = "/cats", method = RequestMethod.GET)
    public Page<SimpleCatView> getCats(@KeysetConfig(Cat.class) KeysetPageable pageable) {
        return simpleCatViewRepository.findAll(pageable);
    }
}

Note that Blaze Persistence imposes some very important requirements that have to be fulfilled

  • There must always be a sort specification

  • The last sort specification must be a unique identifier

For the keyset pagination to kick in, the client has to remember the values by which the sorting is done of the first and the last element of the result. The values then need to be passed to the next request as JSON encoded query parameters. The values of the first element should use the parameter lowest and the last element the parameter highest.

The following will illustrate how this works.

First, the client makes an initial request.

GET /cats?page=0&size=3&sort=id,desc
{
    content: [
        { id: 10, name: 'Felix', age: 10 },
        { id: 9, name: 'Robin', age: 4 },
        { id: 8, name: 'Billy', age: 7 }
    ]
}

It’s the responsibility of the client to remember the attributes by which it sorts of the first and last element. In this case, {id: 10} will be remembered as lowest and {id: 8} as highest. The client also has to remember the page/offset and size which was used to request this data. When the client then wants to switch to the next page/offset, it has to pass lowest and highest as parameters as well as prevPage/prevOffset representing the page/offset that was used before.

Note that the following is just an example for illustration. Stringified JSON objects in JavaScript should be encoded view encodeURI() before being used as query parameter.

GET /cats?page=1&size=3&sort=id,desc&prevPage=0&lowest={id:10}&highest={id:8}
{
    content: [
        { id: 7, name: 'Kitty', age: 1 },
        { id: 6, name: 'Bob', age: 8 },
        { id: 5, name: 'Frank', age: 14 }
    ]
}

This will make use of keyset pagination as can be seen by looking at the generated JPQL or SQL query.

Note that the client should drop or forget the lowest, highest and prevPage/prevOffset values when

  • the page size changes and it is expected to show data not connected to the last page

  • the sorting changes

  • the filtering changes

For a full AngularJS example see the following example project.

12.3.3. Entity view deserialization

The Spring Data WebMvc integration depends on the Jackson integration and automatically provides support for deserializing entity views. Currently, there is no support for constructor injection into entity views, so entity view attributes that should be deserializable should have a setter.

@EntityView(Cat.class)
@UpdatableEntityView
public interface CatUpdateView {

    @IdMapping
    Long getId();
    String getName();
    void setName(String name);
}

public interface CatViewRepository extends Repository<Cat, Long> {

    public CatUpdateView save(CatUpdateView catCreateView);
}

A controller can then deserialize entity views of request bodies by simply using it as @RequestBody annotated parameter like this:

@RestController
public class MyCatController {

    @Autowired
    private CatViewRepository catViewRepository;

    @RequestMapping(path = "/cats", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> updateCat(@RequestBody CatUpdateView catView) {
        catViewRepository.save(catView);
        return ResponseEntity.ok(catView.getId().toString());
    }
}

In the example above, the entity view id will be sourced from the request body. Alternatively, it is also possible to retrieve the id from a path variable like this:

@RestController
public class MyCatController {

    @Autowired
    private CatViewRepository catViewRepository;

    @RequestMapping(path = "/cats/{id}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> updateCat(@EntityViewId("id") @RequestBody CatUpdateView catView) {
        catViewRepository.save(catView);
        return ResponseEntity.ok(catView.getId().toString());
    }
}

12.4. Spring Data WebFlux integration

The Spring Data WebFlux integration provides the same features as the Spring Data WebMvc integration. In addition it also supports using Mono and Flux types.

12.4.1. Setup

To setup the project for Spring Data WebFlux you have to add the following additional dependency.

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-spring-data-webflux</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

12.4.2. Usage

A controller can be written like for Spring Data WebMvc:

@RestController
public class MyCatController {

    @Autowired
    private KeysetAwareCatViewRepository simpleCatViewRepository;

    @RequestMapping(path = "/cats", method = RequestMethod.GET)
    public Page<SimpleCatView> getCats(@KeysetConfig(Cat.class) KeysetPageable pageable) {
        return simpleCatViewRepository.findAll(pageable);
    }
}

It can also use Mono or Flux types, but note that Spring Data JPA repositories don’t support reactive access.

@Controller
public class MyCatController {

    @Autowired
    private KeysetAwareCatViewRepository simpleCatViewRepository;

    @Bean
    public RouterFunction<ServerResponse> createRouterFunctions(CatRestController controller) {
        return RouterFunctions.route(RequestPredicates.GET("/cats"), this::getCats);
    }

    public Flux<SimpleCatView> getCats() {
        return Flux.fromIterable(simpleCatViewRepository.findAll().getContent());
    }
}

13. Spring HATEOAS integration

In addition to the Spring Data and Spring WebMvc or WebFlux integration, we also provide an integration for Spring HATEOAS 1.0+ that allows to create keyset aware pagination links.

13.1. Setup

To setup the project for Spring HATEOAS you first have to setup the spring data integration as described in the Setup section.

In short, the following Maven dependencies are required

<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>blaze-persistence-integration-spring-hateoas-webmvc</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-hibernate-5.6</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

or if you are using Jakarta APIs and Spring 6+

<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>blaze-persistence-integration-spring-hateoas-webmvc-jakarta</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-hibernate-6.2</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

The dependencies for other JPA providers or other versions can be found in the core module setup section.

Note that Spring HATEOAS 1.0 requires Spring Data 2.2 and Spring HATEOAS 1.1 requires Spring Data 2.3.

13.2. Features

The integration provides a custom PagedResourcesAssembler that will generate proper keyset aware pagination links when provided with a KeysetAwarePage object.

Assume we have the following entity view:

@EntityView(Cat.class)
public interface SimpleCatView {

    @IdMapping
    public getId();

    String getName();

    @Mapping("LOWER(name)")
    String getLowerCaseName();

    Integer getAge();
}

A very simple repository might look like this:

@Transactional(readOnly = true)
public interface KeysetAwareCatViewRepository extends Repository<Cat, Long> {

    KeysetAwarePage<SimpleCatView> findAll(Pageable pageable);
}

A controller can then inject the KeysetPageable object along with a KeysetAwarePagedResourcesAssembler and use it like in the following example:

@RestController
public class MyCatController {

    @Autowired
    private KeysetAwareCatViewRepository simpleCatViewRepository;

    @RequestMapping(path = "/cats", method = RequestMethod.GET, produces = { "application/hal+json" })
    public PagedModel<EntityModel<SimpleCatView>> getCats(
            @KeysetConfig(Cat.class) KeysetPageable pageable,
            KeysetAwarePagedResourcesAssembler<SimpleCatView> assembler) {
        return assembler.toModel(simpleCatViewRepository.findAll(pageable));
    }
}

The PagedModel object could also be used to generate a rel HTTP header like this:

@RestController
public class MyCatController {

    @Autowired
    private KeysetAwareCatViewRepository simpleCatViewRepository;

    @RequestMapping(path = "/cats", method = RequestMethod.GET)
    public PagedModel<EntityModel<SimpleCatView>> getCats(
            @KeysetConfig(Cat.class) KeysetPageable pageable,
            KeysetAwarePagedResourcesAssembler<SimpleCatView> assembler) {
        Page<DocumentView> resultPage = simpleCatViewRepository.findAll(pageable);

        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        for (Link link : assembler.toModel(resultPage).getLinks()) {
            headers.add(HttpHeaders.LINK, link.toString());
        }

        return new HttpEntity<>(resultPage, headers);
    }
}

For more information about the Spring-Data or Spring WebMvc integration, on which the Spring HATEOAS support is based on, take a look into the Spring-Data chapter. For a full example see the following example project.

14. DeltaSpike Data integration

Blaze Persistence provides an integration with DeltaSpike Data to create entity view based repositories.

14.1. Setup

To setup the project for DeltaSpike Data you have to add the entity view and CDI integration dependencies as described in the getting started section along with the integration dependencies for your JPA provider as described in the core module setup section.

In addition, the following Maven dependencies are required:

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-deltaspike-data-api</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-deltaspike-data-impl-1.8</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

This will also work for DeltaSpike Data 1.9.

If you still work with DeltaSpike Data 1.7 you will have to use a different integration as DeltaSpike Data 1.9 and 1.8 changed quite a bit.

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-deltaspike-data-impl-1.7</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

You also need to make beans available for CriteriaBuilderFactory and EntityViewManager as laid out in the CDI environment section.

14.2. Features

To mark a class or an interface as repository, use the DeltaSpike org.apache.deltaspike.data.api.Repository annotation.

@Repository(forEntity = Cat.class)
public interface CatViewRepository {
    List<SimpleCatView> findByLowerCaseName(String lowerCaseName);
}

The integration provides the following base interfaces that you may optionally extend to define entity view repositories:

  • com.blazebit.persistence.deltaspike.data.EntityViewRepository provides simple base methods.

  • com.blazebit.persistence.deltaspike.data.FullEntityViewRepository adds JPA criteria support to the com.blazebit.persistence.deltaspike.data.EntityViewRepository interface.

@Repository
public abstract class CatViewRepository extends FullEntityViewRepository<Cat, SimpleCatView, Long> {

    public List<SimpleCatView> findByAge(final int minAge) {
        return criteria().gt(Cat_.age, minAge)
            .select(SimpleCatView.class).orderAsc(Cat_.id).getResultList();
    }
}

Similar to what Spring Data offers, it is also possible to make use of a Specification which essentially is a callback that allows to refine a query.

@Repository(forEntity = Cat.class)
public interface SimpleCatViewRepository {
    List<SimpleCatView> findAll(Specification spec);
}

@Path("cats")
public class MyCatController {

    @Inject
    private SimpleCatViewRepository simpleCatViewRepository;

    @GET
    public List<SimpleCatView> getCatDataForDisplay(@QueryParam("minage") final int minAge) {
        return simpleCatViewRepository.findAll(new Specification<Cat>() {
            @Override
            public Predicate toPredicate(Root<Cat> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                return criteriaBuilder.ge(root.<Integer>get("age"), minAge);
            }
        });
    }

The integration handles ad-hoc uses of @EntityGraph by adapting the query generation through call of CriteriaBuilder.fetch() rather than passing the entity graphs as hints.

Another notable feature the integration provides is the support for a Pageable object with Page return type similar to what Spring Data offers. The integration also supports the return type KeysetAwarePage. By using KeysetAwarePage the keyset pagination feature is enabled for the repository method.

@Repository(forEntity = Cat.class)
public interface KeysetAwareCatViewRepository {

    KeysetAwarePage<SimpleCatView> findAll(Pageable pageable);
}

Note that the Pageable should be an instance of KeysetPageable if keyset pagination should be used. A KeysetPageable can be retrieved through the KeysetAwarePage or manually by constructing a KeysetPageRequest. Note that constructing a KeysetPageRequest or actually the contained KeysetPage manually is not recommended. When working with JAX-RS, the DeltaSpike Data Rest integration might come in handy. For stateful server side frameworks, it’s best to put the KeysetAwarePage into a session like storage to be able to use the previousOrFirst() and next() methods for retrieving KeysetPageable objects.

All other DeltaSpike Data repository features like restrictions, explicit offset pagination, returning QueryResult and others are supported as usual. Please consult the DeltaSpike Data documentation for further information.

14.3. DeltaSpike Data Rest integration

The DeltaSpike Data Rest integration offers similar pagination features for normal and keyset pagination to what Spring Data offers for normal offset based pagination.

14.3.1. Setup

To setup the project for using DeltaSpike along with JAX-RS you have to add the following additional dependency.

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-deltaspike-data-rest-api</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-deltaspike-data-rest-impl</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

This will also pull in the JAX-RS integration as well as the Jackson integration that allows for deserializing entity views in JAX-RS controllers.

14.3.2. Usage

First, a keyset pagination enabled repository is needed.

@Repository(forEntity = Cat.class)
public interface KeysetAwareCatViewRepository {

    KeysetAwarePage<SimpleCatView> findAll(Pageable pageable);
}

A controller can then use this repository like the following:

@Path("cats")
public class MyCatController {

    @Inject
    private KeysetAwareCatViewRepository simpleCatViewRepository;

    @GET
    public Page<SimpleCatView> getCats(@KeysetConfig(Cat.class) KeysetPageable pageable) {
        return simpleCatViewRepository.findAll(pageable);
    }
}

Note that Blaze Persistence imposes some very important requirements that have to be fulfilled

  • There must always be a sort specification

  • The last sort specification must be a unique identifier

For the keyset pagination to kick in, the client has to remember the values by which the sorting is done of the first and the last element of the result. The values then need to be passed to the next request as JSON encoded query parameters. The values of the first element should use the parameter lowest and the last element the parameter highest.

The following will illustrate how this works.

First, the client makes an initial request.

GET /cats?page=0&size=3&sort=id,desc
{
    content: [
        { id: 10, name: 'Felix', age: 10 },
        { id: 9, name: 'Robin', age: 4 },
        { id: 8, name: 'Billy', age: 7 }
    ]
}

It’s the responsibility of the client to remember the attributes by which it sorts of the first and last element. In this case, {id: 10} will be remembered as lowest and {id: 8} as highest. The client also has to remember the page/offset and size which was used to request this data. When the client then wants to switch to the next page/offset, it has to pass lowest and highest as parameters as well as prevPage/prevOffset representing the page/offset that was used before.

Note that the following is just an example for illustration. Stringified JSON objects in JavaScript should be encoded view encodeURI() before being used as query parameter.

GET /cats?page=1&size=3&sort=id,desc&prevPage=0&lowest={id:10}&highest={id:8}
{
    content: [
        { id: 7, name: 'Kitty', age: 1 },
        { id: 6, name: 'Bob', age: 8 },
        { id: 5, name: 'Frank', age: 14 }
    ]
}

This will make use of keyset pagination as can be seen by looking at the generated JPQL or SQL query.

Note that the client should drop or forget the lowest, highest and prevPage/prevOffset values when

  • the page size changes and it is expected to show data not connected to the last page

  • the sorting changes

  • the filtering changes

For a full AngularJS example see the following example project.

14.3.3. Entity view deserialization

The DeltaSpike Data Rest integration depends on the JAX-RS integration and thus also on the Jackson integration through which it automatically provides support for deserializing entity views. Currently, there is no support for constructor injection into entity views, so entity view attributes that should be deserializable should have a setter.

@EntityView(Cat.class)
@UpdatableEntityView
public interface CatUpdateView {

    @IdMapping
    Long getId();
    String getName();
    void setName(String name);
}

@Repository(forEntity = Cat.class)
public interface CatViewRepository {

    public CatUpdateView save(CatUpdateView catCreateView);
}

The JAX-RS integration can automatically deserialize entity views of request bodies by simply using the entity view type as parameter like this:

@Path("")
public class MyCatController {

    @Inject
    private CatViewRepository catViewRepository;

    @POST
    @Path("/cats")
    @Consumes(MediaType.APPLICATION_JSON)
    public Response updateCat(CatUpdateView catUpdateView) {
        catViewRepository.save(catUpdateView);

        return Response.ok(catUpdateView.getId().toString()).build();
    }
}

15. JAX-RS integration

The JAX-RS integration module serves as the API which contains the @EntityViewId annotation. The MessageBodyReader and ParamConverter implementations to integrate serialization frameworks with JAX-RS are available for

The integration is discovered automatically through the javax.ws.rs.ext.Providers ServiceLoader contract. Simply putting the artifact on the classpath is enough for the integration.

15.1. Setup

To use the Jackson integration directly you need the following Maven dependencies:

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-jaxrs-jackson</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

or if you are using Jakarta JPA

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-jaxrs-jackson-jakarta</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

To use the JSONB integration directly you need the following Maven dependencies:

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-jaxrs-jsonb</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

or if you are using Jakarta JPA

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-jaxrs-jsonb-jakarta</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>runtime</scope>
</dependency>

15.2. Features

The main feature is the possibility to use entity views just like normal POJOs that can be deserialized automatically.

@EntityView(Cat.class)
@UpdatableEntityView
public interface CatUpdateView {

    @IdMapping
    Long getId();
    String getName();
    void setName(String name);
}

The JAX-RS integration can automatically deserialize entity views of request bodies by simply using the entity view type as parameter like this:

@Path("")
public class MyCatController {

    @Inject
    private EntityManager em;
    @Inject
    private EntityViewManager evm;

    @POST
    @Path("/cats")
    @Consumes(MediaType.APPLICATION_JSON)
    @Transactional
    public Response updateCat(CatUpdateView catUpdateView) {
        evm.save(em, catUpdateView);

        return Response.ok(catUpdateView.getId().toString()).build();
    }
}

In the example above, the entity view id will be sourced from the request body. Alternatively, it is also possible to retrieve the id from a path variable like this:

@Path("")
public class MyCatController {

    @Inject
    private EntityManager em;
    @Inject
    private EntityViewManager evm;

    @PUT
    @Path("/cats/{id}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Transactional
    public Response updateCat(@EntityViewId("id") CatUpdateView catUpdateView) {
        evm.save(em, catUpdateView);

        return Response.ok(catUpdateView.getId().toString()).build();
    }
}

16. GraphQL integration

GraphQL is a language for data communication that requires a schema. Defining that schema and keeping it in sync with the model can become a very painful task. This is where the GraphQL integration comes to rescue as it is capable of contributing entity view types to the GraphQL schema and also create a EntityViewSetting object from a GraphQL DataFetchingEnvironment with full support for partial loading as defined by selection lists.

In addition, it also has support for the Relay pagination specification to allow easy keyset pagination.

16.1. Setup

To use the GraphQL integration you need the following Maven dependencies:

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-graphql</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

or if you are using Jakarta JPA

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-graphql-jakarta</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

Note that the integration works with the de facto standard runtime for GraphQL which is graphql-java. At least version 17.3 is required.

16.2. Usage

The integration works by contributing entity view type as GraphQL types to a GraphQL TypeDefinitionRegistry or GraphQLSchema.Builder.

Let’s consider the following entity views

@EntityView(Person.class)
public interface PersonIdView {
    @IdMapping
    Long getId();
}
@EntityView(Person.class)
public interface PersonSimpleView extends PersonIdView {
    String getName();
}
@EntityView(Cat.class)
public interface CatSimpleView {
    @IdMapping
    Long getId();
    String getName();
}
@EntityView(Cat.class)
public interface CatWithOwnerView extends CatSimpleView {
    PersonSimpleView getOwner();
}

This will generate the following GraphQL types

type CatSimpleView {
  id: ID!
  name: String
}
type CatWithOwnerView {
  id: ID!
  name: String
  owner: PersonSimpleView
}
type PersonIdView {
  id: ID!
}
type PersonSimpleView {
  id: ID!
  name: String
}

The integration happens through the class GraphQLEntityViewSupportFactory, which produces a GraphQLEntityViewSupport which you can then use. The created GraphQLEntityViewSupport object is a singleton that should only be created during boot and can be used for creating EntityViewSetting objects in GraphQL DataFetcher implementations. Usually, the object is exposed as @Bean in Spring or @ApplicationScoped bean in CDI.

The GraphQLEntityViewSupport.createSetting() and GraphQLEntityViewSupport.createPaginatedSetting() methods inspect the data fetching environment and know which entity view type is needed, but you can also provide a custom EntityViewSetting with a custom entity view type or some prepared filters/sorters. In addition, these methods will determine what to fetch according to the DataFetchingEnvironment.getSelectionList(). This will lead to the optimal query to be generated for the fields that are requested. This is not only about skipping select items, but also about avoiding unnecessary joins!

16.2.1. Plain graphql-java setup

With just graphql-java, you have to provide a schema and do the runtime-wiring. This could look like the following with a sample schema:

type Query {
    catById(id: ID!): CatWithOwnerView
}

and the setup logic:

EntityViewManager evm = ...

// Read in the GraphQL schema
URL url = Resources.getResource("schema.graphqls");
String sdl = Resources.toString(url, StandardCharsets.UTF_8);
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);

// Configure how to integrate entity views
boolean defineNormalTypes = true;
boolean defineRelayTypes = true;
GraphQLEntityViewSupportFactory graphQLEntityViewSupportFactory = new GraphQLEntityViewSupportFactory(defineNormalTypes, defineRelayTypes);
graphQLEntityViewSupportFactory.setImplementRelayNode(false);
graphQLEntityViewSupportFactory.setDefineRelayNodeIfNotExist(true);

// Integrate and create the support class for extraction of EntityViewSetting objects
GraphQLEntityViewSupport graphQLEntityViewSupport = graphQLEntityViewSupportFactory.create(typeRegistry, evm);

Next, one needs to define a DataFetcher for the defined query catById like so

CatViewRepository repository;

RuntimeWiring.newRuntimeWiring()
    .type(TypeRuntimeWiring.newTypeWiring("Query")
            .dataFetcher("catById", new DataFetcher() {
                @Override
                public Object get(DataFetchingEnvironment dataFetchingEnvironment) {
                    return repository.findById(
                            graphQLEntityViewSupport.createSetting(dataFetchingEnvironment),
                            Long.valueOf(dataFetchingEnvironment.getArgument("id"))
                    );
                }
            })
    )
    .build();

Finally, the RuntimeWiring and TypeDefinitionRegistry are joined together to a GraphQL schema which is required for the GraphQL runtime.

SchemaGenerator schemaGenerator = new SchemaGenerator();
return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
Naming types or additional fields

Types can be explicitly named by putting the @GraphQLName annotation on a type.

@GraphQLName("TheEntity")
@EntityView(MyEntity.class)
public interface MyEntityView {
    //...
}

Additional fields can be declared as getter methods that follow the Java beans convention:

@EntityView(MyEntity.class)
public interface MyEntityView {
    //...

    default String getAdditionalField() {
        return "some data";
    }

    @GraphQLName("additionalData")
    default String getData() {
        return "more data";
    }
}

In this case the schema for MyEntityView will contain two additional fields additionalField and additionalData. Note that when the GraphQL field name does not match the property name of a getter method like in the previous example, an additional data fetcher must be declared for the field:

RuntimeWiring.newRuntimeWiring()
    .type(TypeRuntimeWiring.newTypeWiring("MyEntityView")
            .dataFetcher("additionalData", new DataFetcher() {
                @Override
                public Object get(DataFetchingEnvironment dataFetchingEnvironment) {
                      Object source = dataFetchingEnvironment.getSource();
                      if (source instanceof MyEntityView) {
                          return ((MyEntityView) source).getData();
                      }
                      return null;
                }
            })
    )
    .build();
Ignoring types or fields

Types can be explicitly ignored by putting the @GraphQLIgnore annotation on a type.

It’s also possible to prevent getters in entity views to appear as fields in the GraphQL type schema, by annotating the getter method with the @GraphQLIgnore annotation.

@EntityView(MyEntity.class)
public interface MyEntityView {
    //...

    @GraphQLIgnore
    default String getAdditionalField() {
        return "some data";
    }
}
Forcing non-null types on fields

The type of a GraphQL field can be forced to be non-null by putting the @GraphQLNonNull annotation on a getter method.

Usually, the integration is able to figure out non-null types through its nullability analysis of mapping expressions, but for custom methods or cases when the analysis fails, the explicit annotation can be used.

@EntityView(MyEntity.class)
public interface MyEntityView {
    //...

    @GraphQLNonNull
    default String getAdditionalField() {
        return "some data";
    }
}

For a full example see the following example project.

16.2.2. Netflix DGS setup

To use the Netflix DGS integration you need the following Maven dependencies:

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-graphql-dgs</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

or if you are using Jakarta APIs

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-graphql-dgs-7.0</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

The Netflix DGS setup is similar to the plain graphql-java one, as you have to provide a schema as well, although you have to follow a convention. A schema must be located in a schema folder and have a suffix of *.graphls according to the documentation. The runtime-wiring looks different though as it supports an annotation based model. This could look like the following with a sample schema:

type Query {
    catById(id: ID!): CatWithOwnerView
}

Next, one needs to define a DataFetcher for the defined query catById like so

@DgsComponent
public class CatFetcher {

    @Autowired
    CatViewRepository repository;
    @Autowired
    GraphQLEntityViewSupport graphQLEntityViewSupport;

    @DgsQuery
    public CatWithOwnerView catById(@InputArgument("id") Long id, DataFetchingEnvironment dataFetchingEnvironment) {
        return repository.findById(graphQLEntityViewSupport.createSetting(dataFetchingEnvironment), Long.valueOf(dataFetchingEnvironment.getArgument("id")));
    }
}
Naming types or additional fields

Types can be explicitly named by putting the @GraphQLName annotation on a type.

@GraphQLName("TheEntity")
@EntityView(MyEntity.class)
public interface MyEntityView {
    //...
}

Additional fields can be declared as getter methods that follow the Java beans convention:

@EntityView(MyEntity.class)
public interface MyEntityView {
    //...

    default String getAdditionalField() {
        return "some data";
    }

    @GraphQLName("additionalData")
    default String getData() {
        return "more data";
    }
}

In this case the schema for MyEntityView will contain two additional fields additionalField and additionalData. Note that when the GraphQL field name does not match the property name of a getter method like in the previous example, an additional data fetcher must be declared for the field:

@DgsComponent
public class GraphQLExtensionApi {
    @DgsData(parentType = "MyEntityView", field = "theData")
    public String getNodeData(DataFetchingEnvironment dataFetchingEnvironment) {
      Object source = dataFetchingEnvironment.getSource();
      if (source instanceof MyEntityView) {
          return ((MyEntityView) source).getData();
      }
      return null;
    }
}
Ignoring types or fields

Types can be explicitly ignored by putting the @GraphQLIgnore annotation on a type.

It’s also possible to prevent getters in entity views to appear as fields in the GraphQL type schema, by annotating the getter method with the @GraphQLIgnore annotation.

@EntityView(MyEntity.class)
public interface MyEntityView {
    //...

    @GraphQLIgnore
    default String getAdditionalField() {
        return "some data";
    }
}
Forcing non-null types on fields

The type of a GraphQL field can be forced to be non-null by putting the @GraphQLNonNull annotation on a getter method.

Usually, the integration is able to figure out non-null types through its nullability analysis of mapping expressions, but for custom methods or cases when the analysis fails, the explicit annotation can be used.

@EntityView(MyEntity.class)
public interface MyEntityView {
    //...

    @GraphQLNonNull
    default String getAdditionalField() {
        return "some data";
    }
}

For a full example see the following example project.

16.2.3. SPQR setup

To use the SPQR GraphQL integration you need the following Maven dependencies:

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-graphql-spqr</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

or if you are using Jakarta JPA

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-graphql-spqr-jakarta</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

The SPQR configuration is very simple and since the framework is fully declarative, you don’t need a dedicated GraphQL schema definition.

@Configuration
public class GraphQLProvider {

    @Autowired
    EntityViewManager evm;
    @Autowired
    GraphQLSchema graphQLSchema;

    private GraphQLEntityViewSupport graphQLEntityViewSupport;

    @PostConstruct
    public void init() {
        GraphQLEntityViewSupportFactory graphQLEntityViewSupportFactory = new GraphQLEntityViewSupportFactory(false, false);
        graphQLEntityViewSupportFactory.setImplementRelayNode(false);
        graphQLEntityViewSupportFactory.setDefineRelayNodeIfNotExist(false);
        this.graphQLEntityViewSupport = graphQLEntityViewSupportFactory.create(graphQLSchema, evm);
    }

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    @Lazy(false)
    public GraphQLEntityViewSupport graphQLEntityViewSupport() {
        return graphQLEntityViewSupport;
    }

}

Next, one needs to define a DataFetcher for the defined query catById like so

@Component
@GraphQLApi
public class CatFetcher {

    @Autowired
    CatViewRepository repository;
    @Autowired
    GraphQLEntityViewSupport graphQLEntityViewSupport;

    @GraphQLQuery
    public CatWithOwnerView catById(@GraphQLArgument(name = "id") Long id, @GraphQLEnvironment ResolutionEnvironment env) {
        return repository.findById(graphQLEntityViewSupport.createSetting(env.dataFetchingEnvironment), id);
    }
}
Naming types or additional fields

Types can be explicitly named by putting the @GraphQLType or @GraphQLName annotation on a type.

@GraphQLType("TheEntity")
@EntityView(MyEntity.class)
public interface MyEntityView {
    //...
}

Additional fields can be declared as getter methods that follow the Java beans convention, or named explicitly by annotating the methods with @GraphQLQuery:

@EntityView(MyEntity.class)
public interface MyEntityView {
    //...

    default String getAdditionalField() {
        return "some data";
    }

    @GraphQLQuery(name = "additionalData")
    default String getData() {
        return "more data";
    }
}

In this case the schema for MyEntityView will contain two additional fields additionalField and additionalData. Note that when the GraphQL field name does not match the property name of a getter method like in the previous example, the @GraphQLName annotation will not work, and the SPQR annotation @GraphQLQuery is preferred.

Ignoring types or fields

Types can be explicitly ignored by putting the @GraphQLIgnore annotation on a type.

It’s also possible to prevent getters in entity views to appear as fields in the GraphQL type schema, by annotating the getter method with the @GraphQLIgnore annotation.

@EntityView(MyEntity.class)
public interface MyEntityView {
    //...

    @GraphQLIgnore
    default String getAdditionalField() {
        return "some data";
    }
}
Forcing non-null types on fields

The type of a GraphQL field can be forced to be non-null by putting the @GraphQLNonNull annotation on a getter method.

Usually, the integration is able to figure out non-null types through its nullability analysis of mapping expressions, but for custom methods or cases when the analysis fails, the explicit annotation can be used.

@EntityView(MyEntity.class)
public interface MyEntityView {
    //...

    @GraphQLNonNull
    default String getAdditionalField() {
        return "some data";
    }
}

For a full example see the following example project.

16.2.4. MicroProfile GraphQL - SmallRye

MicroProfile GraphQL (version 1.1 at the time of writing) has a completely different approach, as it is completely annotation based. At the moment, only the SmallRye implementation is supported and unfortunately, not yet within Quarkus.

Let’s consider the following sample schema

type Query {
    catById(id: ID!): CatWithOwnerView
}

and the setup logic:

@ApplicationScoped
public class GraphQLProducer {

    @Inject
    EntityViewManager evm;

    GraphQLEntityViewSupport graphQLEntityViewSupport;

    void configure(@Observes GraphQLSchema.Builder schemaBuilder) {
        // Option 1: As of SmallRye GraphQL 1.3.1 you can disable the generation of GraphQL types and annotate all entity views with @Type instead
        // boolean defineNormalTypes = false;
        // boolean defineRelayTypes = false;

        // Option 2: Let the integration replace the entity view GraphQL types
        boolean defineNormalTypes = true;
        boolean defineRelayTypes = true;

        // Configure how to integrate entity views
        GraphQLEntityViewSupportFactory graphQLEntityViewSupportFactory = new GraphQLEntityViewSupportFactory(defineNormalTypes, defineRelayTypes);

        graphQLEntityViewSupportFactory.setImplementRelayNode(false);
        graphQLEntityViewSupportFactory.setDefineRelayNodeIfNotExist(true);
        graphQLEntityViewSupportFactory.setScalarTypeMap(GraphQLScalarTypes.getScalarMap());
        // Integrate and create the support class for extraction of EntityViewSetting objects
        this.graphQLEntityViewSupport = graphQLEntityViewSupportFactory.create(schemaBuilder, evm);
    }

    @Produces
    @ApplicationScoped
    GraphQLEntityViewSupport graphQLEntityViewSupport() {
        return graphQLEntityViewSupport;
    }
}

Note that you need a microprofile-config.properties file in META-INF with the config option smallrye.graphql.events.enabled=true to enable the events.

Next, one needs to define a DataFetcher for the defined query catById like so

@GraphQLApi
public class CatFetcher {

    @Inject
    CatViewRepository repository;
    @Inject
    Context context;
    @Inject
    GraphQLEntityViewSupport graphQLEntityViewSupport;

    @Query
    public CatWithOwnerView catById(@Input("id") Long id) {
        return repository.findById(graphQLEntityViewSupport.createSetting(context.unwrap(DataFetchingEnvironment.class)), id);
    }
}
Naming types or additional fields

Types can be explicitly named by putting the @Name or @GraphQLName annotation on a type.

@Name("TheEntity")
@EntityView(MyEntity.class)
public interface MyEntityView {
    //...
}

Additional fields can be declared as getter methods that follow the Java beans convention, or named explicitly by annotating the methods with @Query:

@EntityView(MyEntity.class)
public interface MyEntityView {
    //...

    default String getAdditionalField() {
        return "some data";
    }

    @Query("additionalData")
    default String getData() {
        return "more data";
    }
}

In this case the schema for MyEntityView will contain two additional fields additionalField and additionalData. Note that when the GraphQL field name does not match the property name of a getter method like in the previous example, the @GraphQLName annotation will not work, and the MicroProfile GraphQL annotation @Query is preferred.

Ignoring types or fields

Types can be explicitly ignored by putting the @Ignore or @GraphQLIgnore annotation on a type.

It’s also possible to prevent getters in entity views to appear as fields in the GraphQL type schema, by annotating the getter method with the @GraphQLIgnore annotation.

@EntityView(MyEntity.class)
public interface MyEntityView {
    //...

    @Ignore
    default String getAdditionalField() {
        return "some data";
    }
}
Forcing non-null types on fields

The type of a GraphQL field can be forced to be non-null by putting the @NonNull annotation on a getter method.

Usually, the integration is able to figure out non-null types through its nullability analysis of mapping expressions, but for custom methods or cases when the analysis fails, the explicit annotation can be used.

@EntityView(MyEntity.class)
public interface MyEntityView {
    //...

    @NonNull
    default String getAdditionalField() {
        return "some data";
    }
}

For a full example see the following example project.

16.2.5. Sample query

The repository for the previously presented setups could look like this:

public class CatViewRepository {

    private final EntityManager em;
    private final CriteriaBuilderFactory cbf;
    private final EntityViewManager evm;

    public CatViewRepository(EntityManager em, CriteriaBuilderFactory cbf, EntityViewManager evm) {
        this.em = em;
        this.cbf = cbf;
        this.evm = evm;
    }

    public <T> T findById(EntityViewSetting<T, CriteriaBuilder<T>> setting, Long id) {
        return evm.find(em, setting, id);
    }
}

A sample GraphQL query

query {
    findCatById(id: 1) {
        id
        name
    }
}

will cause a JPQL query similar to the following

SELECT
    c.id,
    c.name
FROM Cat c
WHERE c.id = :param

It does not select or join the owner information, although it is specified in the entity view! This optimization works through applying the selection list of the DataFetchingEnvironment via EntityViewSetting.fetch().

16.3. Pagination support

GraphQL itself does not really define a standard pagination mechanism, so the integration implements part of the Relay pagination specification in order to provide support for keyset pagination in a more or less common format.

To generate the types that are necessary for using a Relay compatible client, the GraphQLEntityViewSupportFactory can be further configured.

boolean defineNormalTypes = true;
// This time, also define the relay types i.e. Connection, Edge and Node
boolean defineRelayTypes = true;
GraphQLEntityViewSupportFactory graphQLEntityViewSupportFactory = new GraphQLEntityViewSupportFactory(defineNormalTypes, defineRelayTypes);
// Implementing the Node interface requires a custom type resolver which is out of scope here, so configure to not doing that
graphQLEntityViewSupportFactory.setImplementRelayNode(false);
// If the type registry does not yet define the Node interface, we specify that it should be generated
graphQLEntityViewSupportFactory.setDefineRelayNodeIfNotExist(true);

With the entity views defined before, this will generate the following GraphQL types

type PageInfo {
  startCursor: String
  endCursor: String
}
type CatWithOwnerViewConnection {
  edges: [CatWithOwnerViewEdge]
  pageInfo: PageInfo
}
type CatWithOwnerViewEdge {
  node: CatWithOwnerViewNode!
  cursor: String!
}
type CatWithOwnerViewNode {
  id: ID!
  name: String
  owner: PersonSimpleView
}
type PersonSimpleView {
  id: ID!
  name: String
}

To use these type, the static GraphQL Schema needs to be extended. Note that you can skip this for MicroProfile GraphQL.

type Query {
    findAll(first: Int, last:Int, offset: Int, before: String, after: String): CatWithOwnerViewConnection!
}

The Relay specification defines the first and last arguments to represent the amount of element to fetch. Using first will fetch the next X elements after the given reference point or the start, according to a specific ordering. Using last will fetch the last X elements before the given reference point or the end, according to a specific ordering.

If you can’t use keyset pagination, the GraphQL integration also allows to use an offset argument, but it is not recommended as offset based pagination has scalability problems.

A data fetcher for using this, could look like the following

CatViewRepository repository = ...
DataFetchingEnvironment dataFetchingEnvironment = ...

EntityViewSetting<Object, ?> setting = graphQLEntityViewSupport.createPaginatedSetting(dataFetchingEnvironment);
// The last order by item must be a unique expression for deterministic ordering
setting.addAttributeSorter("id", Sorters.ascending());
if (setting.getMaxResults() == 0) {
    return new GraphQLRelayConnection<>(Collections.emptyList());
}
return new GraphQLRelayConnection<>(repository.findAll(setting));

Note that in case of MicroProfile GraphQL, you will have to define the various input arguments in the method signature of the data fetcher:

@Query
public GraphQLRelayConnection<CatWithOwnerView> findAll(
        @Name("first") Integer first,
        @Name("last") Integer last,
        @Name("offset") Integer offset,
        @Name("before") String before,
        @Name("after") String after) {
    // ...
}

The GraphQLEntityViewSupport.createPaginatedSetting() method is capable of reading all necessary information from the DataFetchingEnvironment and the schema. It knows how to process first, last, offset, before and after arguments as well as integrates with the selection list feature to

  • Avoid count queries to determine the overall count

  • Avoid fetching non-requested node attributes

If the query does not specify first or last, the EntityViewSetting.getMaxResults() will be 0 which will cause an exception if used for querying.

Finally, the DataFetcher must return a GraphQLRelayConnection object that wraps a List or PagedList such that the correct result structure is produced.

A sample GraphQL query

query {
  findAll(first: 1){
    edges {
      node {
        id
        name
      }
    }
    pageInfo {
      startCursor
      endCursor
    }
  }
}

will cause a JPQL query similar to the following

SELECT
    c.id,
    c.name
FROM Cat c
LIMIT 1

and provide a result object like the following

query: {
  findAll: {
    edges: [{
      node: {
        id: 1,
        name: "Cat 1"
      }
    }],
    pageInfo: {
      startCursor: "...",
      endCursor: "..."
    }
  }
}

You can the use the endCursor on the client side as value for the after argument to get the next page:

query {
  findAll(first: 1, after: "..."){
    edges {
      node {
        id
        name
      }
    }
    pageInfo {
      startCursor
      endCursor
    }
  }
}

which will cause a JPQL query similar to the following

SELECT
    c.id,
    c.name
FROM Cat c
WHERE c.id > :previousId
LIMIT 1

and provide a result object like the following

query: {
  findAll: {
    edges: [{
      node: {
        id: 2,
        name: "Cat 2"
      }
    }],
    pageInfo: {
      startCursor: "...",
      endCursor: "..."
    }
  }
}

For a full example see one of the following example projects:

17. Quarkus integration

We provide a basic Quarkus extension that allows to use Blaze Persistence core and entity views in a Quarkus application. As outlined in the setup section you need the following dependency for the integration:

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-quarkus</artifactId>
    <version>${blaze-persistence.version}</version>
</dependency>

The use in native images also requires a dependency on the entity view annotation processor that may be extracted into a separate native profile:

<profiles>
    <profile>
        <id>native</id>
        <dependencies>
            <dependency>
                <groupId>com.blazebit</groupId>
                <artifactId>blaze-persistence-entity-view-processor</artifactId>
                <version>${blaze-persistence.version}</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    </profile>
</profiles>

17.1. Entity view and entity view listener discovery

The extension performs entity view and entity view listener scanning at deployment time while the remainder of the bootstrapping is performed at runtime.

17.2. CDI support

CriteriaBuilderFactory and EntityViewManager are injectable out of the box.

17.3. Multiple Blaze Persistence instances

In order to allow users to utilize the Hibernate ORM extension’s support for multiple persistence units the extension supports multiple Blaze Persistence instances (i.e. CriteriaBuilderFactory and EntityViewManager) that can use different persistence units using the Quarkus configuration properties approach.

The properties at the root of the quarkus.blaze-persistence. namespace refer to the default Blaze Persistence instance that is automatically created as long as no other named instances have been defined.

17.3.1. Assigning persistence units to Blaze Persistence instances

If not specified otherwise, the default Blaze Persistence uses the default persistence unit.

Using a map based approach, it is possible to define named Blaze Persistence instances. The used persistence units can be assigned using the persistence-unit property.

quarkus.blaze-persistence.persistence-unit=UserPU

quarkus.blaze-persistence."order".persistence-unit=OrderPU

The above snippet assigns the persistence unit UserPU to the default Blaze Persistence instance and the OrderPU to the instance named order.

17.3.2. Attaching entity view and entity view listener classes to Blaze Persistence instances

When multiple Blaze Persistence instances have been defined, it is required to specify packages for each instance that determine the attachment of discovered entity views and entity view listeners to the respective instances.

There are two ways to do this which cannot be mixed:

  • Via the packages configuration property;

  • Via the @com.blazebit.persistence.integration.quarkus.runtime.BlazePersistenceInstance package-level annotation.

If mixed use is detected, the annotations are ignored and only the packages configuration properties are taken into account.

Using the packages configuration property:

quarkus.blaze-persistence.packages=com.example.view.shared,com.example.view.user

quarkus.blaze-persistence."order".packages=com.example.view.shared,com.example.view.order

The above snippet assigns all enity views under the com.example.view.user package to the default Blaze Persistence instance and all entity views under the com.example.view.order package to the named "order" instance. Views under the com.example.view.shared package will be known to both instances.

An alternative approach to attach entity view and entity view listener classes to Blaze Persistence instances is to use package-level @com.blazebit.persistence.integration.quarkus.runtime.BlazePersistenceInstance annotations. The two approaches cannot be mixed.

To obtain a configuration similar to the one above with the packages configuration property, create package-info.java files with the following contents:

@BlazePersistenceInstance("order")
package com.example.view.order;

import com.blazebit.persistence.integration.quarkus.runtime.BlazePersistenceInstance;
@BlazePersistenceInstance(BlazePersistenceInstance.DEFAULT)
package com.example.view.user;

import com.blazebit.persistence.integration.quarkus.runtime.BlazePersistenceInstance;
@BlazePersistenceInstance(BlazePersistenceInstance.DEFAULT)
@BlazePersistenceInstance("order")
package com.example.view.shared;

import com.blazebit.persistence.integration.quarkus.runtime.BlazePersistenceInstance;

Both approaches take subpackages into account.

17.3.3. CDI integration

The CDI integration is straightforward and uses @com.blazebit.persistence.integration.quarkus.runtime.BlazePersistenceInstance annotation to specify the Blaze Persistence instance for injection.

@Inject
CriteriaBuilderFactory cbf;
@Inject
EntityViewManage evm;

This will inject the CriteriaBuilderFactory and EntityViewManager of the default Blaze Persistence instance.

@Inject
@BlazePersistenceInstance("order")
CriteriaBuilderFactory cbf;
@Inject
@BlazePersistenceInstance("order")
EntityViewManage evm;

This will inject the CriteriaBuilderFactory and EntityViewManager of the named order instance.

Be careful to not mix up the entity managers you pass to CriteriaBuilderFactory when performing operations. In the context of the above example, always pass the entity manager for the default persistence unit to the default CriteriaBuilderFactory and the entity manager for the orderPU to the CriteriaBuilderFactory belonging to the order Blaze Persistence instance.

17.4. Hot reload

The extension supports hot reload.

17.5. Configuration properties

There are various optional properties useful to refine your EntityViewManager and CriteriaBuilderFactory or guide guesses of Quarkus.

There are no required properties, as long as the Hibernate ORM extension is configured properly.

When no property is set, the Blaze Persistence defaults apply.

The configuration properties listed here allow you to override such defaults, and customize and tune various aspects.

Key quarkus.blaze-persistence.template-eager-loading

Type

boolean

Default

false

Description

A boolean flag to make it possible to prepare all view template caches on startup. By default the eager loading of the view templates is disabled to have a better startup performance. Valid values for this property are true or false.

Key quarkus.blaze-persistence.managed-type-validation-disabled

Type

boolean

Default

false

Description

A boolean flag to make it possible to disable the managed type validation. By default the managed type validation is enabled, but since the validation is not bullet proof, it can be disabled. Valid values for this property are true or false.

Key quarkus.blaze-persistence.default-batch-size

Type

int

Default

1

Description

An integer value that defines the default batch size for entity view attributes. By default the value is 1 and can be overridden either via com.blazebit.persistence.view.BatchFetch#size() or by setting this property via com.blazebit.persistence.view.EntityViewSetting#setProperty.

Key quarkus.blaze-persistence.expect-batch-mode

Type

String

Default

"values"

Description

A mode specifying if correlation value, view root or embedded view batching is expected. By default the value is values and can be overridden by setting this property via com.blazebit.persistence.view.EntityViewSetting#setProperty. Valid values are - values - view_roots - embedding_views

Key quarkus.blaze-persistence.updater.eager-loading

Type

boolean

Default

false

Description

A boolean flag to make it possible to prepare the entity view updater cache on startup. By default the eager loading of entity view updates is disabled to have a better startup performance. Valid values for this property are true or false.

Key quarkus.blaze-persistence.updater.disallow-owned-updatable-subview

Type

boolean

Default

true

Description

A boolean flag to make it possible to disable the strict validation that disallows the use of an updatable entity view type for owned relationships. By default the use is disallowed i.e. the default value is true, but since there might be strange models out there, it possible to allow this. Valid values for this property are true or false.

Key quarkus.blaze-persistence.updater.strict-cascading-check

Type

boolean

Default

true

Description

A boolean flag to make it possible to disable the strict cascading check that disallows setting updatable or creatable entity views on non-cascading attributes before being associated with a cascading attribute. When disabled, it is possible, like in JPA, that the changes done to an updatable entity view are not flushed when it is not associated with an attribute that cascades updates. By default the use is enabled i.e. the default value is true. Valid values for this property are true or false.

Key quarkus.blaze-persistence.updater.error-on-invalid-plural-setter

Type

boolean

Default

false

Description

A boolean flag that allows to switch from warnings to boot time validation errors when invalid plural attribute setters are encountered while the strict cascading check is enabled. When true, a boot time validation error is thrown when encountering an invalid setter, otherwise just a warning. This configuration has no effect when the strict cascading check is disabled. By default the use is disabled i.e. the default value is false. Valid values for this property are true or false.

Key quarkus.blaze-persistence.create-empty-flat-views

Type

boolean

Default

true

Description

A boolean flag that allows to specify if empty flat views should be created by default if not specified via EmptyFlatViewCreation. By default the creation of empty flat views is enabled i.e. the default value is true. Valid values for this property are true or false.

Key quarkus.blaze-persistence.expression-cache-class

Type

String

Default

"com.blazebit.persistence.parser.expression.ConcurrentHashMapExpressionCache"

Description

The fully qualified expression cache implementation class name.

Key quarkus.blaze-persistence.inline-ctes

Type

boolean

Default

true

Description

If set to true, the CTE queries are inlined by default. Valid values for this property are true, false or auto. Default is true which will always inline non-recursive CTEs. The auto configuration will only make use of inlining if the JPA provider and DBMS dialect support/require it. The property can be changed for a criteria builder before constructing a query.

Key quarkus.blaze-persistence.query-plan-cache-enabled

Type

boolean

Default

true

Description

If set to true, the query plans are cached and reused. Valid values for this property are true and false. Default is true. This configuration option currently only takes effect when Hibernate is used as JPA provider. The property can be changed for a criteria builder before constructing a query.

17.6. Customization

As of version 1.6.4, a CDI event of the type EntityViewConfiguration is fired with an optional @BlazePersistenceInstance qualifier at boot time. This allows to further customize the configuration which is often necessary for

Custom type test values

Providing this is necessary if you make use of some Hibernate UserType or custom BasicType to allow Blaze Persistence to figure out if equals/hashCode is properly implemented.

Register custom type converter

If you want to automatically convert between a domain type, and a persistence entity model type, a type converter is needed which can be registered on EntityViewConfiguration.

Register custom basic user type

In order to make proper use of a custom type in entity views, it is necessary to register a BasicUserType on EntityViewConfiguration.

Configure default values for optional parameters

Sometimes it is useful to provide access to services into entity views through optional parameters, for which a global default value can be registered on EntityViewConfiguration.

As of version 1.6.5, also a CDI event of the type CriteriaBuilderConfiguration is fired with an optional @BlazePersistenceInstance qualifier at boot time. This allows to further customize the configuration which is often necessary if the context-less variant CriteriaBuilderConfigurationContributor isn’t enough

Register named type for VALUES

If you want to use a type that isn’t supported out of the box, it needs to be registered under a name.

Register custom JpqlFunctionGroup

If you want to register a CDI context aware JpqlFunctionGroup.

Register JpqlMacro

If you want to register a CDI context aware JpqlMacro.

Register custom dialect

When a dialect has a bug, needs a customization, or a new kind of dialect should be registered.

Configure properties

Sometimes it is simply necessary to override a configuration property through setProperty

18. Serialization integration

In general, serialization of entity views should be no problem for most serialization frameworks as they rely on getter based access and entity views naturally define their state through getters.

The deserialization support is a different thing though for which special integrations are required.

By default, the deserialization will invoke EntityViewManager.getReference(viewType, id) to construct objects. If the view type that should be deserialized is creatable i.e. annotated with @CreatableEntityView and no value for the @IdMapping attribute is provided in the payload, it will be created via EntityViewManager.create(). The rest of the payload values are then deserialized onto the constructed object via the setters the entity view types provide.

Read-only types only support deserialization when a value for the @IdMapping attribute is given and obviously need setters for the deserialization to work properly. Adding setters makes the models mutable which might not be desirable, so consider this when wanting to deserialize payload to entity views. A possible solution is to create an entity view subtype that has the sole purpose of providing setters so that it can be deserialized.

Since entity view objects are created via EntityViewManager.getReference() or EntityViewManager.create(), the objects can then be directly saved via EntityViewManager.save(). For updatable entity views it is important to understand that EntityViewManager.save() will only flush non-null attributes i.e. do a partial flush. This is due to dirty tracking thinking that the initial state is all null attributes. Through deserialization, some attributes are set to values which are then considered dirty. That behavior is exactly what you would expect from an HTTP endpoint using the PATCH method.

If you need to flush the full state regardless of the dirty tracking information, you can use EntityViewManager.saveFull(), but be aware that orphan removals will currently not work when using the QUERY flush strategy because the initial state is unknown. If you need orphan removal in such a case, you are advised to switch to the ENTITY flush strategy for now. You could also make use of the EntityViewManager.saveTo() and EntityViewManager.saveFullTo() variants to flush data to an entity that was loaded via e.g. EntityManager.find().

An alternative to this would be to deserialize the state onto an existing updatable entity view that was loaded via e.g. EntityViewManager.find(). With the initial state being known due to loading from the database, orphan removal will work correctly, but be aware that providing null values for attributes in the JSON payload will obviously cause null to be set on the entity view attributes.

18.1. Jackson integration

18.1.1. Setup

To use the Jackson integration directly you need the following Maven dependencies:

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-jackson</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

or if you are using Jakarta JPA

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-jackson-jakarta</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

If you are using JAX-RS, Spring WebMvc or Spring WebFlux, consider using the already existing integrations instead to avoid unnecessary work.

18.1.2. Usage

The integration happens by adding a module and a custom visibility checker to an existing ObjectMapper.

ObjectMapper existingMapper = ...
EntityViewManager evm = ...
EntityViewAwareObjectMapper mapper = new EntityViewAwareObjectMapper(evm, existingMapper);

The EntityViewAwareObjectMapper class provides utility methods for integrating with JAX-RS, Spring WebMvc and Spring WebFlux, but you can use your ObjectMapper directly as before as the module and visibility checker is registered in the existing mapper.

18.2. JSONB integration

18.2.1. Setup

To use the JSONB integration directly you need the following Maven dependencies:

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-jsonb</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

or if you are using Jakarta JPA

<dependency>
    <groupId>com.blazebit</groupId>
    <artifactId>blaze-persistence-integration-jsonb-jakarta</artifactId>
    <version>${blaze-persistence.version}</version>
    <scope>compile</scope>
</dependency>

If you are using JAX-RS, Spring WebMvc or Spring WebFlux, consider using the already existing integrations instead to avoid unnecessary work.

18.2.2. Usage

The integration happens by adding custom JsonDeserializer instances per entity view and a PropertyVisibilityStrategy to a JsonbConfig.

JsonbConfig jsonbConfig = new JsonbConfig();
EntityViewJsonbDeserializer.integrate(jsonbConfig, evm, idValueAccessor);
Jsonb jsonb = JsonbBuilder.create(jsonbConfig);

The resulting Jsonb instance will make use of the special deserializers for entity views and you can simply use it as usual e.g. jsonb.fromString("...", MyEntityView.class).

19. Metamodel

The metamodel for entity views is very similar to the JPA metamodel and the entry point is ViewMetamodel which can be acquired through EntityViewManager.getMetamodel()

It allows access to views(ViewType) and flat views(FlatViewType) which both are subtypes of managed views(ManagedViewType). The only difference between the two is that a flat view has no id mapping, so it’s identity is composed of all attributes which results in some limitations as described in the flat view mapping section.

Managed view types class diagram

A view can have multiple named constructors that have parameter attributes. Additionally, a view can also have multiple named view filters. Every managed view has attributes which are structured based on the arity(singular or plural), the mapping type(parameter or method) and correlation type(normal, subquery or correlated).

An attribute is always either an instance of ParameterAttribute or MethodAttribute depending on whether it is defined on a constructor as parameter or as getter method. A parameter attribute is defined by it’s index and it’s declaring MappingConstructor. Method attributes have a name, may have multiple named attribute filters and might possibly be updatable.

A singular attribute is always an instance of SingularAttribute and is given if isCollection() returns false. If it is a subquery i.e. isSubquery() returns true, it is also an instance of SubqueryAttribute. If it is correlated i.e. isCorrelated() returns true, it is also an instance of CorrelatedAttribute. If it is neither a subquery nor correlated, it is going to be an instance of MappingAttribute.

A plural attribute is always an instance of PluralAttribute and is given if isCollection() return true. Since plural attributes can’t be defined via a subquery mapping, it is never an instance of SubqueryAttribute. If it is correlated i.e. isCorrelated() returns true, it is also an instance of CorrelatedAttribute, otherwise it is going to be an instance of MappingAttribute. Depending on the collection type returned by getCollectionType a plural attribute is also an instance of

  • CollectionAttribute if CollectionType.COLLECTION

  • ListAttribute if CollectionType.LIST

  • SetAttribute if CollectionType.SET

  • MapAttribute if CollectionType.MAP

20. Annotation processor

Blaze Persistence provides an annotation processor that can be used to generate static metamodels, implementations and builders for entity views. The setup is described in getting started chapter.

The annotation processor supports the following annotation processor options:

  • debug to print debug information. Default false

  • addGenerationDate to add the generation date to the generated java files. Default false

  • addGeneratedAnnotation to add the @Generated annotation to the generated java files. Default true

  • addSuppressWarningsAnnotation to add the @SuppressWarnings annotation to the generated java files. Default false

  • strictCascadingCheck whether to generate strict cascading checks for entity view implementations. Default true

  • defaultVersionAttributeName the name of an entity view attribute that should be considered the optimistic lock version. No default

  • defaultVersionAttributeType the type the entity view version attribute should have to be considered the version attribute. No default

  • generateImplementations whether to generate entity view implementations. If false, the generateBuilders option is meaningless. Default true

  • generateBuilders whether to generate entity view builders. Default true

  • createEmptyFlatViews whether to create empty flat views by default unless specified via @EmptyFlatViewCreation. Default is true

  • generateDeepConstants whether to create nested classes for subview attributes that allow deep static referencing into a model. Default is true

  • optionalParameters a semicolon separated list of names with optional types in the format NAME=java.lang.String, for globally registered optional parameters

20.1. Static metamodel

Static metamodel classes will reflect the structure of the entity view and provide access to the typed metamodel elements for an entity view. The static metamodels for entity views like the following:

@EntityView(Cat.class)
interface SimpleCatView {
    @IdMapping
    Long getId();
    String getName();
}
@EntityView(Cat.class)
interface CatView extends SimpleCatView {
    Set<SimpleCatView> getKittens();
}

look like this:

@StaticMetamodel(SimpleCatView.class)
public class SimpleCatView_ {
    public static volatile SingularAttribute<SimpleCatView, Long> id;
    public static volatile SingularAttribute<SimpleCatView, String> name;

    public static final String ID = "id";
    public static final String NAME = "name";
}
@StaticMetamodel(CatView.class)
public class CatView_ {
    public static volatile SingularAttribute<CatView, Long> id;
    public static volatile SingularAttribute<CatView, String> name;
    public static volatile SetAttribute<CatView, SimpleCatView> kittens;

    public static final String ID = "id";
    public static final String NAME = "name";
    public static final String KITTENS = "kittens";
}

The attributes can be used for doing type safe operations like e.g. setting attributes through entity view builders in a type safe yet generic manner.

Generated metamodels are annotated with @StaticMetamodel and are scanned for during boot which can be turned off via the configuration property STATIC_METAMODEL_SCANNING_DISABLED.

20.2. Static implementation

The static implementation of an entity view is a class that implements the entity view contract as well as some entity view SPI contracts like e.g. com.blazebit.persistence.view.spi.type.MutableStateTrackable, com.blazebit.persistence.view.spi.type.DirtyStateTrackable, com.blazebit.persistence.view.spi.type.EntityViewProxy. The implementations for the state tracking contracts contain code for efficient dirty tracking.

The static implementations for entity views like the following:

@EntityView(Cat.class)
interface SimpleCatView {
    @IdMapping
    Long getId();
    String getName();
}
@EntityView(Cat.class)
interface CatView extends SimpleCatView {
    Set<SimpleCatView> getKittens();
}

look like this:

@StaticImplementation(SimpleCatView.class)
public class SimpleCatViewImpl implements SimpleCatView, EntityViewProxy {
    private final Long id;
    private final String name;

    public SimpleCatViewImpl(SimpleCatViewImpl noop, Map<String, Object> optionalParameters) {
        this.id = null;
        this.name = null;
    }

    public SimpleCatViewImpl(Long id) {
        this.id = id;
        this.name = null;
    }

    public SimpleCatViewImpl(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public SimpleCatViewImpl(SimpleCatViewImpl noop, int offset, Object[] tuple) {
        this.id = (Long) tuple[offset + 0];
        this.name = (String) tuple[offset + 1];
    }

    public SimpleCatViewImpl(SimpleCatViewImpl noop, int offset, int[] assignment, Object[] tuple) {
        this.id = (Long) tuple[offset + assignment[0]];
        this.name = (String) tuple[offset + assignment[1]];
    }

    @Override
    public Long getId() {
        return id;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Class<?> $$_getJpaManagedClass() {
        return Cat.class;
    }

    @Override
    public Class<?> $$_getJpaManagedBaseClass() {
        return Cat.class;
    }

    @Override
    public Class<?> $$_getEntityViewClass() {
        return SimpleCatView.class;
    }

    @Override
    public boolean $$_isNew() {
        return false;
    }

    @Override
    public Object $$_getId() {
        return id;
    }

    @Override
    public Object $$_getVersion() {
        return null;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(id);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || this.$$_getId() == null) {
            return false;
        }
        if (obj instanceof EntityViewProxy) {
            EntityViewProxy other = (EntityViewProxy) obj;
            if (this.$$_getJpaManagedBaseClass() == other.$$_getJpaManagedBaseClass() && this.$$_getId().equals(other.$$_getId())) {
                return true;
            } else {
                return false;
            }
        }
        if (obj instanceof SimpleCatView) {
            SimpleCatView other = (SimpleCatView) obj;
            if (!Objects.equals(this.getId(), other.getId())) {
                return false;
            }
            return true;
        }
        return false;
    }
}

@StaticImplementation(CatView.class)
public class CatViewImpl implements CatView, EntityViewProxy {
    // Similar to SimpleCatViewImpl with some additions for kittens

    private final Set<SimpleCatView> kittens;

    @Override
    public Set<SimpleCatView> getKittens() {
        return kittens;
    }
}

The first constructor public SimpleCatViewImpl(SimpleCatViewImpl noop, Map<String, Object> optionalParameters) is the so called "create"-constructor i.e. the one used for EntityViewManager.create(). The next constructor public SimpleCatViewImpl(Long id) is the id-reference constructor i.e. the one used for EntityViewManager.getReference(). The third constructor public SimpleCatViewImpl(Long id, String name) is the full state constructor which can be used by end-users. The other two constructors public SimpleCatViewImpl(SimpleCatViewImpl noop, int offset, Object[] tuple) and public SimpleCatViewImpl(SimpleCatViewImpl noop, int offset, int[] assignment, Object[] tuple) are used internally by the runtime to construct entity view objects. The variant with int[] assignment is usually only relevant when entity view inheritance is enabled.

Generated implementations are annotated with @StaticImplementation and are scanned for during boot which can be turned off via the configuration property STATIC_IMPLEMENTATION_SCANNING_DISABLED.

The generation of static implementations can be turned off by setting the generateImplementations option to false in the annotation processor option map.

20.3. Static builder

The static builder of an entity view is a class that implements the com.blazebit.persistence.view.EntityViewBuilder contract to build a static implementation instance. The generated class is a straightforward implementation of the builder interface tailored for the entity view state i.e. every attribute is a separate field in the builder. A call to EntityViewManager.createBuilder() will return an instance of a registered static builder type, or if none is registered, a generic builder.

Generated builders are annotated with @StaticBuilder and are scanned for during boot which can be turned off via the configuration property STATIC_BUILDER_SCANNING_DISABLED.

The generation of static builders can be turned off by setting the generateBuilders option to false in the annotation processor option map.

21. Configuration

Blaze Persistence can be configured by setting properties on a com.blazebit.persistence.view.spi.EntityViewConfiguration object and creating a EntityViewManager from it.

21.1. Configuration properties

21.1.1. PROXY_EAGER_LOADING

Defines whether proxy classes for entity views should be created eagerly when creating the EntityViewManager or on demand. To improve startup performance this is deactivated by default. When using entity views in a clustered environment you might want to enable this!

Key com.blazebit.persistence.view.proxy.eager_loading

Type

boolean

Default

false

Applicable

Configuration only

21.1.2. TEMPLATE_EAGER_LOADING

Defines whether entity view template objects should be created eagerly when creating the EntityViewManager or on demand. To improve startup performance this is deactivated by default. In a production environment you might want to enable this so that templates don’t have to be built on-demand but are retrieved from a cache.

Key com.blazebit.persistence.view.eager_loading

Type

boolean

Default

false

Applicable

Configuration only

21.1.3. PROXY_UNSAFE_ALLOWED

Defines whether proxy classes that support using the getter methods in a constructor should be allowed. These proxy classes have to be defined via sun.misc.Unsafe to avoid class verification errors. Disabling this property makes the use of the getter in the constructor return the default value for the property instead of the actual value.

Key com.blazebit.persistence.view.proxy.unsafe_allowed

Type

boolean

Default

true

Applicable

Configuration only

21.1.4. EXPRESSION_VALIDATION_DISABLED

Defines whether the expressions of entity view mappings should be validated.

Key com.blazebit.persistence.view.expression_validation_disabled

Type

boolean

Default

true

Applicable

Configuration only

21.1.5. DEFAULT_BATCH_SIZE

Defines the default batch size to be used for attributes that are fetched via the SELECT fetch strategy. To specify the batch size of a specific attribute, append the attribute name after the "batch_size" like e.g. com.blazebit.persistence.view.batch_size.subProperty

Key com.blazebit.persistence.view.batch_size

Type

int

Default

1

Applicable

Always

21.1.6. EXPECT_BATCH_CORRELATION_VALUES

This was deprecated in favor of EXPECT_BATCH_MODE.

Defines whether by default batching of correlation values or view root ids is expected for attributes that are fetched via the SELECT fetch strategy. To specify the batch expectation of a specific attribute, append the attribute name after the "batch_correlation_values" like e.g. com.blazebit.persistence.view.batch_correlation_values.subProperty

Key com.blazebit.persistence.view.batch_correlation_values

Type

boolean

Default

true

Applicable

Always

21.1.7. EXPECT_BATCH_MODE

Defines the expected batch mode i.e. whether correlation values, view root ids or embedding view ids are expected to be batched for attributes that are fetched via the SELECT fetch strategy. To specify the batch expectation of a specific attribute, append the attribute name after the "batch_mode" like e.g. com.blazebit.persistence.view.batch_mode.subProperty

Key com.blazebit.persistence.view.batch_mode

Type

String

Default

values, view_roots, embedding_views

Applicable

Always

21.1.8. UPDATER_EAGER_LOADING

Defines whether entity view updater objects should be created eagerly when creating the EntityViewManager or on demand. To improve startup performance this is deactivated by default. In a production environment you might want to enable this so that updaters don’t have to be built on-demand but are retrieved from a cache.

Key com.blazebit.persistence.view.updater.eager_loading

Type

boolean

Default

false

Applicable

Configuration only

21.1.9. UPDATER_FLUSH_MODE

Defines the flush mode the entity view updater objects should be using which is normally defined via @UpdatableEntityView(mode = ...). This is a global override. To override the flush mode of a specific class, append the fully qualified class name after the "flush_mode" part like e.g. com.blazebit.persistence.view.updater.flush_mode.com.test.MyUpdatableCatView.

Key com.blazebit.persistence.view.updater.flush_mode

Type

String

Values

partial, lazy or full

Default

none

Applicable

Configuration only

21.1.10. UPDATER_FLUSH_STRATEGY

Defines the flush strategy the entity view updater objects should be using which is normally defined via @UpdatableEntityView(strategy = ...). This is a global override. To override the flush strategy of a specific class, append the fully qualified class name after the "flush_strategy" part like e.g. com.blazebit.persistence.view.updater.flush_strategy.com.test.MyUpdatableCatView.

Key com.blazebit.persistence.view.updater.flush_strategy

Type

String

Values

entity or query

Default

none

Applicable

Configuration only

21.1.11. UPDATER_DISALLOW_OWNED_UPDATABLE_SUBVIEW

Defines whether the use of an updatable entity view type for owned relationships is disallowed. By default the use is disallowed i.e. the default value is true, but since there might be strange models out there, it is possible to allow this.

The main reason to disallow this, is that this kind of usage would break the idea of a separate model per use case, but there is also technical reason. Updatable entity views are only allowed to have a single parent object due to the way dirty tracking is implemented. This is not necessarily a limitation, but was simply done this way because the developers believe in the model per use case approach and want to encourage this way of working.

During loading of entity views, tuples are transformed into entity views. Updatable entity views are de-duplicated i.e. if another tuple would be transformed, it uses the existing object instead. During construction of an entity view all it’s child views are registered for dirty tracking. Since an updatable view may only have one parent, and owned *ToOne relationships do not guarantee that the relationship object will only have one parent, this will result in a runtime exception depending on the data.

Beware that allowing updatable entity view types for *ToOne relationships might lead to these exceptions at runtime if the relationship isn’t logically a OneToOne.

Key com.blazebit.persistence.view.updater.disallow_owned_updatable_subview

Type

boolean

Default

true

Applicable

Configuration only

21.1.12. UPDATER_STRICT_CASCADING_CHECK

Defines whether the strict cascading check that disallows setting updatable or creatable entity views on non-cascading attributes before being associated with a cascading attribute is enabled. When disabled, it is possible, like in JPA, that the changes done to an updatable entity view are not flushed when it is not associated with an attribute that cascades updates. By default the use is enabled i.e. the default value is true.

Key com.blazebit.persistence.view.updater.strict_cascading_check

Type

boolean

Default

true

Applicable

Configuration only

21.1.13. UPDATER_ERROR_ON_INVALID_PLURAL_SETTER

Defines whether warnings or boot time validation errors should be emitted when invalid plural attribute setters are encountered while the strict cascading check is enabled. When true, a boot time validation error is thrown when encountering an invalid setter, otherwise just a warning. This configuration has no effect when the strict cascading check is disabled. By default the use is disabled i.e. the default value is false.

Key com.blazebit.persistence.view.updater.error_on_invalid_plural_setter

Type

boolean

Default

false

Applicable

Configuration only

21.1.14. PAGINATION_DISABLE_COUNT_QUERY

Defines whether the pagination count query should be disabled when applying a EntityViewSetting to a CriteriaBuilder. When true, the pagination count query is disabled via PaginatedCriteriaBuilder.withCountQuery(false). By default the pagination count query is enabled i.e. the default value is false.

Key com.blazebit.persistence.view.pagination.disable_count_query

Type

boolean

Default

false

Applicable

EntityViewSetting only

21.1.15. PAGINATION_EXTRACT_ALL_KEYSETS

Defines whether the pagination query should extract all keysets rather than just the first and last ones. When true, the keyset extraction is enabled via PaginatedCriteriaBuilder.withExtractAllKeysets(true). By default only the first and last keysets are extracted i.e. the default value is false.

Key com.blazebit.persistence.view.pagination.extract_all_keysets

Type

boolean

Default

false

Applicable

EntityViewSetting only

21.1.16. PAGINATION_FORCE_USE_KEYSET

Defines whether the pagination query should force the usage of a keyset when available even if page or page size changes. By default only the first and last keysets are extracted i.e. the default value is false.

Key com.blazebit.persistence.view.pagination.force_use_keyset

Type

boolean

Default

false

Applicable

EntityViewSetting only

21.1.17. PAGINATION_HIGHEST_KEYSET_OFFSET

Defines the offset from the maxResults at which to find the highest keyset i.e. the highest keyset will be at position Math.min(size, maxResults - offset). Setting 1 along with a maxResults + 1 allows to look ahead one element to check if there are more elements which is useful for pagination with lazy page count or endless scrolling. By default the offset is disabled i.e. the default value is null.

Key com.blazebit.persistence.view.pagination.highest_keyset_offset

Type

integer

Default

null

Applicable

EntityViewSetting only

21.1.18. PAGINATION_BOUNDED_COUNT

Defines the maximum value up to which the count query should count. By default the bounded count is disabled i.e. all rows are counted.

Key com.blazebit.persistence.view.pagination.bounded_count

Type

integer

Default

null

Applicable

EntityViewSetting only

21.1.19. STATIC_BUILDER_SCANNING_DISABLED

Defines whether the scanning for @StaticBuilder classes for the registered entity views should be disabled. When true, the scanning is disabled which improves startup performance but causes that entity view builders returned via EntityViewManager.createBuilder() will use a generic implementation. By default the scanning is enabled i.e. the default value is false.

Key com.blazebit.persistence.view.static_builder_scanning_disabled

Type

boolean

Default

false

Applicable

Configuration only

21.1.20. STATIC_IMPLEMENTATION_SCANNING_DISABLED

Defines whether the scanning for @StaticImplementation classes for the registered entity views should be disabled. When true, the scanning is disabled which improves startup performance but causes that entity view implementations will be generated at runtime. By default the scanning is enabled i.e. the default value is false.

Key com.blazebit.persistence.view.static_implementation_scanning_disabled

Type

boolean

Default

false

Applicable

Configuration only

21.1.21. STATIC_METAMODEL_SCANNING_DISABLED

Defines whether the scanning for @StaticMetamodel classes for the registered entity views should be disabled. When true, the scanning is disabled which improves startup performance but causes that the static metamodels are not initialized. By default the scanning is enabled i.e. the default value is false.

Key com.blazebit.persistence.view.static_metamodel_scanning_disabled

Type

boolean

Default

false

Applicable

Configuration only

21.1.22. CREATE_EMPTY_FLAT_VIEWS

Defines whether empty flat views should be created by default if not specified via @EmptyFlatViewCreation. When false, null will be set for an attribute if the flat view would be empty, otherwise an empty flat view is set. By default the creation of empty flat views is enabled i.e. the default value is true.

Key com.blazebit.persistence.view.create_empty_flat_views

Type

boolean

Default

true

Applicable

Configuration only

22. FAQ

This section tries to cover some standard questions that often come up when introducing entity views or updatable entity views into a project as well as some common problems with explanations and possible solutions.

22.1. Why do I get an optimistic lock exception when updating an updatable entity view?

The com.blazebit.persistence.view.OptimisticLockException is very similar to the javax.persistence.OptimisticLockException and when thrown, it signals that an update isn’t possible because of a change of an object that happened in the meantime. This can also happen when you do not use optimistic locking explicitly.

22.1.1. If you try to update a non-existent entity

When trying to update an entity that does not exist, the EntityViewManager.update operation will throw the com.blazebit.persistence.view.OptimisticLockException.

  • The entity could have been deleted in the meantime i.e. between loading the view and the update operation

  • The entity view causing the exception is the result of a wrong usage of EntityViewManager.convert as it is missing the ConvertOption.CREATE_NEW

22.1.2. If you try to update a concurrently updated entity

Either the entity was updated within the current transaction or within another transaction through a different mechanism or a different entity view object. If an update in a different transaction caused the exception, it is necessary to load the new version of the entity view and let the end-user enter the values to update again. By inspecting the change model of the old instance one can assist the user by copying over non-conflicting value changes and just highlight conflicting changes.

If a previous update in the same transaction causes the exception, the code should be adapted to prevent this from happening or updating the version on the entity view accordingly.

22.2. Why do I get a "could not invoke proxy constructor" exception when fetching entity views?

Entity views are type checked for most parts, but there are some dynamic non-declarative parts that can’t be type checked that might cause this runtime exception when using a wrong result. Usually, this happens when a SubqueryProvider or CorrelationProvider is in use. The implementations of these classes define the result type in a manner that is not type checkable.

If a SubqueryProvider returns an integer via e.g. select("1") or select("someIntAttribute"), but the entity view attribute using the subquery provider uses a different type like e.g. boolean, constructing an instance of that entity view might fail when trying to interpret the integer as boolean with an IllegalArgumentException saying that types are incompatible. The obvious fix is to correct either the select item to return the correct type or the entity view attribute to declare the appropriate attribute type. In case of a subquery provider it is also possible to wrap the subquery into a more complex expression by using e.g. @MappingSubquery(value = MyProvider.class, subqueryAlias = "subquery", expression = "CASE WHEN EXISTS subquery THEN true ELSE false END").

A CorrelationProvider can fail in a similar manner as it defines the entity type it correlates via correlate(SomeEntity.class). If the entity view attribute expects a different type that is not compatible, it will fail at runtime with an IllegalArgumentException saying that types are incompatible. If a correlation result is defined via @MappingCorrelated(correlationResult = "someAttributeOfCorrelatedEntity") the type of that expression must be compatible which can be another cause for an error. This problem can be fixed by adapting the correlation result expression, by changing the correlated entity in the correlation provider or by changing the declared attribute type.