Jakarta Data – An unified API for Persistence

Advertisements
On this article

JPA (Java/Jakarta Persistence API) become a long time ago the standard way persiste data in Java applications. It provides a simple approach to map the Java classes with Database tables, the relationships and make operations in the database like queries, save, delete with minimum effort. The days writing SQL sentences in the Java code, registering JDBC drivers, opening and closing connections were left in the past.

A lot of frameworks took advantage of JPA, making even simpler interacting with the database, reducing a lot of boiler plate code by following a “repository pattern”. For example, Spring Data JPA is a reference in the Java World, reducing drastically the boiler plate code, the micro-service world hug its approach because in a couple of minute a lot of code is available to start doing queries and operations on the database. On the other side, the Jakarta EE frameworks was laking on an specification like Spring Data. The could be workaround by using Data Module in Apache Delta Spike, very useful library with support to CDI, EJB and other Jakarta Specifications.

That is enough when working with SQL database (relational), but it’s very common nowadays to use also No-SQL database in the applications. That’s why Jakarta NoSQL was created as the specification to interact with NoSQL Database. As a developer we could use it to “map” documents, columns, graph and more in NoSQL database following a non-sql approach like the use in JPA.

So, Jakarta Data can be defined as a unified specification providing the simplicity of Spring Data or Data Module of Delta Spike to remove boiler plate code, which is able to support the JPA and Jakarta NoSQL specifications, and providing integration with other specifications like CDI, BeanValidation, and more. Applications using frameworks based on the Jakarta EE specifications like Jakarta EE, Quarkus, Helidon, Dropwizard might take advantage of this the Jakarta Data specification, which is though to be part of a future Jakarta EE 11 version. So, let’s see some examples of what is delivered on Jakarta Data.


Repository Pattern

Other frameworks like Spring JPA and Delta Spike Data Module, follow the repository pattern to reduce the boiler plate code to implement a data access layer. By using generics and extending some Interfaces our code is able to implement out of the box a group of basic operations. In addition to that it’s possible to add more “abstract” methods to the interface, and framework will create the code for us…

@Repository
public interface ProductRepository extends CrudRepository<Product, Long> {

}

For the developer, it only needs to annotate the interface with @Repository and extend from one of the available DataRepository subclasses, CrudRepository or PageableRepository. That interface uses Java Generic to know the type of the entity to access, in the previous example the entity is of type Product and the id of the class if of type Long.

There are more repositories, for example MongoDBRepository , ArangoDBRepository , CouchbaseDBRepository which extends from PageableRepository and return specific features for those NoSQL databases. But remember, as more you use specific implementations it’s harder to switch later to other general implementations.

Entities

As mentioned before, Jakarta Data has support for JPA and NoSQL specification, it means that it is possible to use jakarta.persistence.Entity , and other annotations from the same package (jakarta.persistence.Id or jakarta.persistence.Column) to map the entities for relation databases. And also it’s possible to use jakarta.nosql.Entity and Jakarta NoSQL specification annotations (jakarta.nosql.Id or jakarta.nosql.Column) to mapp the entities for NoSQL databases. From the Repository level there is no difference, both kind of entities will be mapped.

The entities also have support for record class type, and that will help us to reduce even more the amount of code.

@Entity
public record Pokemon(@Id UUID id, @Column String name, @Column String location){}

Query Methods

Following the same approach as Spring Data or Delta Spike Data Module, it’s possible to add new operations to the repositories with minimum effort. There are 2 ways to do it.

Query Annotation

In the interface an abstract method is create which is annotated with @Query and the query to execute. In case of parameters in the query, the method might receive parameters which are bound to the query by position or by name.

@Repository
public interface ProductRepository extends CrudRepository<Product, Long> {
  @Query("SELECT p FROM Products p WHERE p.name=?1")  
  Optional<Product> findByName(String name);
  
  @Query("SELECT p FROM Products p WHERE p.ranking=:ranking")  
  List<Product> findByRanking(@Param("ranking") String ranking);
}

Query By Method

The Query by method mechanism allows for creating query commands by naming convention.

@Repository
public interface ProductRepository extends CrudRepository<Product, Long> {

  List<Product> findByName(String name);

  Stream<Product> findByNameLike(String namePattern);

  @OrderBy(value = "price", descending = true)
  List<Product> findByNameLikeAndPriceLessThan(String namePattern, float priceBelow);
}

It’s very important to understand the naming convention, therefore the code is generated automagically and returns the expected result.

KeywordDescription
findByGeneral query method returning the repository type.
deleteByDelete query method returning either no result (void) or the delete count.
countByCount projection returning a numeric result.
existsByExists projection, returning typically a boolean result.
Advertisements

And with the following keywords it’s possible to make operations, add clause or filter values.

