Jakarta Data – Un API unificado para persistencia

Advertisements

JPA (Java/Jakarta Persistence API) se convirtió hace mucho tiempo en la forma estándar de persistir datos en aplicaciones Java. Proporciona un enfoque simple para mapear las clases de Java con tablas de bases de datos, sus relaciones y realizar operaciones en la base de datos como consultas, guardar y eliminar con el mínimo esfuerzo. Los días en los que se escribían sentencias SQL en código Java, registraban controladores JDBC y abrían y cerraban conexiones quedaron en el pasado.

Muchos frameworks aprovecharon JPA, simplificando aún más la interacción con la base de datos, reduciendo una gran cantidad de código repetitivo al seguir un “patrón de repositorio”. Por ejemplo, Spring Data JPA es una referencia en el mundo Java, reduciendo drásticamente el código, el mundo de los micro-servicios abraza su enfoque porque en un par de minutos hay una gran cantidad de código disponible para comenzar a realizar consultas y operaciones en la base de datos. Por otro lado, los frameworks de Jakarta EE carecían de una especificación como Spring Data. La solución podría ser utilizando el módulo de datos en Apache Delta Spike, una biblioteca muy útil compatible con CDI, EJB y otras especificaciones de Jakarta.

Esto es suficiente cuando se trabaja con una base de datos SQL (relacional), pero hoy en día es muy común utilizar también una base de datos No-SQL en las aplicaciones. Es por eso que se creó Jakarta NoSQL como la especificación para interactuar con la base de datos NoSQL. Como desarrollador, podríamos usarlo para “mapear” documentos, columnas, gráficos y más en una base de datos NoSQL siguiendo un enfoque que no sea SQL como el uso en JPA.

Por lo tanto, Jakarta Data se puede definir como una especificación unificada que proporciona la simplicidad de Spring Data o el módulo de datos de Delta Spike para reducir el código, que es capaz de admitir las especificaciones JPA y Jakarta NoSQL, y proporciona integración con otras especificaciones como CDI, BeanValidation y más. Las aplicaciones que utilizan frameworks basados en las especificaciones de Jakarta EE como Jakarta EE, Quarkus, Helidon, Dropwizard podrían aprovechar esta especificación de datos de Jakarta, que se cree que será parte de una futura versión de Jakarta EE 11. Entonces, veamos algunos ejemplos de lo que se ofrece en Jakarta Data.


Patrón Repositorio

Otros frameworks, como Spring JPA y Delta Spike Data Module, siguen el patrón de repositorio para reducir el código estándar para implementar una capa de acceso a datos. Al utilizar genéricos y ampliar algunas interfaces, nuestro código puede implementar de forma inmediata un grupo de operaciones básicas. Además de eso, es posible agregar más métodos “abstractos” a la interfaz, y el framework creará el código por nosotros…

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

}

Para el desarrollador, solo necesita anotar la interfaz con @Repository y extenderla desde una de las subclases de DataRepository disponibles, CrudRepository o PageableRepository. Esa interfaz utiliza Java “generics” para saber el tipo de entidad a acceder, en el ejemplo anterior la entidad es de tipo Product y el id de la clase si es de tipo Long.

Entidades

Como se mencionó anteriormente, Jakarta Data es compatible con las especificaciones JPA y NoSQL, lo que significa que es posible usar jakarta.persistence.Entity y otras anotaciones del mismo paquete (jakarta.persistence.Id o jakarta.persistence.Column) para mapear las entidades para bases de datos de relaciones. Y también es posible utilizar las anotaciones de especificación jakarta.nosql.Entity y Jakarta NoSQL (jakarta.nosql.Id o jakarta.nosql.Column) para asignar las entidades a las bases de datos NoSQL.Desde el nivel del Repositorio no hay diferencia, se mapearán ambos tipos de entidades.

Las entidades también tienen soporte al tipo de class record , esto ayuda aún mas a reducir la cantidad código necesario en la aplicación.

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

Métodos de Query

Siguiendo el mismo enfoque que Spring Data o Delta Spike Data Module, es posible agregar nuevas operaciones a los repositorios con el mínimo esfuerzo. Hay 2 maneras de hacerlo.

Anotación Query

En la interfaz se crea un método abstracto el cual se anota con @Query y la consulta a ejecutar. En el caso de parámetros en la consulta, el método puede recibir parámetros vinculados a la consulta por posición o por nombre.

@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 por el nombre del método

El mecanismo de consulta por “nombre del método” permite crear comandos de consulta mediante una convención de nomenclatura.

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

Es muy importante comprender la convención de nomenclatura, por lo tanto el código se genera automáticamente y devuelve el resultado esperado.

KeywordDescripción
findByMétodo de consulta general que devuelve el tipo de entidad del repositorio.
deleteByEl método de consulta de eliminación devuelve ningún resultado (nulo) o el recuento de eliminaciones.
countByDevuelve el número de registros para dicha consulta
existsByExiste una proyección que normalmente devuelve un resultado booleano.

