3.5.3.2. Data Loaders
Loaders are designed to load data from the middle tier to containers.
There are slightly different interfaces of loaders depending on containers they work with:
-
InstanceLoader
loads a single instance toInstanceContainer
by entity id or JPQL query. -
CollectionLoader
loads a collection of entities toCollectionContainer
by a JPQL query. You can specify paging, sorting and other optional parameters. -
KeyValueCollectionLoader
loads a collection ofKeyValueEntity
instances toKeyValueCollectionContainer
. In addition toCollectionLoader
parameters, you can specify a data store name.
In screen XML descriptors, all loaders are defined by the same <loader>
element and the type of a loader is determined by what container it is enclosed in.
Loaders are optional because you can just load data using DataManager
or your custom service and set directly to containers, but they simplify this process in declaratively defined screens, especially with the Filter component. Usually, a collection loader obtains a JPQL query from the screen XML descriptor and query parameters from the filter component, creates LoadContext
and invokes DataManager
to load entities. So the typical XML descriptor looks like this:
<data>
<collection id="customersDc" class="com.company.sample.entity.Customer" view="_local">
<loader id="customersDl">
<query>
select e from sample_Customer e
</query>
</loader>
</collection>
</data>
<layout>
<filter id="filter" applyTo="customersTable" dataLoader="customersDl">
<properties include=".*"/>
</filter>
<!-- ... -->
</layout>
Attributes of the loader
XML element allow you to define optional parameters like cacheable
, softDeletion
, etc.
In an entity editor screen, the loader XML element is usually empty, because the instance loader requires an entity identifier which is specified programmatically by the StandardEditor
base class:
<data>
<instance id="customerDc" class="com.company.sample.entity.Customer" view="_local">
<loader/>
</instance>
</data>
Loaders can delegate actual loading to a function which can be provided using the setLoadDelegate()
method or declaratively using the @Install
annotation in the screen controller, for example:
@Inject
private DataManager dataManager;
@Install(to = "customersDl", target = Target.DATA_LOADER)
protected List<Customer> customersDlLoadDelegate(LoadContext<Customer> loadContext) {
return dataManager.loadList(loadContext);
}
In the example above, the customersDlLoadDelegate()
method will be used by the customersDl
loader to load the list of Customer
entities. The method accepts LoadContext
which will be created by the loader based on its parameters: query, filter (if any), etc. In the example, the loading is done via DataManager
which is effectively the same as the standard loader implementation, but you can use a custom service or perform any post-processing of the loaded entities.
You can listen to PreLoadEvent
and PostLoadEvent
to add some logic before or after loading:
@Subscribe(id = "customersDl", target = Target.DATA_LOADER)
private void onCustomersDlPreLoad(CollectionLoader.PreLoadEvent<Customer> event) {
// do something before loading
}
@Subscribe(id = "customersDl", target = Target.DATA_LOADER)
private void onCustomersDlPostLoad(CollectionLoader.PostLoadEvent<Customer> event) {
// do something after loading
}
A loader can also be created and configured programmatically, for example:
@Inject
private DataComponents dataComponents;
private void createCustomerLoader(CollectionContainer<Customer> container) {
CollectionLoader<Customer> loader = dataComponents.createCollectionLoader();
loader.setQuery("select e from sample_Customer e");
loader.setContainer(container);
loader.setDataContext(getScreenData().getDataContext());
}
When DataContext is set for a loader (which is always the case when the loader is defined in XML descriptor), all loaded entities are automatically merged into the data context.
- Query conditions
-
Sometimes you need to modify a data loader query at runtime to filter the loaded data at the database level. The simplest way to provide filtering based on parameters entered by users is to connect the Filter visual component to the data loader.
Instead of the universal filter or in addition to it, you can create a set of conditions for the loader query. A condition is a set of query fragments with parameters. These fragments are added to the resulting query text only when all parameters used in the fragments are set for the query. Conditions are processed on the data store level, so they can contain fragments of different query languages supported by data stores. The framework provides conditions for JPQL.
Let’s consider creating a set of conditions for filtering a
Customer
entity by two of its attributes: stringname
and booleanstatus
.Loader query conditions can be defined either declaratively in the
<condition>
XML element, or programmatically using thesetCondition()
method. Below is an example of configuring the conditions in XML:<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" xmlns:c="http://schemas.haulmont.com/cuba/screen/jpql_condition.xsd" (1) caption="Customers browser" focusComponent="customersTable"> <data> <collection id="customersDc" class="com.company.demo.entity.Customer" view="_local"> <loader id="customersDl"> <query><![CDATA[select e from demo_Customer e]]> <condition> (2) <and> (3) <c:jpql> (4) <c:where>e.name like :name</c:where> </c:jpql> <c:jpql> <c:where>e.status = :status</c:where> </c:jpql> </and> </condition> </query> </loader> </collection> </data>
1 - add the JPQL conditions namespace 2 - define the condition
element insidequery
3 - if you have more than one condition, add and
oror
element4 - define a JPQL condition with optional join
element and mandatorywhere
elementSuppose that the screen has two UI components for entering the condition parameters:
nameFilterField
text field andstatusFilterField
check box. In order to refresh the data when a user changes their values, add the following event listeners to the screen controller:@Inject private CollectionLoader<Customer> customersDl; @Subscribe("nameFilterField") private void onNameFilterFieldValueChange(HasValue.ValueChangeEvent<String> event) { if (event.getValue() != null) { customersDl.setParameter("name", "(?i)%" + event.getValue() + "%"); (1) } else { customersDl.removeParameter("name"); } customersDl.load(); } @Subscribe("statusFilterField") private void onStatusFilterFieldValueChange(HasValue.ValueChangeEvent<Boolean> event) { if (event.getValue()) { customersDl.setParameter("status", true); } else { customersDl.removeParameter("status"); } customersDl.load(); }
1 - notice how we use Case-Insensitive Substring Search provided by ORM As mentioned above, a condition is included in the query only when its parameters are set. So the resulting query executed on the database will depend on what is entered in the UI components:
Only nameFilterField has a valueselect e from demo_Customer e where e.name like :name
Only statusFilterField has a valueselect e from demo_Customer e where e.status = :status
Both nameFilterField and statusFilterField have valuesselect e from demo_Customer e where (e.name like :name) and (e.status = :status)