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();
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 likeTREAT(..).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
andNavigableSet
-
SortedMap
andNavigableMap
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 asMap
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 aList
again. To do so useignoreIndex
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
orMap
can specify a collection value to collect all values grouped by their index value. Valid types for the collections areCollection
,Set
,SortedSet
andList
.
@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 thethis
mapping is used for the index mapping which allows the key view to be based on the target mappingkittens
.
@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 aLinkedHashSet
.
@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
- TheCorrelationProvider
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 aCorrelationProvider
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(); }
Default attribute filters are enabled by calling addAttributeFilter(String attributeName, Object filterValue)
whereas named filters require calling addAttributeFilter(String attributeName, String filterName, Object filterValue)
.
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 - |
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<?>
andObject
- 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
viapage(int firstResult, int maxResults)
create(Class<T> entityViewClass, Object entityId, int maxRows)
-
Creates a entity view setting that will apply pagination to a
CriteriaBuilder
viapageAndNavigate(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
-
A
Person
entity is created with the defined properties of theOwnerCreateView
-
The
Person
entity is persisted viaEntityManager.persist()
-
The generated identifier is set on the
OwnerCreateView
object -
The
OwnerCreateView
object is converted to the context specific declared typeOwnerView
-
The
OwnerCreateView
object is replaced by theOwnerView
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 viaEntityViewManager.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 aCriteriaBuilder
which also happens implicitly when usingEntityViewManager.find()
. Another way to load is to get a reference for an entity view viaEntityViewManager.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()
orEntityViewManager.saveFull()
/EntityViewManager.saveFullTo()
/EntityViewManager.saveFullWith()
/EntityViewManager.saveFullWithTo()
as well as implicitly forCascadeType.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 theowner_id
column -
Updatability of the relation type represented by the entity view
PersonView
or more specifically the row in theperson
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 toNULL
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 |
||
@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 |
|
@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 |
||
@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 |
|
@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 JPAPESSIMISTIC_READ
lock when reading the entity view -
LockMode.PESSIMISTIC_WRITE
- Acquires a JPAPESSIMISTIC_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
If you miss a type you can register it via EntityViewConfiguration.registerBasicUserType(Class type, BasicUserType userType)
.
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 customBasicUserType
.
If found on the classpath, TypeConverters
for the following types are registered
-
java.util.Optional
for all Object types -
java.util.OptionalInt
forjava.lang.Integer
-
java.util.OptionalLong
forjava.lang.Long
-
java.util.OptionalDouble
forjava.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
forjava.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
-
If you miss a TypeConverter
you can register it via EntityViewConfiguration.registerTypeConverter(Class entityModelType, Class viewModelType, TypeConverter typeConverter)
.
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 thecom.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
-
At some point there will also be support for JAXB
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.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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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
onEntityViewConfiguration
. - 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.
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
ifCollectionType.COLLECTION
-
ListAttribute
ifCollectionType.LIST
-
SetAttribute
ifCollectionType.SET
-
MapAttribute
ifCollectionType.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. Defaultfalse
-
addGenerationDate
to add the generation date to the generated java files. Defaultfalse
-
addGeneratedAnnotation
to add the@Generated
annotation to the generated java files. Defaulttrue
-
addSuppressWarningsAnnotation
to add the@SuppressWarnings
annotation to the generated java files. Defaultfalse
-
strictCascadingCheck
whether to generate strict cascading checks for entity view implementations. Defaulttrue
-
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. Iffalse
, thegenerateBuilders
option is meaningless. Defaulttrue
-
generateBuilders
whether to generate entity view builders. Defaulttrue
-
createEmptyFlatViews
whether to create empty flat views by default unless specified via@EmptyFlatViewCreation
. Default istrue
-
generateDeepConstants
whether to create nested classes for subview attributes that allow deep static referencing into a model. Default istrue
-
optionalParameters
a semicolon separated list of names with optional types in the formatNAME=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 theConvertOption.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.