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 to InstanceContainer by an entity id or JPQL query.

  • CollectionLoader loads a collection of entities to CollectionContainer by a JPQL query. You can specify paging, sorting and other optional parameters.

  • KeyValueCollectionLoader loads a collection of KeyValueEntity instances to KeyValueCollectionContainer. In addition to CollectionLoader parameters, you can specify a data store name.

In view 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 views.

Usually, a collection loader obtains a JPQL query from the view XML descriptor, creates LoadContext and invokes DataManager to load entities. So the typical XML descriptor looks like this:

<collection id="departmentsDc"
            class="com.company.onboarding.entity.Department">
    <fetchPlan extends="_base">
        <property name="hrManager" fetchPlan="_base"/>
    </fetchPlan>
        <loader id="departmentsDl" readOnly="true">
        <query>
            <![CDATA[select e from Department e]]>
        </query>
    </loader>
</collection>

In an entity detail view, the loader XML element is usually empty, because the instance loader requires an entity identifier that is specified programmatically by the StandardDetailView base class:

<instance id="departmentDc"
          class="com.company.onboarding.entity.Department">
    <fetchPlan extends="_base">
        <property name="hrManager" fetchPlan="_base"/>
    </fetchPlan>
    <loader id="departmentDl"/>
</instance>

If the loader does not have the readOnly="true" attribute in XML or if the DataContext is set for the loader programmatically, 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 view controllers.

To generate a handler stub in Jmix Studio, select the element in the view descriptor XML or in the Jmix UI structure panel and use the Handlers tab of the Jmix UI inspector panel.

Alternatively, you can use the Generate Handler button in the top panel of the view controller.

loadDelegate

Loaders can delegate actual loading to a method of the view controller, where you can call a custom service instead of DataManager used by default. For example:

@Autowired
private DepartmentService departmentService;

@Install(to = "departmentsDl", target = Target.DATA_LOADER) (1)
private List<Department> departmentsDlLoadDelegate(final LoadContext<Department> loadContext) { (2)
    LoadContext.Query query = loadContext.getQuery();
    return departmentService.loadDepartments( (3)
            query.getCondition(),
            query.getSort(),
            query.getFirstResult(),
            query.getMaxResults()
    );
}
1 The departmentsDlLoadDelegate() method will be used by the departmentsDl loader to load the list of Department entities.
2 The method accepts LoadContext that will be created by the loader based on its parameters: query, conditions (if any), etc.
3 The loading is done via DepartmentService.loadDepartments() method which accepts the filtering conditions, sorting and pagination set to the loader by visual components of the view.

Apart from invoking a custom service, the load delegate allows you to perform any post-processing of the loaded entities.

PreLoadEvent

This event is sent before loading entities.

@Subscribe(id = "departmentsDl", target = Target.DATA_LOADER)
public void onDepartmentsDlPreLoad(final CollectionLoader.PreLoadEvent<Department> 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 = "departmentsDl", target = Target.DATA_LOADER)
public void onDepartmentsDlPostLoad(final CollectionLoader.PostLoadEvent<Department> 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 propertyFilter or genericFilter 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 out-of-the-box handling of conditions for JPQL.

Let’s consider creating a set of conditions for filtering the Department entity by its name and hrManager attributes.

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:

<view xmlns="http://jmix.io/schema/flowui/view"
      xmlns:c="http://jmix.io/schema/flowui/jpql-condition"> (1)
    <data readOnly="true">
        <collection id="departmentsDc"
                    class="com.company.onboarding.entity.Department">
            <fetchPlan extends="_base">
                <property name="hrManager" fetchPlan="_base"/>
            </fetchPlan>
            <loader id="departmentsDl">
                <query>
                    <![CDATA[select e from Department e]]>
                    <condition> (2)
                        <and> (3)
                            <c:jpql> (4)
                                <c:where>e.name like :name</c:where>
                            </c:jpql>
                            <c:jpql>
                                <c:where>e.hrManager = :hrManager</c:where>
                            </c:jpql>
                        </and>
                    </condition>
                </query>
            </loader>
        </collection>
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 view has nameFilterField text field and hrManagerFilterField combo box for entering the condition parameters:

<textField id="nameFilterField" label="Name"/>
<entityComboBox id="hrManagerFilterField" label="HR Manager"
                metaClass="User" itemsContainer="usersDc">
    <actions>
        <action id="entityClear" type="entity_clear"/>
    </actions>
</entityComboBox>

In order to refresh the data when a user changes their values, add the following event listeners to the view controller:

@ViewComponent
private CollectionLoader<Department> departmentsDl;

@Subscribe("nameFilterField")
public void onNameFilterFieldComponentValueChange(final AbstractField.ComponentValueChangeEvent<TextField, String> event) {
    departmentsDl.setParameter("name", "(?i)%" + event.getValue() + "%");
    departmentsDl.load();
}

@Subscribe("hrManagerFilterField")
public void onHrManagerFilterFieldComponentValueChange(final AbstractField.ComponentValueChangeEvent<EntityComboBox<User>, User> event) {
    departmentsDl.setParameter("hrManager", event.getValue());
    departmentsDl.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:

Only nameFilterField has a value
select e from Department e where e.name like :name
Only hrManagerFilterField has a value
select e from Department e where e.hrManager = :hrManager
Both nameFilterField and hrManagerFilterField have values
select e from Department e where (e.name like :name) and (e.hrManager = :hrManager)