All Articles ↓
10 months ago
Spring Query Interfaces in CUBA

Rationale

Developers usually don’t like to change their coding habits. When I started working with CUBA, I didn’t need to learn a lot of new things, creating applications was a pretty smooth process. One of the things that I ought to rediscover was working with data.

In Spring there are several libraries that you can use for working with data, and one of the most popular is spring-data-jpa which allows developers to avoid writing SQL or JPQL in most cases. You just need to specify an interface with methods that have special names and Spring will generate and execute a query for you.

For example, this is an interface with a method to count all customers with a given last name:

interface CustomerRepository extends CrudRepository<Customer, Long> {
  long countByLastName(String lastName);
}

You can inject this interface to your services and call this method where needed.

CUBA provides a lot of features out-of-the-box for data manipulation like partially loaded entities and sophisticated data security subsystem that restricts access to attributes or table rows. And all those features come with an API which is a bit different from well-known Spring Data or JPA/Hibernate.

So why don’t we have query interfaces in CUBA and is it possible to add them?

Working with Data in CUBA

There are three main classes in CUBA’s API for working with data: DataStore, EntityManager, and DataManager.

DataStore abstraction provides an API to deal with a persistent storage like RDBMS, file system or cloud storage. It allows to perform basic operations with data, however, it is not recommended to work with DataStore directly unless you’re developing a custom persistent storage or in a need for very special access to the underlying storage.

EntityManager is mostly a copy of a well-known JPA EntityManager, but it has additional methods to work with CUBA views, soft deletion and methods to handle CUBA queries. As a CUBA developer you may rarely need this class in your day-to-day work, but in cases when you need to overcome CUBA’s security restrictions.

The next facility, DataManager, is the main class to work with data in CUBA. It provides an API for data manipulation and supports CUBA security model including attribute- and row-level security. When you select data, DataManager modifies it implicitly. For example, in case of a relational data store, it updates “select” clause to exclude restricted attributes and appends “where” condition(s) that filter out database rows that should not be visible to a current user. This security-awareness is just amazing, while development you don't need to remember what security filters should be applied in each query.

Here is a diagram of CUBA classes interaction for fetching data from an RDBMS when DataManager is used.

text

With DataManager you can select entities (as well as entity hierarchies using CUBA views) relatively easy. The simplest query may look like this:

dataManager.load(Customer.class).list()

DataManager will take care about filtering out “soft-deleted” records, restricted attributes and entities as well as creating a transaction.

But when it comes to queries with complex “where” conditions you need to write a JPQL statement.

For example, if you need to count all customers by their last name, you need to write something like this:

public Long countByLastName(String lastName){
   return dataManager
           .loadValue("select count(c) from sample$Customer c where c.lastName = :lastName", Long.class)
           .parameter("lastName", lastName)
           .one();
}

public Long countByLastName(String lastName){
   LoadContext<Person> loadContext = LoadContext.create(Person.class);
   loadContext
      .setQueryString("select c from sample$Customer c where c.lastName = :lastName")
      .setParameter("lastName", lastName);
   return dataManager.getCount(loadContext);
}

As you can see, you need to pass JPQL statement to DataManager for execution. In CUBA API you should define JPQL as a string (Criteria API is not supported yet). It is a readable and clear definition of a query, but it might be a bit challenging to debug if something goes wrong. Also, JPQL strings cannot be verified by a compiler during application build or by Spring Framework during context initialization.

Compare it to Spring JPA code snippet:

interface CustomerRepository extends CrudRepository<Customer, Long> {
  long countByLastName(String lastName);
}

It is three times shorter and does not include any strings. In addition to this, method countByLastName is verified at deploy stage. If you make a typo in method’s name like countByLastName there will be an error:

Caused by: org.springframework.data.mapping.PropertyReferenceException: No property LastNome found for type Customer!

