Simplify Data Access Code With Micronaut Data

It’s been almost a year since our first blog post about Micronaut. We saw a big potential of this new tool but found it is lacking two important features: RabbitMQ support and data access framework similar to Spring Data.
RabbitMQ support is already available and we blogged about it here. In this post we will look at recent addition to Micronaut framework – Micronaut Data (previously called Predator – Precomputed Data Repositories).

Intro

Micronaut Data is a database access toolkit providing features and based on similar concepts as Spring Data or GORM Data Services. It offers many of the benefits of the aforementioned libraries but with additional benefits of Ahead of Time (AoT) compilation. Micronaut Data uses less memory as it does not need a runtime model to generate queries, it generates it at compile time. This is very important when using JPA provider like Hibernate which already maintains meta-model in memory. Micronaut Data should also be faster as it does not runtime query translation and does not use reflection and proxies. Not using proxies should also result in smaller stack traces. These are the main reasons why we wanted to try this new stuff.

Adding to project

We will add Micronaut Data to Payment Service from our article Building Microservices with Micronaut.
Before we start adding Micronaut Data to our project, let’s quickly review required steps to have JPA based data access.

We need the following dependencies added to our pom.xml:

<dependency>
	<groupId>io.micronaut.configuration</groupId>
	<artifactId>micronaut-jdbc-hikari</artifactId>
	<scope>compile</scope>
</dependency>

<dependency>
	<groupId>io.micronaut.configuration</groupId>
	<artifactId>micronaut-hibernate-jpa</artifactId>
	<scope>compile</scope>
</dependency>

<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
	<version>1.4.197</version>
	<scope>runtime</scope>
</dependency>

This way we added Hibernate support, Hikari connection pool and H2 database to our project.
Next step is specifying data source and configuring JPA in our configuration file application.yml

---
datasources:
  default:
    url: jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    username: sa
    password: ""
    driverClassName: org.h2.Driver
---
jpa:
 default:
    packages-to-scan:
        - 'pl.altkom.asc.lab.micronaut.poc.pricing.domain'
    properties:
        hibernate:
            hbm2ddl:
                auto: update
            show_sql: true

Here we configure connection to H2 database and setup Hibernate, specifying where to look for persistent classes. That’s all. With this setup we can connect to in-memory database using Hibernate.

Now we need to add Micronaut Data specific dependencies to pom.xml.
We need to add a new repository to the repositories section to be able to download pre-release packages.

<repository>
	<id>https://oss.sonatype.org</id>
	<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>

Now we can add Micronaut Data

<dependency>
	<groupId>io.micronaut.data</groupId>
	<artifactId>micronaut-data-hibernate-jpa</artifactId>
	<version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>

Last step is adding annotation processor for Micronaut Data to annotation processor paths section.

<path>
	<groupId>io.micronaut.data</groupId>
	<artifactId>micronaut-data-processor</artifactId>
	<version>1.0.0.BUILD-SNAPSHOT</version>
</path>

The resulting pom.xml can be found here.

Doing basic CRUD

Now we are ready to roll. Providing basic CRUD is extremely easy, we just have to add an interface extending Micronaut Data CrudRepository.

import io.micronaut.data.repository.CrudRepository;
import io.micronaut.data.annotation.*;
import io.micronaut.data.model.*;

@Repository
public interface Tariffs extends CrudRepository<Tariff, Long>  {}

Now you can inject Tariffs interface into your service or controller classes and use it.

You can Create new entity:

tariffs.save(new Tariff(“New tariff”));

Read it by id:

Tariff tariff = tariffs.findById(1L).get();

Update it:

tariffs.save(tariff);

And Delete it:

tariffs.deleteById(1L);

There are other useful methods like existsById, saveAll and deleteAll available in CrudRepository.

If you don’t want to expose all CRUD operation, for example you only allow entities to be created/updated instead of extending CrudRepository you can extend GenericRepository.

@Repository
public interface PolicyAccountRepository extends GenericRepository<PolicyAccount, Long> {

    @EntityGraph(attributePaths = {"entries"})  
    Optional findByPolicyNumber(String policyNumber);
    
    @EntityGraph(attributePaths = {"entries"}) 
    @Query("FROM PolicyAccount p WHERE p.policyAccountNumber = :policyAccountNumber")
    Optional findByPolicyAccountNumber(String policyAccountNumber);

    PolicyAccount save(PolicyAccount policyAccount);

    Collection findAll();
}

Here we define some custom finders and expose only save method.

There are other base interfaces you can chose as a base for your repository: AsyncCrudRepository, ReactiveStreamsCrudRepository, RxJavaCrudRepository and PageableRepository.

Also note that your repositories do not have to be interfaces, you can use abstract classes instead if you want to provide some additional custom methods.

