Ir al contenido principal

Datos con Spring - Repositorios con JPA

Como usar Spring con repositorios JPA

Uno de los usos más habituales de los sistemas informáticos es el almacenamiento y recuperación de información en bases de datos. Un gran cantidad de sistemas son simples bases de datos, en los que por un lado se van rellenando registros, y por otro lado se van recorriendo los registros para realizar determinadas acciones o cálculos puntuales. La conversión de los datos de un sistema orientado a objeto en un entorno relacional de una base de datos, es uno de los grandes dolores de cabeza para muchos de los desarrollos de software, y las herramientas que facilitan esa integración son extremadamente útiles.

Spring ofrece diferentes niveles de abstracción para las bases de datos:

  • JdbcTemplates: un conjunto de clases java para lanzar consultas contra base de datos, y facilitar el mapeo de las respuestas.
  • Jpa: un conjunto de clases java y anotaciones para mapear objetos y tablas de base de datos, y automatizar el mapeo de las respuestas.
  • Repositorios: u conjunto de interfaces java para realizar la persistencia y recuperación de los datos.

Los repositorios constituyen el más moderno de los mecanismos de conexión con las bases de datos. Usando repositorios es posible conectarse a diferentes modelos de persistencia de datos (bases de datos relacionales, base de datos nosql, directorios ldap...). En este caso hablaremos un poco de como utilizar las anotaciones JPA para mapear las tablas de una base de datos relacional, y los repositorios para la persistencia JPA.

Debemos tener en cuenta que no es lo mismo usar repositorios sobre JPA, que utilizar JPA. JPA es un estandar Java para la persistencia automática de clases. Por un lado define un conjunto de anotaciones para asociar clases con entidades de base de datos, y con sus relaciones; y por otro lado define un conjunto de clases para establecer la conexión a las bases de datos, y realizar consultas y modificaciones. Con JPA debemos controlar instancias de EntityManager, Sesiones y Transacciones. Los repositorios de Spring abstraen todo eso. Partiendo de un conjutno de clases anotadas para asociar con las bases de datos, se exponen una serie de métodos para guardar y recuperar datos de manera completamente transparente.

Modelo de datos sencillo.

Las anotanciones JPA se ponen sobre una clase Java para asociarla tanto a una tabla, como para asociar sus relaciones. Aunque JPA ofrece mucha potencia para mapear estructuras de base de datos muy complejas, la tendencia es cada vez más la de tener modelos de base de datos lo más finos posibles. Cada vez son más los sistemas que combinan información obtenida de bases de datos, con información obtenida de servicio externos, y manteniendo un modelo de base de datos sencillo se "uniformiza" el acceso a datos. Por ello tendremos en cuenta una serie de simplicaciones:

  • No utilizaremos herencia en las base de datos. Crearemos una clase Java independiente por cada tabla de la base de datos.
  • Utilizaremos una columna como clave de cada tabla de la base de datos. No utilizarmos objetos de clave multiple.
  • Las claves de una tabla seran generadas por dicha base de datos, y no tenemos que proporcionar ningún modo de código para determinar la clave principal de un objeto en base de datos.
  • Las columnas de clave principal de una tabla no serán a su vez claves foráneas, ni parte de claves foráneas. Esto último es ya evidente cuando cumplimos los dos puntos anteriores.
  • No mapearemos las tablas de relaciones como simples relaciones; todas las tablas con las que interacturemos se mapearan en clases Java.

JPA da soporte para todas estas característas que comentamos, sin embargo en muchas ocasiones hacer uso de ellas significa correr dos riesgos:

  • El modelo mapeado es poco flexible, y si surge la necesidad de añadir una columna puede surgir la necesidad de cambiar modos de relaciones y añadir entidades nuevas.
  • El modelo mapeado es muy restrictivo, y los casos de herencia entre entidades pueden encontrarse con facilidad con una jerarquía en la que es necesario mover atributos hacia arriba, y aparecen columnas duplicadas.
  • Al usar clave entre multiples columnas, las referencias a esas columnas también tienen que ser por multiples columnas. De la misma forma las tablas de relaciones tiene claves cuya multiplicidad puede ser la suma de columnas de los padres. Para un modelo con muchas relaciones, pueden acabar apareciendo rápidamente tablas con ocho campos que forma la clave, y dos campos de datos; al ser tablas de unión entre tres tablas de "segundo nivel".

Entidades JPA

Las principales anotaciones JPA que se utilizan para describir modelos sencillos. Siempre intentaremos crear propiedades privadas para las clases que será donde pondremos las anotaciones de propiedades, y crearemos métodos get y set para dichas propiedades. Teóricamente se podrían anotar los método get de las clases, pero el resultado es menos legible, y el funcionamiento posterior presenta pequeñas diferencias que pueden complicar el mantenimiento del sistema.

