Curso:
1.
Primeros pasos |
2.
Modelo básico del dominio (1) |
3.
Modelo básico del dominio (2) |
4.
Refinar la interfaz de usuario |
5.
Desarrollo ágil |
6.
Herencia de superclases mapedas |
7.
Herencia de entidades |
8.
Herencia de vistas |
9.
Propiedades Java |
10.
Propiedades calculadas |
11.
@DefaultValueCalculator en colecciones |
12.
@Calculation y totales de colección |
13.
@DefaultValueCalculator desde archivo |
14.
Evolución del esquema manual |
15.
Cálculo de valor por defecto multiusuario |
16.
Sincronizar propiedades persistentes y calculadas |
17. Lógica desde la base de datos
|
18.
Validando con @EntityValidator |
19.
Alternativas de validación |
20.
Validación al borrar |
21. Anotación Bean Validation propia |
22.
Llamada REST desde una validación |
23.
Atributos en anotaciones |
24.
Refinar el comportamiento predefinido |
25.
Comportamiento y lógica de negocio |
26.
Referencias y colecciones |
A.
Arquitectura y filosofía |
B. Java Persistence API
|
C. Anotaciones |
D. Pruebas automáticas
Java Persistence API (JPA) es el estándar Java para hacer mapeo
objeto-relacional. El mapeo objeto-relacional te permite acceder a los
datos en una base de datos relacional usando un estilo orientado a
objetos. En tu aplicación solo trabajas con objetos, estos objetos se
declaran como persistentes, y es responsabilidad del motor JPA leer y
grabar los objetos desde la base de datos a la aplicación.
JPA mitiga el famoso problema de desajuste de impedancia, que se produce
porque las bases de datos relacionales tienen una estructura, tablas y
columnas con datos simples, y las aplicaciones orientadas a objetos otra,
clases con referencias, colecciones, interfaces, herencia, etc. Es decir,
si en tu aplicación estás usando clases Java para representar conceptos de
negocio, tendrás que escribir bastante código SQL para escribir los datos
desde tus objetos a la base de datos y viceversa. JPA lo hace para ti.
Este lección es una introducción a JPA. Para una completa inmersión en
esta tecnología estándar necesitarías leer un libro completo sobre JPA, de
hecho, se citan algunos en el sumario de esta lección. OpenXava soporta
JPA 2.2 desde v6.1.
Por otra parte, si ya conoces JPA puedes saltarte esta lección.
Anotaciones
JPA
JPA tiene 2 aspectos diferenciados, el primero es un conjunto de
anotaciones Java para añadir a tus clases marcándolas como persistentes y
dando detalles acerca del mapeo entre las clases y las tablas. Y el
segundo es un API para leer y escribir objetos desde tu aplicación. Veamos
primero las anotaciones.
Entidad
En la nomenclatura JPA a una clase persistente se le llama entidad.
Podemos decir que una entidad es una clase cuyas instancias se graban en
la base de datos. Usualmente cada entidad representa un concepto del
dominio, por lo tanto usamos una entidad JPA como base para definir un
componente de negocio en OpenXava, de hecho puedes crear un aplicación
completa de OpenXava a partir de simples entidades JPA. Una entidad JPA se
define así:
@Entity // Para definir esta clase como persistente
@Table(name="GSTCST") // Para indicar la tabla de la base de datos (opcional)
public class Cliente {
Como puedes ver solo has de marcar tu clase con la anotación
@Entity
y opcionalmente también con la anotación
@Table, en este caso
estamos diciendo que la entidad
Cliente se graba en la tabla
GSTCST de la base de datos. De ahora en adelante, JPA guardará y
recuperará información entre los objetos Cliente en la aplicación y la
tabla GSTCST en la base de datos, como se muestra aquí:
Además, marcar Cliente con
@Entity es suficiente para que
OpenXava la reconozca como un componente de negocio. Sí, en OpenXava
“entidad” es sinónimo de componente de negocio.
Propiedades
El estado básico de una entidad se representa mediante propiedades. Las
propiedades de una entidad son propiedades Java convencionales, con
getters
y
setters:
private String nombre;
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
Por defecto las propiedades son persistentes, es decir, JPA asume que la
propiedad
nombre se almacena en la columna llamada 'nombre' de
la tabla en la base de datos. Si quieres que una determinada propiedad no
se guarde en la base de datos has de marcarla como
@Transient:
@Transient // Marcada como transitoria, no se almacena en la base de datos
private String nombre;
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
Nota que hemos anotado el campo, puedes anotar el
getter en su
lugar, si así lo prefieres:
private String nombre;
@Transient // Marcamos el getter, por tanto todas las anotaciones JPA
public String getNombre() { // en esta entidad tienen que estar en los getters
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
Esta norma aplica a todas la anotaciones JPA, puedes anotar el campo
(acceso basado en el campo) o el
getter (acceso basado en la
propiedad), pero no mezcles los dos estilos en la misma entidad.
Otras anotaciones útiles para las propiedades son
@Column para
especificar el nombre y longitud de la columna de la tabla, e
@Id
para indicar que propiedad es la propiedad clave. Puedes ver el uso de
estas anotaciones en la ya utilizable entidad
Cliente:
@Entity
@Table(name="GSTCST")
public class Cliente {
@Id // Indica que number es la propiedad clave (1)
@Column(length=5) // Aquí @Column indica solo la longitud (2)
private int numero;
@Column(name="CSTNAM", length=40) // La propiedad name se mapea a la columna
private String nombre; // CSTNAM en la base de datos
public int getNumero() {
return numero;
}
public void setNumero(int numero) {
this.numero = numero;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
}
Es obligatorio que al menos una propiedad sea clave (1). Has de marcar la
propiedad clave con
@Id y normalmente se mapea contra la columna
clave de la tabla.
@Column puede usarse para indicar la longitud
sin el nombre de columna (2). La longitud es usada por el motor JPA para
la generación de esquema, pero también es usada por OpenXava para conocer
el tamaño del editor en la interfaz de usuario. A partir del código de la
entidad
Cliente OpenXava genera la siguiente interfaz de
usuario:
Ahora que ya sabes como definir propiedades básicas en tu entidad,
aprendamos como declarar relaciones entre entidades usando referencias y
colecciones.
Referencias
Una entidad puede hacer referencia a otra entidad. Únicamente has de
definir una referencia Java convencional anotada con la anotación JPA @
ManyToOne:
@Entity
public class Factura {
@ManyToOne( // La referencia se almacena como una relación a nivel de base de datos (1)
fetch=FetchType.LAZY, // La referencia se cargará bajo demanda (2)
optional=false) // La referencia tiene que tener valor siempre
@JoinColumn(name="INVCST") // INVCST es la columna para la clave foranea (3)
private Cliente cliente; // Una referencia Java convencional (4)
// Getter y setter para cliente
Como puedes ver hemos declarado una referencia a
Cliente dentro
de
Factura en un estilo Java simple y llano (4).
@ManyToOne
(1) es para indicar que esta referencia se almacenará en la base de datos
como una relación muchos-a-uno entre la tabla para
Factura y la
tabla para
Cliente, usando la columna INVCST (3) como clave
foránea.
@JoinColumn (3) es opcional. JPA asume valores por
defecto para las columnas de unión (CLIENTE_NUMERO en este caso).
Si usas
fetch=FetchType.LAZY (3) los datos del objeto
Cliente
no se cargarán hasta que se usen por primera vez. Es decir, en el momento
justo que uses la referencia a
Cliente, por ejemplo, llamando al
método
factura.getCliente().getNombre() los datos del
Cliente
son cargados desde la base de datos. Es aconsejable usar siempre
lazy
fetching.
Una referencia Java convencional normalmente corresponde a una relación
@ManyToOne
en JPA y a una asociación *..1 en UML:
Esta es la interfaz de usuario que OpenXava genera automáticamente para
una referencia:
Has visto como hacer referencia a otras entidades, pero también puedes
hacer referencia a otros objetos que no son entidades, por ejemplo, a
objetos incrustados.
Clases
incrustables
Además de entidades puedes usar clases incrustables para modelar algunos
conceptos de tu dominio. Si tienes una entidad A que tiene una referencia
a B, modelarías B como una clase incrustable cuando:
- Puedas decir que A tiene un B.
- Si A es borrado su B es borrado también.
- B no es compartido.
A veces el mismo concepto puede ser modelado como incrustable o como
entidad. Por ejemplo, el concepto de dirección. Si una dirección es
compartida por varias personas entonces tienes que usar una referencia a
una entidad, mientras que si cada persona tiene su propia dirección quizás
un objeto incrustable sea mejor opción.
Modelemos una dirección como una clase incrustable. Es fácil, simplemente
crea una simple clase Java y anótala como
@Embeddable:
@Embeddable // Para definir esta clase como incrustable
public class Direccion {
@Column(length=30) // Puedes usar @Column como en una entidad
private String viaPublica;
@Column(length=5)
private int codigoPostal;
@Column(length=20)
private String poblacion;
// Getters y setters
...
}
Y ahora, crear una referencia a
Direccion desde una entidad
también es fácil. Se trata simplemente de una referencia Java normal
anotada como
@Embedded:
@Entity
@Table(name="GSTCST")
public class Cliente {
@Embedded // Referencia a una clase incrustable
private Direccion direccion; // Una referencia Java convencional
// Getter y setter para address
...
}
Desde el punto de vista de la persistencia un objeto incrustable se
almacena en la misma tabla que su entidad contenedora. En este caso las
columnas viaPublica, codigoPostal y poblacion están en la tabla para
Cliente.
Direccion no tiene tabla propia.
OpenXava genera automáticamente para una referencia a una clase
incrustable una interfaz de usuario como esta:
Colecciones
Una entidad puede tener una colección de entidades. Sólo has de definir
una colección de Java convencional anotada con las anotaciones JPA
@OneToMany
o
@ManyToMany:
@Entity
public class Cliente {
@OneToMany( // La colección es persistente (1)
mappedBy="cliente") // La referencia cliente de Factura se usa
// para mapear la relación a nivel de base de datos (2)
private Collection<Factura> facturas; // Una colección Java convencional (3)
// Getter y setter para facturas
...
}
Como puedes ver declaramos una colección de entidades
Factura
dentro de
Cliente en un estilo Java plano (3).
@OneToMany
(1) es para indicar que esta colección se almacena en la base de datos con
una relación uno-a-muchos entre la tabla para
Cliente y la tabla
para
Factura, usando la columna de
cliente en
Factura
(normalmente una clave foránea hacia la tabla de
Cliente desde
la tabla de
Factura) .
Una colección de entidades en Java corresponde a una relación
@OneToMany
o
@ManyToMany en JPA y a una asociación con una multiplicidad
1..* o *..* en UML:
Se puede simular la semántica de los objetos incrustados usando una
colección de entidades con el atributo
cascade de
@OneToMany:
@Entity
public class Factura {
@OneToMany (mappedBy="factura",
cascade=CascadeType.REMOVE) // Cascade REMOVE para simular incrustamiento
private Collection<DetalleFactura> detalles;
// Getter y setter para detalles
...
}
DetalleFactura es una entidad:
@Entity
public class DetalleFactura {
...
}
Así, cuando una factura se borra sus líneas de detalle se borran también.
Podemos decir que una factura tiene líneas de detalle.
Esta es la interfaz de usuario que OpenXava genera automáticamente para
una colección de entidades:
Hay algunas ligeras diferencias en el
comportamiento de la interfaz de usuario si usas cascade REMOVE o ALL.
Con cascade REMOVE o ALL cuando el usuario pulsa para añadir
un nuevo elemento, puede introducir todos los datos del elemento, por
otra parte si tu colección no es cascade REMOVE o ALL , cuando
el usuario pulsa para añadir un nuevo elemento se muestra una lista de
entidades para escoger.
Además, se puede definir una colección de auténticos objetos embebidos.
Podriamos reescribir el ejemplo de los detalles de factura así:
@Entity
public class Factura {
@ElementCollection
private Collection<DetalleFactura> detalles;
// Getter and setter for details
...
}
Fíjate que anotamos la colección con
@ElementCollection. En este
caso
DetalleFactura es una clase incrustada anotada con
@Embeddable:
@Embeddable
public class DetalleFactura {
...
}
La interfaz de usuario que OpenXava genera para una
@ElementCollection:
En una colección de elementos (
@ElementCollection) el usuario
puede editar cualquier propiedad de cualquier fila en cualquier momento.
También se puede quitar y añadir filas, pero los datos no se grabarán en
la base de datos hasta que la entidad principal se grabe.
Hay muchos casos que se pueden modelar con
@OneToMany(cascade=CascadeType.REMOVE)
o
@ElementCollection. Ambas opciones borran los elementos de la
colección cuando la entidad contenedora se borra. Ambas opciones tiene la
semántica de incrustado, es decir podemos decir "tiene un". Escoger una
opción es fácil para un programador OpenXava, porque cada opción genera
una interfaz de usuario totalmente diferente.
Clave
compuesta
Es mejor evitar el uso de claves compuestas. Siempre tienes la opción de
usar un identificador oculto autogenerado. Aunque, algunas veces tienes la
necesidad de conectarte a bases de datos legadas o puede que el diseño del
esquema lo haya hecho alguien que le gustan las claves compuestas, y no
tengas otra opción que usar claves compuestas aunque no sea lo ideal. Por
lo tanto, vamos a aprender como usar una clave compuesta.
Veamos una versión sencilla de una entidad
Factura:
package com.tuempresa.facturacion.modelo;
import java.time.*;
import java.util.*;
import javax.persistence.*;
import org.openxava.annotations.*;
@Entity
@IdClass(FacturaKey.class) // La clase id contiene todas las propiedades clave (1)
public class Factura {
@Id // Aunque tenemos las clase id aún es necesario marcarlo como @Id (2)
@Column(length = 4)
private int anyo;
@Id // Aunque tenemos las clase id aún es necesario marcarlo como @Id (2)
@Column(length = 6)
private int numero;
@Required
private LocalDate fecha;
@Stereotype("MEMO")
private String observaciones;
// RECUERDA GENERAR LOS GETTERS Y SETTERS PARA LOS CAMPOS
}
Si quieres usar anyo y
numero como clave compuesta para Factura, una forma
de hacerlo, es marcándolos con @Id (2), y además tener una
clase id (1). La clase id tiene que tener anyo y numero
como propiedades. Puedes ver FacturaKey aquí:
package com.tuempresa.facturacion.modelo;
public class FacturaKey implements java.io.Serializable { // La clase key tiene que ser serializable
private int anyo; // Contiene las propiedades marcadas ...
private int numero; // ... como @Id en la entidad
public boolean equals(Object obj) { // Ha de definir el método equals
if (obj == null) return false;
return obj.toString().equals(this.toString());
}
public int hashCode() { // Ha de definir el método hashCode
return toString().hashCode();
}
public String toString() {
return "FacturaKey::" + anyo + ":" + numero;
}
// RECUERDA GENERAR LOS GETTERS Y SETTERS PARA anyo Y numero
}
En este código se ven algunos de
los requerimientos para una clase id, como el ser serializable e
implementar hashCode() y equals(). OpenXava Studio
(o Eclipse) puede generartelos con Source > Generate hashCode()
and equals()...
Has visto como escribir tus entidades usando anotaciones JPA, y como
OpenXava las interpreta para así generar una interfaz de usuario adecuada.
Ahora vas a aprender como usar el API JPA para leer y escribir de la base
de datos de desde tu propio código.
API
JPA
La clase de JPA más importante es
javax.persistence.EntityManager.
Un
EntityManager te permite grabar, modificar y buscar
entidades.
El siguiente código muestra la forma típica de usar JPA en una aplicación
no OpenXava:
EntityManagerFactory f = // Necesitas un EntityManagerFactory para crear un manager
Persistence.createEntityManagerFactory("default");
EntityManager manager = f.createEntityManager(); // Creas el manager
manager.getTransaction().begin(); // Has de empezar una transacción
Cliente cliente = new Cliente(); // Ahora creas tu entidad
cliente.setNumero(1); // y la rellenas
cliente.setNombre("JAVI");
manager.persist(cliente); // persist marca el objeto como persistente
manager.getTransaction().commit(); // Al confirmar la transacción los cambios se
// efectúan en la base de datos
manager.close(); // Has de cerrar el manager
Ves como es una forma muy verbosa de trabajar. Demasiado código
burocrático. Si así lo prefieres puedes usar código como éste dentro de
tus aplicaciones OpenXava, aunque OpenXava te ofrece una forma más sucinta
de hacerlo:
Cliente cliente = new Cliente();
cliente.setNumero(1);
cliente.setNombre("PEDRO");
XPersistence.getManager().persist(cliente); // Esto es suficiente (1)
Dentro de un aplicación OpenXava puedes obtener el
manager
mediante la clase
org.openxava.jpa.XPersistence. No necesitas
cerrar el
manager, ni arrancar y parar la transacción. Este
trabajo sucio lo hace OpenXava por ti. El código de arriba es suficiente
para grabar una nueva entidad en la base de datos (1).
Si quieres modificar una entidad existente has de hacerlo así:
Cliente cliente = XPersistence.getManager()
.find(Cliente.class, 1); // Primero, buscas el objeto a modificar (1)
cliente.setNombre("PEDRITO"); // Entonces, cambias el estado del objeto. Nada más
Para modificar un objeto solo has de buscarlo y modificarlo. JPA es
responsable de grabar los cambios en la base de datos al confirmar la
transacción (a veces antes), y OpenXava confirma las transacciones JPA
automáticamente.
Has visto como encontrar por clave primaria, usando
find().
Además, JPA te permite usar consultas:
Cliente pedro = (Cliente) XPersistence.getManager()
.createQuery(
"from Cliente c where c.nombre = 'PEDRO')") // Consulta JPQL (1)
.getSingleResult(); // Para obtener una única entidad (2)
List pedros = XPersistence.getManager()
.createQuery(
"from Cliente c where c.nombre like 'PEDRO%')") // Consulta JPQL
.getResultList(); // Para obtener una colección de entidades (3)
Puedes usar el lenguaje de consultas de JPA (Java Persistence Query
Language, JPQL, 1) para crear consultas complejas sobre tu base de datos,
y obtener una entidad única, usando el método
getSingleResult()
(2), o una colección de entidades mediante el
getResultList()
(3).
Resumen
Esta lección ha sido una breve introducción a la tecnología JPA. Por
desgracia, muchas e interesantes cosas sobre JPA se nos han quedado en el
tintero, como la herencia, el polimorfismo, las claves compuestas,
relaciones uno a uno y muchos a muchos, relaciones unidireccionales,
métodos de retrollamada, consultas avanzadas, etc. De hecho, hay más de 80
anotaciones en JPA. Necesitaríamos varios libros completos para aprender
todos los detalles sobre JPA.
Afortunadamente, tendrás la oportunidad de aprender algunos casos de uso
avanzados de JPA en el transcurso de este curso. Y si aun quieres aprender
más, lee libros y referencias, como por ejemplo:
JPA es una tecnología indisputable en el universo Java de empresa, por
tanto todo el conocimiento y código que acumulemos alrededor de JPA es
siempre una buena inversión.
¿Problemas con la lección? Pregunta en el foro