Y con las siguientes palabras clave es posible realizar operaciones, agregar cláusulas o filtrar valores.

KeywordDescripciónEjemplo de nombre del método
AndEl operador and.findByNameAndYear
OrEl operador or.findByNameOrYear
BetweenEncuentra resultado donde la propiedad está entro los dos valores indicados.findByDateBetween
EmptyFind results where the property is an empty collection or has a null value.
Encuentra resultados cuando el atributo es una coleción vacía o el valor es null
deleteByPendingTasksEmpty
LessThanEncuentre resultados donde la propiedad sea menor que el valor dadofindByAgeLessThan
GreaterThanEncuentre resultados donde la propiedad sea mayor que el valor dadofindByAgeGreaterThan
LessThanEqualEncuentre resultados donde la propiedad sea menor o igual al valor dadofindByAgeLessThanEqual
GreaterThanEqualEncuentre resultados donde la propiedad sea mayor o igual al valor dadofindByAgeGreaterThanEqual
LikeEncuentra valores del string “like” en la expresión dadafindByTitleLike
IgnoreCaseSolicita que los valores de cadena se comparen independientemente del caso para las condiciones de consulta y el orden.findByStreetNameIgnoreCaseLike
InEncuentre resultados donde la propiedad sea uno de los valores contenidos en la lista dadafindByIdIn
NullEncuentra resultados donde la propiedad tiene un valor nulo.findByYearRetiredNull
TrueEncuentra resultados donde la propiedad tiene un valor booleano verdadero.findBySalariedTrue
FalseEncuentra resultados donde la propiedad tiene un valor booleano falso.findByCompletedFalse
OrderByEspecifique un orden de clasificación estático seguido de la ruta de propiedad y la dirección ascendente.findByNameOrderByAge
OrderBy____DescEspecifique un orden de clasificación estático seguido de la ruta de propiedad y la dirección descendente.findByNameOrderByAgeDesc
OrderBy____AscEspecifique un orden de clasificación estático seguido de la ruta de propiedad y la dirección ascendente.findByNameOrderByAgeAsc
OrderBy____(Asc|Desc)*(Asc|Desc)Especificar varios órdenes de clasificación estáticosfindByNameOrderByAgeAscNameDescYearAsc


Integración con otras especificacioens de Jakarta

Jakarta Data está integrado con otras especificaciones de Jakarta para simplificar el desarrollo de funciones.

JPA

Como se mencionó anteriormente, con Jakarta Data, es posible crear repositorios para entidades de JPA, jakarta.persistence.Entity y es compatible con otras funciones en el paquete jakarta.persistence.*.

NoSQL

Tiene soporte para entidades NoSQL de Jakarta (jakarta.nosql.Entity) y tiene soporte para otras características en el paquete jakarta.nosql.*.

CDI

El repositorio se considera automáticamente como un bean administrado manejado por CDI (Inyección de contexto y dependencia). El repositorio solo necesita ser declarado y anotado con @Inject y CDI se encargará de crear la instancia por nosotros.

@Application
public class MyService {

  @Inject CarRepository repository;

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

El uso de CDI nos permite utilizar otras funciones de CDI como interceptores de métodos, por lo que podría ser posible implementar un estilo básico de AOP (programación orientada a aspectos) en la capa de datos.

Transactions

De forma predeterminada, cuando se utilizan CDI y Jakarta Transaction API, si una transacción está abierta en un hilo, cada llamada a los repositorios se registrará como parte de la transacción. Cuando se confirma la transacción, se confirmarán todas las llamadas a los repositorios. Sin embargo, ese comportamiento se puede modificar, el método del repositorio se puede anotar con la anotación jakarta.transaction.Transactional, que se aplica a la ejecución del método del repositorio; en ese caso, el método se puede ejecutar en su propia transacción.

@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)
}

En el ejemplo anterior, el método MyRepository2.updateByIdSetModifiedOnAddPrice se ejecutará en una transacción separada de la creada en MyService.myMethod.

BeanValidation

La integración con Jakarta Validation garantiza la coherencia de los datos dentro de la capa de Java. Al aplicar reglas de validación a los datos, los desarrolladores pueden imponer restricciones y reglas de negocio, evitando que se procese o persista información no válida o inconsistente. El proceso de validación debe realizarse para los métodos save y saveAll de las interfaces del repositorio antes de realizar inserciones o actualizaciones en la base de datos.

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

    @Id
    private String id;

    @Column
    @NotBlank
    private String name;

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


Cómo usarlo?

Para comenzar a utilizar Jakarta Data, agregue la API como una dependencia de Maven:

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

o la dependencia gradle equivalente

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

Conclusión

Definitivamente, Jakarta Data es una gran adición a la especificación Jakarta EE, que aprovecha un estándar oficial y una implementación en Java para el patrón de repositorio con JPA y NoSQL. Ayudará a muchas aplicaciones a simplificar el código y reducir el tiempo para tener una capa de datos lista, permitiendo centrarse más en las reglas de negocio y el dominio de la aplicación o servicios.

Referencias

Advertisements

Leave a Reply

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