@Entity: Es la anotación a nivel de clase que marca a dicha clase java como "mapeada contra una tabla de bbdd".

@Table: Es una anotación opcional a nivel de clase que proporcional información adicional sobre la tabla con la que se mapea la clase. Dentro de una anotación de tabla se indican varios atributos:

  • name: para indicar el nombre real de la tabla. Si no se indica este atributo, se usara el nombre de la clase como nombre de la tabla.
  • indexes: para indicar la lista de indices multi-columnas de la entidad.

@Id: Es una anotación que indica que dicho campo es el identificador de la entidad. Sólo se puede anotar con esta anotación a un único campo del objeto, y ese campo debe ser Serializable. Para el poco recomendable caso de usar claves compuestas, es necesario crear una clase separada que se anotará como @CompositeKey, y añadir la anotación @Id a una referencia a dicha clase.

@GeneratedValue: Es una anotación que indica la forma es la que se espera que la base de datos genere los valores para la clave (en caso de querer usar valores autogenerados). Sólo se puede anotar con ella a una propiedad que esté anotada como @Id; se utilizará la propiedade strategy de la anotación para seleccionar el modo de generación

  • GenerationType.AUTO: es el valor por defecto, y se encarga de utilizar un generador único a nivel de bbdd que aporta números únicos para todas las entidades que usen esta estrategia.
  • GenerationType.IDENTITY: en este caso se utiliza un generador de números diferente para cada entidad.
  • GenerationType.SECUENCE: en este caso se indica una secuencia de la base de datos como fuente para generar los números únicos. Es necesario indicar el nombre del generador de secuencia en esta anotación, y configurar la secuencia a nivel de entidad con la anotación "SequenceGenerator". No todos los gestores de bases de datos soportan secuencias; pero para aquellos gestores que lo soportan es el modo más rápido de conseguir identificadores únicos.
  • GenerationType.TABLE: Funciona igual que un generador de secuencia, siendo necesario indicar el nombre del generador de tablas; y configurando la tabla de generación a nivel de entidad con la anotación "TableGenerator". El uso de tablas para la generación se puede utilizar en cualquier gestor de bases de datos, pero tiene un rendimiento extremadamente bajo en la generación de identificadores

@Column: En una anotación que indica información sobre la columna que hay en la base de datos para la propiedad dada. Podemos usar la anotación de columna para proporcionar información sobre dicha columna de la base de datos con propiedades como:

  • name: el nombre de la columna (se usará el nombre de la propiedad java si no se indica).
  • nullable: indicador de verdadero o falso que indica si la columna admite valores nulos (por defecto es verdadero, y admite nulos).
  • length: longitud máxima de la columna en la base de datos.
  • unique: indicador de verdadero o falso que indica si la columna admite valores duplicados (por defecto es falso, y admite duplicados).

@Temporal: Si un atributo es de tipo Date, tenemos que anotar la propiedad con esta marca para indicar el formato de fecha que se guarda en base de datos. Con TemporaType.DATE se guarda una fecha (dia, mes y año); mientras que con TemporalType.TIMESTAMP se guarda un instante (en milisegundo).