KeywordDescriptionMethod signature Sample
AndThe and operator.findByNameAndYear
OrThe or operator.findByNameOrYear
BetweenFind results where the property is between the given valuesfindByDateBetween
EmptyFind results where the property is an empty collection or has a null value.deleteByPendingTasksEmpty
LessThanFind results where the property is less than the given valuefindByAgeLessThan
GreaterThanFind results where the property is greater than the given valuefindByAgeGreaterThan
LessThanEqualFind results where the property is less than or equal to the given valuefindByAgeLessThanEqual
GreaterThanEqualFind results where the property is greater than or equal to the given valuefindByAgeGreaterThanEqual
LikeFinds string values “like” the given expressionfindByTitleLike
IgnoreCaseRequests that string values be compared independent of case for query conditions and ordering.findByStreetNameIgnoreCaseLike
InFind results where the property is one of the values that are contained within the given listfindByIdIn
NullFinds results where the property has a null value.findByYearRetiredNull
TrueFinds results where the property has a boolean value of true.findBySalariedTrue
FalseFinds results where the property has a boolean value of false.findByCompletedFalse
OrderBySpecify a static sorting order followed by the property path and direction of ascending.findByNameOrderByAge
OrderBy____DescSpecify a static sorting order followed by the property path and direction of descending.findByNameOrderByAgeDesc
OrderBy____AscSpecify a static sorting order followed by the property path and direction of ascending.findByNameOrderByAgeAsc
OrderBy____(Asc|Desc)*(Asc|Desc)Specify several static sorting ordersfindByNameOrderByAgeAscNameDescYearAsc


Integration with other Jakarta Specifications

Jakarta Data is integrated with other Jakarta specifications to simplify the development of features.

JPA

As mentioned before, with Jakarta Data, it’s possible to create repositories for entities from JPA, jakarta.persistence.Entity and has support for other features in the jakarta.persistence.* package.

NoSQL

It has support to Jakarta NoSQL entities (jakarta.nosql.Entity) and has support for other features in the jakarta.nosql.* package.

CDI

Repository are automatically considered as a managed bean handled by CDI (Context and Dependency Injection). The repository just needs to be declared and annotated with @Inject and CDI will be in charge of creating the instance for us.

@Application
public class MyService {

  @Inject CarRepository repository;

  public void myMethod(){
	Car ferrari = Car.id(10L).name("Ferrari").type(CarType.SPORT);
	repository.save(ferrari);
  }
}

Using CDI enabling us to use other CDI features like Method Interceptors, therefore it might be possible implement a basic AOP (Aspect Oriented Programming) style in the data layer.

Transactions

By default, when CDI and Jakarta Transaction API is being used, if a transaction is open on a thread, every call to the repositories will be enlisted as part of the transaction. When the transaction is committed, then all the calls to the repositories will be committed. However, that behavior can be modified, repository method can be annotated with the jakarta.transaction.Transactional annotation, which is applied to the execution of the repository method, in that case the method can have run on its own transaction.

@Application
public class MyService {

  @Inject MyRepository1 repository1;
  @Inject MyRepository2 repository2;
  @Inject MyRepository3 repository3;

  @Transactional
  public void myMethod(){
	repository1.someMethod();
    repository2.updateByIdSetModifiedOnAddPrice();
    repository3.someOtherMethod();
  }
}

@Repository
public interface MyRepository2 extends CrudRepository<MyEntity, Long>{
  
  @Transactional
  updateByIdSetModifiedOnAddPrice(productId, now, 10.0)
}

In the previous example the method MyRepository2.updateByIdSetModifiedOnAddPrice will run in a separate transaction from the one created in MyService.myMethod.

BeanValidation

Integrating with Jakarta Validation ensures data consistency within the Java layer. By applying validation rules to the data, developers can enforce constraints and business rules, preventing invalid or inconsistent information from being processed or persisted.  The validation process must occur for save and saveAll methods of Repository interfaces prior to making inserts or updates in the database. 

@Schema(name = "Student")
@Entity
public class Student {

    @Id
    private String id;

    @Column
    @NotBlank
    private String name;

    @Positive
    @Min(18)
    @Column
    private int age;
}


How to use it?

To start to use Jakarta Data, add the API as a Maven dependency:

<dependency>
    <groupId>jakarta.data</groupId>
    <artifactId>jakarta-data-api</artifactId>
    <version>1.0.0-b3</version>
</dependency>

or the equivalent gradle dependency

implementation 'jakarta.data:jakarta-data-api:1.0.0-b3'

Conclusion

Definitely Jakarta Data is a great addition to the Jakarta EE specification, which leverage an official standard and implementation on the Java World for the repository pattern with JPA and NoSQL. It will help many applications to simplify the code and reduce the time to have a data layer ready, making possible to focus more on the business rules and the domain of the application or services.

References

Advertisements

Leave a Reply

Your email address will not be published. Required fields are marked *