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. Add @EnableJmixDataRepositories annotation to the main application class or the add-on configuration:

    import io.jmix.core.repository.EnableJmixDataRepositories;
    // ...
    
    @SpringBootApplication
    @EnableJmixDataRepositories
    public class DemoApplication implements AppShellConfigurator {

    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.

You can pass io.jmix.core.querycondition.Condition and other Jmix-specific parameters to Jmix Data Repositories using the JmixDataRepositoryContext parameter. This parameter is applicable to both standard and query methods of Jmix Data Repositories, allowing for dynamic changes at runtime.

You can apply 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
    Iterable<Order> findAll(Sort sort, @Nullable FetchPlan fetchPlan);

    @Override
    @ApplyConstraints(false)
    Iterable<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
    Iterable<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.

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

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

List<Customer> findByEmailContainingIgnoreCase(String emailPart);

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);

Query methods can accept Pageable for pagination and sorting:

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

Another special parameter that can be passed to query methods is a fetch plan:

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:

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