Micronaut Data automatically manages transactions for us. By default it delegates to Spring transaction manager but you can control the process by using the following annotations: (if you don’t want direct dependency on spring) javax.transaction.Transactional, io.micronaut.spring.tx.annotation.Transactional.
Micronaut also automatically wraps query methods with read-only transactions.

Querying

As you have already seen, you can add your own custom finders. You should start method name with keyword find (you can also use search, query, get, read or retrieve) and then use By[PropertyName] to specify filtering criteria.

For example, given the following PolicyAccount entity.

@Entity
@Table(name = "policy_account")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class PolicyAccount {

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "policy_number")
    private String policyNumber;

    @Column(name = "policy_account_number")
    private String policyAccountNumber;

    @OneToMany(mappedBy = "policyAccount", cascade = CascadeType.ALL)
    private List entries;

    @Column(name = "created")
    private Date created; 
    ...
}

And query :

Optional<PolicyAccount> findByPolicyNumber(String policyNumber);

The query will try to find PolicyAccount with policy number equal to given parameter value.

Other supported return types are: entity type, List<T>, Stream<T>, Optional<T>, Page<T>, Slice<T>, Future<T>, CompletableFuture<T>, Publisher<T> (Single, Mono, Flux, Maybe) and primitive/simple types for projections.

Apart from finding entities, you can also perform countBy*, existsBy*, deleteBy* and updateBy*. Alternatively to By* you can use parameter names to match properties to query:

Optional<PolicyAccount> find(String policyNumber);

In our example we had very simple query that checks for policy number equality, but Micronaut Data allows for much more. For example you can query for dates After/Before/Between, use Contains or StartsWith/EndsWith or Like for string comparison, use In/InList, IsNull/IsEmpty operators. Any expression can be negated. Finally expressions can be combined using logical And or Or operator.

Optional<PolicyAccount> findByPolicyNumberLikeOrCreatedAfter(String policyNumber, Date created);

Will look for accounts with policy number like given parameter or created after the given date.

Micronaut Data repositories also support pagination and ordering of data:

Page<PolicyAccount> findByPolicyNumberLikeOrderByCreated(String policyNumber, Pageable pageable);

Will look for given page with policy accounts matching given policy number, results are sorted by created date property.

You can also specify joins, which is very important in order to have efficient queries and properly manage Hibernate session scope. If you want to eagerly fetch some part of your entity object graph you have the following options.
You can use @EntityGraph annotation from JPA 2.1:

@EntityGraph(attributePaths = {"entries"})  
Optional findByPolicyNumber(String policyNumber)

Here we load policy account together with all accounting entries. Alternatively you can use @Join annotation.

Projections

There are situations when you do not need to load the whole entity, you just need to grab id or code or some other value or even instead of entity you want a DTO with set of properties related to the task at hand. Micronaut Data support these scenarios with projections.

List<String> findPolicyNumberByCreatedAfter(Date startDate);

Will look for Policy Accounts with Created property value greater than given start date and will return list of policy numbers for these accounts. Micronaut Data also supports projection expressions like Count, CountDistinct, Distinct, Max, Min, Sum, Avg. This allows us to write queries like this for Offer class.

BigDecimal findSumTotalPriceByCreationDateAfter(LocalDate date);

This query will count sum of total price of offers created after given date.

Micronaut Data is also capable of projecting query results into DTO class. For example we can create PolicyAccountDto class and use it as a return type from repository.

@Introspected
...
public class PolicyAccountDto {
    private String policyAccountNumber;
    private String policyNumber;
    private Date created;
    private Date updated;
}

And we can create method in repository like this:

Collection<PolicyAccountDto> findAll();

DTO type must be annotated with @Introspected, so its metadata are available for Micronaut and must have properties with names that match property names you want to project on. If properties do not much you will get compilation errors.

Other features

Micronaut Data also supports explicit queries – you can annotate a method in your repository with @Query like this:

@Query("FROM PolicyAccount p WHERE p.policyAccountNumber = :policyAccountNumber")
Optional<PolicyAccount> findByPolicyAccountNumber(String policyAccountNumber);

There is also support for native queries, asynchronous and reactive queries.

MIcronaut Data also support simple automatic auditing by setting creation date and last update date on properties marked with @DateCreated and @DateUpdated.

Summary

As you can see, Micronaut Data provides a rich set of features allowing you to easily implement your data access layer using plain Java, without a need to use JPQL or SQL.
For basic CRUD and find operations all you have to do is to define an interface and follow the naming conventions.
If you need to write complex SQL or JPQL queries Micronaut Data still supports it still and gives you an advantage of automatic parameter binding. We are waiting for final release to evaluate performance and memory usage but event at this stage Micronaut Data seems to be another good reason to start micronauting today.

If you want to learn more about Micronaut Data check out official Micronaut Data documentation.