Data Repositories

Spring Data repositories provide a useful abstraction for working with entities, especially when implementing business logic.

Jmix data repositories are built on top of Spring Data and use DataManager under the hood. It allows you to utilize the convenient repository interface while having the full support for advanced Jmix data access features, such as entity events, cross-datastore references, data access checks, etc.

Working with Data Repositories

You can create a Jmix data repository in two ways: using the Data Repository Wizard or following the steps below.

  1. Create an interface extending JmixDataRepository. Use the entity class and the entity identifier class as type parameters for JmixDataRepository. For example:

    public interface CustomerRepository extends JmixDataRepository<Customer, UUID> {
    }
  2. Create a new Spring configuration class in the base package and add the @EnableJmixDataRepositories annotation to it:

    package com.company.demo;
    
    import io.jmix.core.repository.EnableJmixDataRepositories;
    import org.springframework.context.annotation.Configuration;
    
    @EnableJmixDataRepositories
    @Configuration
    public class JmixDataRepositoryConfiguration {
    }

    Jmix will initialize all data repositories located under the base package of the application or the add-on. If you need more customized control over where to search for repositories, use the basePackages, excludeFilters and includeFilters attributes of the annotation.

  3. Inject your repository into Spring beans or UI controllers using the @Autowired annotation:

    @Autowired
    private CustomerRepository customerRepository;

JmixDataRepository Features

The JmixDataRepository interface extends the standard Spring Data PagingAndSortingRepository. It provides a few own methods considering Jmix specifics:

  • Load methods, such as findById() or findAll(), can accept a fetch plan.

  • create() method instantiates a new entity.

  • getById() method with the non-optional result loads an entity by id and throws the exception if the entity is not found.

  • getDataManager() method returns DataManager to use in default methods.

  • save() method persists the provided entity and returns saved instance, loaded with the specified fetch plan. The method accepts the entity to be saved and fetch plan to apply when reloading the saved entity. The entity cannot be null, and fetch plan must be applicable to the entity.

JmixDataRepositoryContext

Load methods of repositories inherited from JmixDataRepository support an additional argument of the JmixDataRepositoryContext type. It allows you to pass the filtering, paging and sorting parameters collected from UI components to the LoadContext object. As a result, all the features of genericFilter, simplePagination and dataGrid components will work seamlessly with data repositories.

Use the JmixDataRepositoryUtils.buildRepositoryContext() static method to convert LoadContext to JmixDataRepositoryContext.

Security Constraints

You can add the io.jmix.core.repository.ApplyConstraints annotation to your data repository. If the value of the annotation is false, the repository uses UnconstrainedDataManager instead of DataManager. The default annotation value is true.

The @ApplyConstraints annotation can be used not only for the entire class, but also for individual methods to either ignore or enable constraints for them only.

public interface OrderRepository extends JmixDataRepository<Order, UUID> {
    @Override
    List<Order> findAll(Sort sort, @Nullable FetchPlan fetchPlan);

    @Override
    @ApplyConstraints(false)
    List<Order> findAll(FetchPlan fetchPlan);

    @ApplyConstraints(false)
    List<Order> findByIdNotNull();
}

In the example above, @ApplyConstraints(false) is applied specifically to two methods, and they will use UnconstrainedDataManager.

In the example below, constraints are disabled for the entire class, but are specifically enabled for individual methods:

@ApplyConstraints(false)
public interface ProductRepository extends JmixDataRepository<Product, UUID> {
    @Override
    List<Product> findAll(Sort sort, @Nullable FetchPlan fetchPlan);

    @Override
    @ApplyConstraints
    Page<Product> findAll(Pageable pageable);

    List<Product> getByIdNotNull();

    @ApplyConstraints
    List<Product> searchByIdNotNull();

    List<Product> searchById(UUID id);
}

The findAll() and searchByIdNotNull() methods will use the regular DataManager, while all other methods will use UnconstrainedDataManager.

Annotations

You can use the following annotations on custom query methods:

  • @io.jmix.core.repository.Query defines a JPQL query string, similar to the Spring Data JPA @Query annotation.

  • @io.jmix.core.repository.FetchPlan defines a fetch plan to be used when loading data.

  • @io.jmix.core.repository.QueryHints and @jakarta.persistence.QueryHint allow you to specify hints for turning soft deletion off and using query cache.

If a method name/query and method parameters specify different values for fetch plan and hints, the final values are based on priority, from highest to lowest.

FetchPlan:

  1. FetchPlan parameter. The fetch plan explicitly provided as a parameter to the method takes highest priority.

  2. JmixDataRepositoryContext#fetchPlan parameter.

  3. @FetchPlan annotation value.

Hints:

  1. JmixDataRepositoryContext parameter.

  2. @QueryHints annotation value.

For hints with the same key, the value from the higher priority source will override the value from the lower priority source. Different keys will be merged.

Query Method Examples

Derived Methods

Jmix data repositories support the standard Spring Data feature of deriving the query from the method name, for example:

List<Customer> findByEmailContainingIgnoreCase(String emailPart);

Query methods can accept Pageable for pagination and sorting:

Page<Customer> findByEmailContainingIgnoreCase(String emailPart, Pageable pageable);

@Query Annotation

Similar to Spring Data JPA, a JPQL query can be defined explicitly using the @io.jmix.core.repository.Query annotation:

@Query("select c from sample_Customer c where c.email like :email")
List<Customer> findCustomersByEmail(@Param("email") String emailPart);

If the query returns multiple scalar values and/or aggregates, you should provide property names in the properties attribute of the @Query annotation. The method should return a list of KeyValueEntity. Each entity instance will have the properties defined for the query. For example:

@Query(
        value = "select c.grade, count(c) from sample_Customer c group by c.grade",
        properties = {"grade", "count"}
)
List<KeyValueEntity> getCountGroupByGrade();

If the query returns a list with a single scalar value, define the method return type as a list of that value:

@Query(value = "select distinct c.grade from sample_Customer c")
List<CustomerGrade> getAllGrades();

If the query returns a single scalar value, define the method return type as that value:

@Query(value = "select count(c) from sample_Customer c where c.grade = ?1")
Long getCountByGrade(CustomerGrade grade);

Jmix-Specific Parameters

Jmix data repositories accept fetch plan as a parameter of query methods:

List<Customer> findByEmailContainingIgnoreCase(String emailPart, FetchPlan fetchPlan);

A shared fetch plan can be defined in the @io.jmix.core.repository.FetchPlan annotation on the query method:

@FetchPlan("customer-minimal")
List<Customer> findByEmail(String email);

A cacheable query can be specified using the @io.jmix.core.repository.QueryHints and @jakarta.persistence.QueryHint annotations:

@QueryHints(@QueryHint(name = PersistenceHints.CACHEABLE, value = "true"))
List<Customer> findByEmail(String email);