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.
Keyword | Description |
---|---|
findBy | General query method returning the repository type. |
deleteBy | Delete query method returning either no result (void) or the delete count. |
countBy | Count projection returning a numeric result. |
existsBy | Exists projection, returning typically a boolean result. |
And with the following keywords it’s possible to make operations, add clause or filter values.
Keyword | Description | Method signature Sample |
---|---|---|
And | The and operator. | findByNameAndYear |
Or | The or operator. | findByNameOrYear |
Between | Find results where the property is between the given values | findByDateBetween |
Empty | Find results where the property is an empty collection or has a null value. | deleteByPendingTasksEmpty |
LessThan | Find results where the property is less than the given value | findByAgeLessThan |
GreaterThan | Find results where the property is greater than the given value | findByAgeGreaterThan |
LessThanEqual | Find results where the property is less than or equal to the given value | findByAgeLessThanEqual |
GreaterThanEqual | Find results where the property is greater than or equal to the given value | findByAgeGreaterThanEqual |
Like | Finds string values “like” the given expression | findByTitleLike |
IgnoreCase | Requests that string values be compared independent of case for query conditions and ordering. | findByStreetNameIgnoreCaseLike |
In | Find results where the property is one of the values that are contained within the given list | findByIdIn |
Null | Finds results where the property has a null value. | findByYearRetiredNull |
True | Finds results where the property has a boolean value of true. | findBySalariedTrue |
False | Finds results where the property has a boolean value of false. | findByCompletedFalse |
OrderBy | Specify a static sorting order followed by the property path and direction of ascending. | findByNameOrderByAge |
OrderBy____Desc | Specify a static sorting order followed by the property path and direction of descending. | findByNameOrderByAgeDesc |
OrderBy____Asc | Specify a static sorting order followed by the property path and direction of ascending. | findByNameOrderByAgeAsc |
OrderBy____(Asc|Desc)*(Asc|Desc) | Specify several static sorting orders | findByNameOrderByAgeAscNameDescYearAsc |
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
- https://jakarta.ee/specifications/data/1.0/
- https://jakarta.ee/specifications/data/1.0/data-1.0.0-b3
- https://blog.payara.fish/what-is-jakarta-data
- https://deltaspike.apache.org/documentation/data.html
- https://spring.io/projects/spring-data