@ManyToOne: Indica que el campo en cuestion es una relación con otra entidad. El tipo de datos del campo debe ser una clase también anotada como entidad. Para una relación many to one debemos indicar:

  • optional: indicador de verdadero o falso que indica si la columna admite valores nulos (por defecto es verdadore, y adminte valores nulos.
  • fetch: indicador para determinar el modo en que se debe recuperar la información de la tabla relacionada.
    • Si indicamos FetchType.EAGER, cuando hagamos una consulta a base de datos para recuperar nuestra entidad, automáticamente se hara una consulta a la base de datos para recuperar el valor relacionado (el peligro de la recuperación en cascada es tan grande que no se recomienda usar este modo).
    • Si indicado FetchType.LAZY, cuando hagamos una consulta a base de datos para recuerar nuestra entidad, se creará un Proxy dinámico de Java para la entidad relacionada, y sólo se recuperará el valor relacionado si es necesario (en este caso también hay un peligro, y es que necesitamos estar en la misma transacción para poder recuperar el valor relacionada; ya que si no tendrémos un error).

@JoinColumn: Indica la información de la columna para una relación entre tablas. Los valores asignables en la entidad son los mismos que en la columna.

@OneToMany: Indica que el campo en cuestión tiene multiples relaciones con otra entidad. Dicho campo debe ser una Collección (listas si queremos respetar el orden, o conjuntos si nos resulta indiferente el orden), y el tipo de la la colección debe estar marcado como una entidad. Su uso es poco recomendable; ya que si tenemos una entidad con una lista de hijos, siempre debemos mover por memoria toda la lista de hijos. Si en algún paso se asigna como nula la lista de hijos, tendrémos un error al intentar sincronizar con la base de datos; y peor, si en algún momento llega una lista vacia, al sincronizar con la base de datos estaremos lanzando consultas para borrar los hijos anteriores. En la medida de lo posible evitemos usar ese mapeo, y tratemos de contenerlo a relaciones con pocas instancias. Para configurarlo debemos indicar:

  • cascade: para indicar que hacer con los hijos cuando se borra el padre.
  • fetch: para indicar como el modo en que se debe recuperar la información de la tabla relacionada (en el punto anterior de ManyToOne se indican los posibles valores y sus problemas).
  • mappedBy: indicador recomenable, que indica cual es el atributo de la tabla hija que mapea la relación con la tabla padre, y de donde obtener la información de columna.
@Entity
@Table(name = "aplicacion")
public class Aplicacion {
  @Id
  @Column(name = "uid", nullable = true)
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer uid;

  @Column(name = "nombre", nullable = false, length = 120, unique=true)
  private String nombre;

  @OneToMany(mappedBy = "aplicacion", fetch = FetchType.LAZY, cascade = CascadeType.ALL,
      orphanRemoval = true)
  private Set<VersionApp> versiones;
  ....
}
@Entity
@Table(name = "version_app",
    indexes = {@Index(name = "aplicacion_nombre_unique", columnList = ["aplicacion", "numero"], unique = true)})
public class VersionApp {
  @Id
  @Column(name = "uid", nullable = true)
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer uid;

  @ManyToOne(optional = false, fetch = FetchType.LAZY)
  @JoinColumn(name = "aplicacion", nullable = false)
  private Aplicacion aplicacion;

  @Column(name = "numero", nullable = false, length = 250)
  private String numero;

  @Column(name = "obsoleta", nullable = true)
  private Boolean obsoleta = false;

  ...
}

Consultas con JPA: HQL y Criteria

Para realizar consultas a la base de datos se puede utilizar directamente SQL mediante las llamadas queries nativas, pero jpa ofrece un lenguaje muy similar a SQL llamado HQL. La ventana de HQL es que hace referenica a las entidades y propiedades, y no tanto a los nombres de columnas, y resulta más cómodo para realizar las operaciones de join que estén mapeadas en las entidades.

Una tercera opción es utilizar un conjunto de funciones orientadas a objetos. Dichas funciones permiten "crear un criterio", e ir añadiendo comparaciones al criterio...

Cualquiera de las técnicas se puede aplicar a la búsqueda con repositorios, pero no entraremos en detalle de ellas aqui.

Repositorios Sring

Los repositorios son un conjunto de interfaces del paquete spring-data que sirven para la persistencia de objetos. Dentro de los disferentes modelos disponibles está un grupo de interfaces que son JpaRepository.

Si en un contexto spring configuramos la generación de repositorios, entonces buscará las diferentes interfaces que hereden de repositorio, y creará una clase dinámica como proxy para las mismas que proporcionará las implementaciones necesarias.

public interface IAplicacionRepository extends JpaRepository {
}

@Component
public class AppService {
  @Autowired
  private IAplicacionRepository appRepo;

  public void nuevaAplicacion(String nombre) {
    Aplicacion app = new Aplicacion(nombre);
    appRepo.save( app );
  }
}

Al heredar de JpaRepository ya disponemos de los métodos para recuperar por id, guardar, borrar y listar. Lo que necesitamos a mayores son métodos para realizar la consultas específicas. Para ello Spring utiliza dos técincas, una se basa en anotaciones, y otra es un truco relativamente sucio, pero muy cómodo: analiza el nombre de los métodos

Cuando se añade un metódo a la interfaz, se puede anotar con @Query o @NativeQuery y usar ? para referirse a parámetros. Por convenio se recupera una Lista de resultados para el caso de multiples valores, o un Opcional para el caso de un único valor.

  @Query("select a from Aplicacion where a.publicacion < ?1 and a.publicacio > ?2")
  List findAllOfTheMonth(Date desde, Date hasta);

  @Query("select a from Aplicacion where a.name=?1");
  Optional findOneNamedAs(String name);

El otro método se basa en el nombre del método. Si en la interfaz existe un método sin la anotación @Query, Spring cuando creé el proxy dinámico va a intentar inferir el comportamiento de consulta. Estos métodos deben retornar una lista o un valor opciones; y su nombre debe empezar por "findBy", segido por la lista de campos, y operadores.

  List findByDesdeGreaterThanAndDesdeLessThan(Date desde, Desde hasta);

  Optional findByName(String name);

Los operadores admitidos son:

And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is, Equals findByFirstname
findByFirstnameIs
findByFirstnameEquals
… where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull, Null findByAge(Is)Null … where x.age is null
IsNotNull, NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (parameter bound with appended %)

EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)

Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)

OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstame) = UPPER(?1)

Comentarios