Data Loaders
Loaders are designed to load data to containers.
There are slightly different interfaces of loaders depending on containers they work with:
-
InstanceLoader
loads a single instance toInstanceContainer
by an 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 it 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 readOnly="true">
<collection id="customersDc"
class="ui.ex1.entity.Customer">
<fetchPlan extends="_base"/>
<loader id="customersDl" >
<query>
<![CDATA[select e from uiex1_Customer e]]>
</query>
</loader>
</collection>
</data>
<layout expand="customersTable" spacing="true">
<filter id="filter"
dataLoader="customersDl">
<properties include=".*"/>
</filter>
<!-- ... -->
</layout>
In the entity editor screen, the loader XML element is usually empty, because the instance loader requires an entity identifier that is specified programmatically by the StandardEditor
base class:
<data>
<instance id="customerDc"
class="ui.ex1.entity.Customer">
<fetchPlan extends="_base"/>
<loader/>
</instance>
</data>
A loader can also be created and configured programmatically, for example:
@Autowired
private DataComponents dataComponents;
private CollectionLoader<Customer> customersDl;
private void createCustomerLoader(CollectionContainer<Customer> container) {
customersDl = dataComponents.createCollectionLoader();
customersDl.setQuery("select e from uiex1_Customer e");
customersDl.setContainer(container);
customersDl.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.
Events and Handlers
This section describes the data loader lifecycle events that can be handled in screen controllers.
To generate a handler stub in Jmix Studio, select the data container element in the screen descriptor XML or in the Component Hierarchy panel and use the Handlers tab of the Component Inspector panel. Alternatively, you can use the Generate Handler button in the top panel of the screen controller. |
loadDelegate
Loaders can delegate actual loading to a method of the screen controller, where you can call a custom service instead of DataManager
used by default. For example:
@Autowired
private CustomerService customerService;
@Install(to = "customersDl", target = Target.DATA_LOADER) (1)
protected List<Customer> customersDlLoadDelegate(LoadContext<Customer> loadContext) { (2)
LoadContext.Query query = loadContext.getQuery();
return customerService.loadCustomers( (3)
query.getCondition(),
query.getSort(),
query.getFirstResult(),
query.getMaxResults()
);
}
1 | The customersDlLoadDelegate() method will be used by the customersDl loader to load the list of Customer entities. |
2 | The method accepts LoadContext that will be created by the loader based on its parameters: query, filter (if any), etc. |
3 | The loading is done via CustomerService.loadCustomers() method which accepts the filtering conditions, sorting and pagination set to the loader by visual components of the screen. |
Apart from invoking a custom service, the load delegate allows you to perform any post-processing of the loaded entities.
If you declare custom data loading with a delegate, and you display the loaded data in the table with a pagination component (Pagination or SimplePagination), then you may also need to define the custom logic to count total number of rows. Take a look at TotalCountDelegate handler for the pagination component associated with the table.
PreLoadEvent
This event is sent before loading entities.
@Subscribe(id = "customersDl", target = Target.DATA_LOADER)
public void onCustomersDlPreLoad(CollectionLoader.PreLoadEvent<Customer> event) {
// some actions before loading
}
You can prevent load using the preventLoad()
method of the event.
PostLoadEvent
This event is sent after successful loading of entities, merging them into DataContext
and setting to the container.
@Subscribe(id = "customersDl", target = Target.DATA_LOADER)
public void onCustomersDlPostLoad(CollectionLoader.PostLoadEvent<Customer> event) {
// some actions after loading
}
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 the Persone
entity by the name
attribute.
Loader query conditions can be defined either declaratively in the <condition>
XML element, or programmatically using the setCondition()
method. Below is an example of configuring the conditions in descriptor:
<window xmlns="http://jmix.io/schema/ui/window"
xmlns:c="http://jmix.io/schema/ui/jpql-condition">(1)
<data readOnly="true">
<collection id="personsDc"
class="ui.ex1.entity.Person">
<fetchPlan extends="_base"/>
<loader id="personsDl">
<query>
<![CDATA[select e from uiex1_Person 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 | Adds the JPQL conditions namespace. |
2 | Defines the condition element inside the query. |
3 | If you have more than one condition, add and or or element |
4 | Defines a JPQL condition with optional join element and mandatory where element. |
Suppose that the screen has two UI components for entering the condition parameters: nameFilterField
text field and statusFilterField
check box. In order to refresh the data when a user changes their values, add the following event listeners to the screen controller:
@Autowired
private CollectionLoader<Person> personsDl;
@Subscribe("nameFilterField")
public void onNameFilterFieldValueChange1(HasValue.ValueChangeEvent event) {
if (event.getValue() != null) {
personsDl.setParameter("name", "(?i)%" + event.getValue() + "%");
} else {
personsDl.removeParameter("name");
}
personsDl.load();
}
@Subscribe("statusFilterField")
public void onStatusFilterFieldValueChange(HasValue.ValueChangeEvent<Boolean> event) {
if (event.getValue()) {
personsDl.setParameter("status", true);
} else {
personsDl.removeParameter("status");
}
personsDl.load();
}
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:
select e from uiex1_Person e where e.name like :name
select e from uiex1_Person e where e.status = :status
select e from uiex1_Person e where (e.name like :name) and (e.status = :status)