Since CUBA is built over Spring framework you can add spring-data-jpa to CUBA project as a library and use this feature. The only issue - Spring’s query interfaces use JPA EntityManager under the hood, so queries will be processed neither by CUBA’s EntityManager nor by DataManager. Therefore, to add query interfaces to CUBA in a proper way they need to be customized - we need to replace all calls to EntityManager with DataManager’s methods invocations and add CUBA views support.

Some might argue that Spring approach is less manageable than CUBA’s because you do not have a control over query generation process. This is always a problem of a balance between convenience and a level of abstraction and it is up to a developer to choose which way to go. But it looks like having an additional (not the only one), simpler way to deal with data won’t hurt.

And if you need more control, in Spring there is a way to specify your own query for an interface method, so this option should and will be added to CUBA, too.

Implementation

Query interfaces are implemented as a CUBA application module using spring-data-commons. This library contains classes for implementing custom query interfaces, for example, Spring’s spring-data-mongodb library is based on it. Spring-data-commons utilizes proxying technique to create proper implementations of the declared query interfaces.

During CUBA’s context initialization all references to query interfaces are implicitly replaced by references to generated proxy beans published in the application context. When a developer invokes an interface method it is intercepted by a corresponding proxy. Then the proxy generates a JPQL query based on method’s name, substitutes parameters values and passes it to DataManager for execution. The diagram below shows simplified interaction between key components of the module.

text

Using Query Interfaces in CUBA

To use CUBA query interfaces you need to add an application module in the project’s build file:

appComponent("com.haulmont.addons.cuba.jpa.repositories:cuba-jpa-repositories-global:0.1-SNAPSHOT")

XML configuration to enable query interfaces:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:context="http://www.springframework.org/schema/context"
            xmlns:repositories="http://www.cuba-platform.org/schema/data/jpa"
            xsi:schemaLocation="
   http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans.xsd
   http://www.springframework.org/schema/context
   http://www.springframework.org/schema/context/spring-context-4.3.xsd
   http://www.cuba-platform.org/schema/data/jpa      
   http://www.cuba-platform.org/schema/data/jpa/cuba-repositories.xsd">

   <!-- Annotation-based beans -->
   <context:component-scan base-package="com.company.sample"/>

   <repositories:repositories base-package="com.company.sample.core.repositories"/>

</beans:beans>

If you prefer using annotations instead of creating XML config, you can enable query interfaces in the following way:

@Configuration
@EnableCubaRepositories
public class AppConfig {
   //Configuration here
}

After enabling query interfaces you can create them in your application. An example of such an interface:

public interface CustomerRepository extends CubaJpaRepository<Customer, UUID> {

   long countByLastName(String lastName);
   List<Customer> findByNameIsIn(List<String> names);

   @CubaView("_minimal")
   @JpqlQuery("select c from sample$Customer c where c.name like concat(:name, '%')")
   List<Customer> findByNameStartingWith(String name);
}

You can use @CubaView and @JpqlQuery annotations for query interface methods. The first one defines a view for the entities that will be fetched (“_local” is the default view if not specified). The second annotation specifies the exact JPQL query that will be used for this method if the query cannot be expressed with a method name.

Query interfaces application component is attached to “global” CUBA module, so you can define and use query interfaces in both “core” and “web” modules, just don’t forget to enable them in the corresponding configuration files. The interface usage example is below:

@Service(CustomerService.NAME)
public class CustomerServiceBean implements PersonService {

   @Inject
   private CustomerRepository customerRepository;

   @Override
   public List<Date> getCustomersBirthDatesByLastName(String name) {
      return customerRepository.findByNameStartingWith(name)
            .stream().map(Customer::getBirthDate).collect(Collectors.toList());
   }
}

Conclusion

CUBA is flexible. If you feel that you need an additional feature for all your application and you don’t want to wait for the new version of CUBA, it is pretty easy to implement and add it not touching CUBA core. By adding query Interfaces to CUBA we hope to help developers work more efficiently delivering reliable code faster. The first version of the library is available at GitHub and supports CUBA version 6.10 and higher.

Andrey Belyaev