
Si las bases de datos relacionales se fundamentan en las tablas, Jakarta Persistence hace lo propio con un concepto equivalente adaptado al mundo de la orientación a objetos: las clases de tipo entidad. Veamos cómo crearlas y las principales opciones a nuestra disposición.
Definición de entidades
Una entidad es una clase que representa un «concepto» cuyas instancias son persistibles en una base de datos relacional. En ella definimos con anotaciones la equivalencia entre sus atributos y las columnas de una tabla. Dicho de la manera más simple posible: una entidad representa a una tabla, y sus objetos a los registros o tuplas. Veremos algunas excepciones, de uso poco frecuente, a esta afirmación.
Se ve más claro con una imagen. La ilustración muestra la estructura de una entidad y la tabla a la que está «mapeada».

Una clase entidad no tiene que heredar ni implementar nada, es un bean o POJO estándar que cumple con los siguientes requisitos.
- Está anotada con @Entity. Como alternativa, hace años en desuso, la clase se puede declarar en el fichero persistence.xml.
- Puede tener varios constructores, pero siempre debe existir el constructor vacío y ser público o protegido. Hibernate admite que sea privado.
- Ni la clase ni los atributos persistibles pueden ser finales.
- Las entidades pueden especializar clases que no lo sean y viceversa. Eso sí, la herencia de entidades debe configurarse. Lo veremos en el capítulo 26.
- Debe tener un atributo utilizado como identificador, o heredar de una entidad que ya lo tenga. Equivale a la clave primaria del modelo relacional.
- Se aceptan clases abstractas.
Los novedosos records de Java 16 no sirven porque sus atributos son finales (están pensados para crear objetos inmutables de usar y tirar).
Veamos una versión simplificada de nuestra primera entidad: la clase que representa un gasto (expense). Se utiliza Lombok, más información al final del capítulo.
package com.danielme.onbudget.domain.entities;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDate;
@Entity
@Getter
@Setter
public class Expense {
@Id
private Long id;
private BigDecimal amount;
private String concept;
private String comments;
private OffsetDateTime dateTime;
}
Tenemos la configuración mínima imprescindible: las anotaciones @Entity e @Id. Si permitimos que Hibernate cree la tabla equivalente en MySQL (propiedad hibernate.hbm2ddl.auto), lanzará la siguiente sentencia SQL.
create table Expense (
id bigint not null,
amount decimal(19,2),
comments varchar(255),
concept varchar(255),
dateTime date,
primary key (id))
engine = InnoDB
La tabla recibe el nombre de la clase y sus columnas el de los atributos, respetando las mayúsculas. No falta su clave primaria.
Obsérvese que los atributos no tienen anotación alguna y todos cuentan con su equivalente en la tabla. En realidad, esto se indica con @Basic, pero no es necesaria porque JPA asume por omisión su comportamiento para todos los atributos. Si alguno debe excluirse de la vinculación con la tabla, lo anotaremos con @Transient.
¿Qué tipos básicos pueden transformarse automáticamente en columnas? La lista es amplia.
- Los tipos primitivos y sus «wrappers» en objetos.
- Tablas de bytes y caracteres.
- Tiempos y fechas con Date, Calendar, LocalDate, LocalTime, LocalDateTime, OffsetTime, OffsetDateTime, java.sql.Date, java.sql.Time y java.sql.Timestamp.
- Enumerados.
- Números con BigDecimal y BigInteger.
- En general, cualquier Serializable, siempre y cuando el atributo se declare de ese tipo.
- Y, por supuesto, String.
Esta tabla muestra la relación que hace Hibernate entre estos tipos y las columnas de MySQL.
long, Long | bigint |
boolean, Boolean | bit(1) |
char, Character | char(1) |
java.sql.Date, LocalDate | date |
java.sql.timestamp, Calendar, Date, LocalDateTime, OffsetDateTime | datetime(6) |
java.sql.time, OffsetTime, LocalTime | time |
BigInteger, BigDecimal | decimal (19, 2) |
double, Double | double |
Float, float | float |
int, Integer | int |
short, Short | smallint |
Serializable, byte[], Byte[] | tinyblob |
byte, Byte | tinyint |
String, char[], Character[] | varchar(255) |
enum | int |
No es ningún problema que estas asociaciones no se amolden a lo que necesitamos, pues veremos cómo personalizarlas y la creación de conversores a medida.
Principales anotaciones
@Id
Es obligatorio que toda entidad tenga un identificador único. Es un tema amplio e importante que examinaremos con detalle en el próximo capítulo.
@Table

Con esta anotación establecemos el nombre de la tabla. Si tengo la posibilidad de elegirlo, me gusta definirlo en minúsculas y en plural. Lo fundamental no es el criterio, sino el hecho de seguir siempre las mismas convenciones. Por cierto, MySQL en Linux tiene en cuenta la capitalización de los nombres de las tablas, así que mucho cuidado.
@Entity
@Table(name = "expenses")
@Getter
@Setter
public class Expense {
Con uniqueConstraints se define la unicidad de una o varias columnas en conjunto usando la anotación @UniqueConstraint. Por ejemplo: dada una tabla que guarda ciudades, se admite tener varias con el mismo nombre si se ubiquen en distintas regiones administrativas.
@Entity
@Table(name="cities", uniqueConstraints={
@UniqueConstraint(name="CITY_REGION_UK", columnNames = {"name" , "region"})})
public class City {
@Id
private Long id;
private String name;
private String region;
Solo usaremos esta opción si creamos la base de datos con Hibernate. El nombre de la constraint es opcional, pero conviene indicarlo porque de lo contrario se generará un alfanumérico ininteligible.

Se puede conseguir lo mismo con la anotación @Index que permite la definición de índices tanto únicos como no únicos.
@Table(name="cities",
indexes={@Index(name="CITY_REGION_UK", columnList="name, region", unique=true)}
)
Las propiedades catalog y schema indican, tal y como su nombre sugiere, el catalog y el schema en el que se encuentra la tabla. Su disponibilidad depende de la base de datos. En MySQL no tenemos schema, y catalog se refiere al nombre de la base de datos.
@Entity
@Table(name = "expenses", catalog = "myCatalog")
public class Expense {
El catalog se añade al nombre de la tabla en las consultas que Hibernate genera.
select expense0_.id as id1_0_0_ from myCatalog.expenses expense0_ where expense0_.id=?
Si en la cadena de conexión al servidor MySQL indicamos la base de datos que vamos a usar -es la práctica habitual- podemos olvidarnos de esta opción salvo que accedamos a tablas pertenecientes a bases de datos distintas, situación que no trataremos en el curso.
@Column

Gracias a esta anotación personalizamos la relación entre los atributos de la entidad y las columnas de la tabla.
- name. El nombre de la columna. Hemos visto que por omisión se toma el del atributo. El criterio usado en Java (notación camel-case) no suele emplearse en las bases de datos por su naturaleza insensible a la capitalización, usándose la notación snake-case (palabras separadas por la barra baja).
@Column(name = "REGION_CODE")
private String regionCode;
- insertable (true, false, por omisión true). Incluye\excluye el atributo de las operaciones de inserción, realizadas con una sentencia INSERT de SQL.
- updatable (true, false, por omisión true). Incluye\excluye el atributo de las operaciones de actualización, realizadas con una sentencia UPDATE de SQL. Por tanto, son atributos que se establecen en el momento de creación del registro y que no deben ser modificados. El ejemplo típico es un atributo que indica el instante en el que se creó el registro.
@Column(updatable = false, nullable = false)
private LocalDateTime creation;
Con la combinación de las opciones updatable=false y insertable=false estamos definiendo atributos de solo lectura. Puesto que las entidades no pueden ser inmutables (atributos finales), podemos conseguir algo parecido con esta configuración para una tabla que, por ejemplo, almacena mediciones y cuya entidad debe usarse en modo de solo lectura.
@Entity
@Table(name="measures")
public class Measure {
@Id
@Column(insertable = false, updatable = false)
private Long id;
@Column(insertable = false, updatable = false)
private BigDecimal value;
@Column(insertable = false, updatable = false)
private LocalDateTime moment;
Hibernate cuenta con la anotación @Inmutable aplicable a entidades y atributos. Su comportamiento es equivalente al de la opción updatable = false: excluye los atributos indicados de las sentencias UPDATE de SQL que actualizan la entidad. Las inserciones y borrados de la entidad realizadas con el gestor de en entidades no se ven afectados.
En cualquier caso, ten en cuenta que las restricciones impuestas por @Inmutable, insertable y updatable pueden saltarse con sentencias de tipo JPQL, SQL y con API Criteria.
Las siguientes propiedades solo se utilizan para la creación de la tabla.
- nullable (true\false, defecto true). Define si el atributo puede ser nulo. Si no, al crearse la tabla desde Hibernate se aplica la restricción «NOT NULL» a la columna. En el caso de los tipos primitivos (int, float…), siempre serán no nulos.
- length (entero, por omisión 255). Tamaño máximo de los campos de texto (atributos de tipo String).
- unique (true, false, por omisión false). Indica si el campo es único. Es la misma funcionalidad que proporciona @UniqueConstraint en @Table, pero aquí aplicada a una sola columna y sin la posibilidad de asignar un nombre al índice.
- precision y scale. Definen el tamaño total y número de decimales de los numéricos decimales (para BigDecimal, no es válido para Double y Float).
- columnDefinition. Cadena que se «inyecta» en el código SQL que el ORM genera para definir la columna lo que nos permite especificar el tipo y su valor predeterminado. Si usamos tipos propios de una base de datos en concreto, perderemos portabilidad. Ejemplo:
@Column(columnDefinition="SMALLINT default 1")
private int code;
- table. El nombre de la tabla que contiene la columna. Veremos esta propiedad cuando hablemos de la anotación @SecondaryTable para asociar una entidad con dos tablas.
@Temporal

Su función es indicar cómo debe tratarse un atributo de tipo Date o Calendar según un valor del enumerado TemporalType: fecha, tiempo, o fecha y hora. Si esta anotación no se usa, se considera fecha y hora.
@Temporal(TemporalType.DATE)
private Calendar fecha;
@Temporal(TemporalType.TIME)
private Calendar hora;
@Temporal(TemporalType.TIMESTAMP)
private Calendar timestamp;
No hace falta usar @Temporal ni para los tipos temporales de java.sql (java.sql.Date , java.sql.Time, java.sql.Timestamp) ni para los de la nueva API de fechas de Java 8 contempladas por JPA 2.2 (LocalDate, LocalDateTime, LocalTime, OffsetTime y OffsetDateTime) (*). Según la clase, se decide de forma automática entre fecha, hora y fecha y hora.
(*) Hibernate amplía la especificación añadiendo soporte para Duration, Instant y ZonedDateTime.
Siempre que podamos, usemos las clases de java.time y digamos adiós -por fin- a las vetustas y limitadas Date y Calendar, así como a librerías externas (la célebre Joda Time).
@Enumerated

Aunque algunas bases de datos incorporan un tipo enumerado, JPA define una conversión válida para cualquiera de ellas. Por omisión, los enumerados se traducen a un entero que representa la posición, empezando en cero, de cada valor según el orden en el que declaran.
public enum Shape {
CIRCLE, // -> 0
RECTANGLE, // -> 1
TRIANGLE; // -> 3
}
Con @Enumerated podemos utilizar como criterio alternativo el nombre. Resulta más legible, pero menos eficiente porque requiere de una columna de mayor tamaño; con la posición solo se almacena un numérico con uno o dos dígitos a lo sumo:
@Column(columnDefinition = "TINYINT(1)")
private Shape shape1;
@Enumerated(EnumType.STRING)
@Column(length = 9)
private Shape shape2;

Ninguna de estas alternativas es robusta del todo una vez que tengamos registros en las tablas. La primera porque no permite cambiar el orden, circunstancia que puede darse eliminando una opción que no sea la última. En el caso de la cadena, no podemos editar el nombre. Son situaciones poco habituales, pero si se producen pueden causar un pequeño caos dando lugar a valores asignados de forma incorrecta o errores a la hora de recuperar los registros.
java.lang.IllegalArgumentException: No enum constant com.danielme.jakartaee.jpa.entities.Icon.Shape.TRIANGLE
Además, si trabajamos con base de datos ya existentes que no podemos rediseñar a nuestra conveniencia, es posible que ninguna de ellas nos sirva. Cuando necesitemos otro modo de traducir enumerados, escribiremos un conversor. Lo trataremos en breve.
@Lob
Las bases de datos cuentan con tipos específicos para las columnas que tienen que almacenar cadenas de texto de gran tamaño y datos binarios (suelen llamarse Blob y Clob). En JPA, se usan con la anotación @Lob.
@Lob
private byte[] binaryContent;
@Lob
private String bigText;

Mucho cuidado con guardar binarios en tablas, aunque sean pequeños. Cada vez que se obtenga la entidad, al menos con Hibernate, siempre se establecerá el contenido del binario en el atributo y esto puede causar graves problemas de rendimiento. Aporto soluciones en mi tutorial JPA e Hibernate: relaciones y atributos Lazy.
@Embeddable
Los atributos incrustados o «embebidos» son una forma directa y sencilla de usar clases nuestras como tipos de atributos en JPA. Estas clases no son entidades, lo que implica que carecen de identificador, y sus datos se guardan en la tabla de la entidad que las contengan.
@Entity
@Table(name = "users")
public class User {
@Id
private Long id;
@Embedded
private Name name;
@Embeddable
public class Name {
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
El contenido de User#name, atributo incrustado (@Embedded) porque pertenece a una clase declarada como «incrustable» (@Embeddable (*)), se guarda en columnas de la tabla de la entidad User.
(*) No es obligatorio anotar el atributo con @Embedded, pero suele hacerse por claridad.

Para facilitar la reutilización de las clases incrustables, JPA permite que sus atributos puedan personalizarse en cada entidad que las use.
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "firstName", column = @Column(name = "USER_FIRSTNAME")),
@AttributeOverride(name = "lastName", column = @Column(name = "USER_LASTNAME"))
})
private Name name;
Si Name fuera una entidad, es decir, tuviera su propia tabla, no podríamos incrustarla en User y tendríamos que definir una relación entre entidades \ tablas, en este ejemplo de tipo one-to-one, y que requiere el uso de una clave ajena. Examinaremos en profundidad la creación de relaciones en próximos capítulos, pero vamos a ver un pequeño aperitivo. Y es que las clases incrustables, además de campos primitivos, pueden contener atributos incrustados y relaciones con entidades.
package com.danielme.jakartaee.jpa.entities;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Embeddable
@Getter
@Setter
public class Preferences {
private Boolean notifications;
@OneToOne
@JoinColumn(name = "lan_id")
private Language language;
}
Vemos que Preferences tiene una relación con la entidad \ tabla Language, reflejada en una clave ajena o foránea de llamada «lan_id». Nada cambia con respecto a lo que ya sabemos, y esa clave ajena estará físicamente en la tabla de la entidad que use a la clase incrustada, al igual que la columna correspondiente a notifications.
@Entity
@Table(name="users")
@Getter
@Setter
public class User {
@Id
private Long id;
@Embedded
private Preferences preferences;
}

Getters y setters
¿Necesita Hibernate que los atributos tengan sus métodos getters y setters? Esta es una duda frecuente, y la respuesta nos la da el modo de acceso que establezcamos en JPA para cada entidad.
El modo se determina cuando empleamos la anotación @Id. Si la aplicamos a un atributo, tal y como hemos venido haciendo, se usa el modo de acceso por atributo (field access) y la implementación de JPA debe ser capaz de acceder a ellos sin pasar por los getters y setters. Todas las anotaciones relacionadas con los atributos (@Column, @Temporal, etc) deben aplicarse sobre los mismos.
El modo de acceso propiedad (property access) se activa si @Id se usa en un método getter. Ahora es obligatorio definir los getters y setters -públicos o protegidos- siguiendo las convenciones habituales para que la implementación de JPA acceda a los atributos. Además, las anotaciones se aplicarán a los getters.
private Long id;
@Id
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
Ambos modos son incompatibles, pero podemos rizar el rizo y combinarlos usando la anotación @Access en aquellos atributos para los que no queremos que se usen sus métodos de acceso.
private Long id;
@Access(AccessType.FIELD)
private Address address;
@Id
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
Lo cierto es que el modo property access no suele usarse. Se prefiere el acceso directo a los atributos porque nos da libertad para implementar solo aquellos getters y setters que necesitemos (*). Asimismo, la configuración es más legible si las anotaciones se ubican al principio de la clase en los atributos y no están repartidas por los getters . Seguiremos este criterio durante el curso, pero, como siempre, no está de más que el lector conozca todas las opciones.
(*) No crees getters y setters para todos los atributos de una clase por costumbre. Añade los que vayas necesitando para para hacer al código más seguro.
Conversores de atributos personalizados
A veces, las conversiones de los valores de las columnas a sus objetos y viceversa no se adaptan a lo que necesitamos. Hace bastantes años conocí a un cliente que guardaba los tipos lógicos en Oracle como un carácter que podía tomar los valores ‘S’ y ‘N’. Para evitar la introducción de datos incorrectos, a las columnas se les aplicaban una restricción de tipo CHECK (disponible MySQL desde la versión 8.0.16).
CONSTRAINT check_logico CHECK (logico IN ('S', 'N'));
Hibernate provee la anotación @Check para crear este tipo de restricciones.
@Check(constraints = "logico IN ('S', 'N')")
@Entity
public class Account {
Para lidiar con estos casos, JPA contempla la creación de conversores de atributos personalizados. Un conversor es una implementación de la interfaz AttributeConverter para dos tipos: la clase del atributo en la entidad y la idónea para la columna. Según esto, y siguiendo con el ejemplo, necesitamos un conversor entre Boolean -admitimos que el campo pueda valer NULL- y Character.
package com.danielme.jakartaee.jpa.converters;
import jakarta.persistence.AttributeConverter;
public class BooleanAttributeConverter implements AttributeConverter<Boolean, Character> {
private static final Character TRUE_VALUE = 'S';
private static final Character FALSE_VALUE = 'N';
@Override
public Character convertToDatabaseColumn(Boolean attribute) {
if (attribute == null) {
return null;
}
return attribute ? TRUE_VALUE : FALSE_VALUE;
}
@Override
public Boolean convertToEntityAttribute(Character dbData) {
if (dbData == null) {
return null;
}
return dbData.equals(TRUE_VALUE);
}
}
BooleanAttributeConverter no requiere de muchas explicaciones. Sus dos métodos hacen la transformación entre lógicos y caracteres. Merece la pena subrayar que en un conversor se admiten inyecciones con Jakarta CDI.

Lo aplicaremos a un atributo con @Convert. En nuestro caso, en la clase incrustable de ejemplos anteriores.
@Embeddable
@Check(constraints = "notifications IN ('S', 'N')")
@Getter
@Setter
public class Preferences {
@Convert(converter = BooleanAttributeConverter.class)
@Column(columnDefinition = "CHAR(1)")
private Boolean notifications;
Considerando el siguiente juego de datos…
users:
- id: 1
notifications: 'S'
- id: 2
notifications: 'N'
- id: 3
…esta prueba, sita en la clase JpaArquillianTest, será correcta.
@Test
@DataSet(value = "/datasets/users.yml")
void testBooleanConverter() {
assertThat(em.find(User.class, Datasets.USER_ID_1).getPreferences().getNotifications())
.isTrue();
assertThat(em.find(User.class, Datasets.USER_ID_2).getPreferences().getNotifications())
.isFalse();
assertThat(em.find(User.class, Datasets.USER_ID_3).getPreferences())
.isNull();
}
Si queremos aplicar el conversor a todos los atributos persistibles de tipo Boolean de nuestras entidades, no hace falta anotar cada uno de ellos con @Convert. Se puede configurar su aplicación automática de esta manera.
@Converter(autoApply = true)
public class BooleanAttributeConverter implements AttributeConverter<Boolean, Character> {
Con esta configuración, si no deseamos la conversión para un atributo en concreto, lo indicamos así.
@Convert(disableConversion=true)
Volvamos a los enumerados. Comenté que si las alternativas ofrecidas por JPA no nos sirven, podemos crear un conversor personalizado. Supongamos que en la base de datos los tipos que modelamos con el enumerado se identifican con un número.
public enum Shape {
CIRCLE(1),
RECTANGLE(2),
TRIANGLE(3);
private final Integer id;
private static final Map<Integer, Shape> reverse;
static {
reverse = new HashMap<>();
for (Shape shape : Shape.values()) {
reverse.put(shape.id(), shape);
}
}
Shape(int id) {
this.id = id;
}
public Integer id() {
return id;
}
public static Shape byId(Integer id) {
Shape result = reverse.get(id);
if (result == null) {
throw new NoSuchElementException("shape with id " + id + " does not exist");
}
return result;
}
}
Aunque algunos programadores lo desconocen o no lo tienen en cuenta, los enumerados admiten atributos y métodos (*). He aprovechado esta característica para hacer que cada elemento de Shape tenga un campo id con el número que debe usarse en la base de datos. Con el método estático byId se recupera el elemento correspondiente a cierto id. Esto nos vendrá de perlas en el siguiente conversor:
(*) Si quieres sacar partido a los enumerados, te recomiendo que revises el capítulo seis de «Effective Java».
package com.danielme.jakartaee.jpa.converters;
import com.danielme.jakartaee.jpa.entities.Geometry;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
@Converter(autoApply = true)
public class ShapeAttributeConverter implements AttributeConverter<Geometry.Shape, Integer> {
@Override
public Integer convertToDatabaseColumn(Geometry.Shape attribute) {
return attribute.id();
}
@Override
public Geometry.Shape convertToEntityAttribute(Integer dbData) {
return Geometry.Shape.byId(dbData);
}
}
El conversor nos ha permitido adaptarnos a las exigencias de la base de datos. Ya podemos cambiar el orden y los nombres de los elementos del enumerado Shape con total seguridad, de tal modo que la siguiente prueba continuará siendo exitosa.
@Test
@DataSet(value = "/datasets/geometries.yml")
void testShapeConverter() {
assertThat(em.find(Geometry.class, Datasets.SHAPE_CIRCLE_ID).getShape())
.isEqualTo(Geometry.Shape.CIRCLE);
assertThat(em.find(Geometry.class, Datasets.SHAPE_RECTANGLE_ID).getShape())
.isEqualTo(Geometry.Shape.RECTANGLE);
assertThat(em.find(Geometry.class, Datasets.SHAPE_TRIANGLE_ID).getShape())
.isEqualTo(Geometry.Shape.TRIANGLE);
}
Si queremos hacer más robusta la propia base de datos, resulta conveniente limitar los valores admitidos por la columna que mapea el enumerado. Podemos usar la restricción check que comenté líneas atrás o incluso convertir la columna en una clave ajena a una tabla -sin entidad en el modelo- que contenga los valores permitidos. El objetivo es mantener la integridad de los datos en que caso de que no se utilice JPA (cambios manuales o procesos externos a nuestro sistema).
Entidad final
Así queda la entidad Expense para los gastos, teniendo en cuenta que la tabla se creará a partir de ella. Poco a poco iremos construyendo el resto del modelo de entidades.
package com.danielme.jakartaee.jpa.entities;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDate;
@Entity
@Table(name = "expenses")
@Getter
@Setter
public class Expense {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, precision = 12, scale = 2)
private BigDecimal amount;
@Column(nullable = false, length = 50)
private String concept;
@Column(length = 512)
private String comments;
@Column(nullable = false)
private LocalDate date;
}
- El identificador lo generará MySQL por nosotros. Es una clave primaria de tipo identidad. Más información, en el próximo capítulo.
- La cantidad es un número obligatorio que tendrá hasta dos decimales. Hay alguna divisa que requiere tres o cuatro valores decimales, pero nos conformaremos con los casos más comunes. Para trabajar con una precisión determinada, recurrimos a BigDecimal. Es más, las monedas no deberían modelarse con el tipo float por las peculiaridades de la aritmética de coma flotante.
- El concepto o título del gasto es un texto obligatorio.
- En comentarios, el usuario podrá detallar todo aquello que considere oportuno y así evitar títulos largos.
- Se guarda la fecha en la que se produjo el gasto.
Lombok
La librería Lombok genera código rutinario y mecánico como, por ejemplo, getters, setters y constructores.
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class MiClase {
private String attributo1;
private String attributo2;
En el fragmento de código anterior, estamos indicando a Lombok que cree los métodos getters y setters para todos los atributos de la clase. Esta generación se realiza de forma «invisible» al compilar. No ahorramos escribir y mantener ese código y la clase queda más limpia y fácil de leer.
Para usar Lombok en un proyecto Maven, añadimos la dependencia al pom tal y como explica la documentación. También configuraremos nuestro IDE.
- Eclipse: hay que descargar este .jar que contiene una aplicación autoejecutable. Si no se abre al hacer doble clic, se puede lanzar con el comando java -jar lombok.jar. En esta mini aplicación indicaremos la ubicación del fichero ejecutable de Eclipse.
- IntelliJ: es comaptible con Lombok desde la versión 2020.3. En versiones interiores hay que instalar el plugin desde el Marketplace (File->Settings->Plugins).
En las entidades, nunca usaremos Lombok para generar los métodos equals, hashCode y toString. Los dos primeros veremos cómo implementarlos cuando hablemos de las relaciones. En cuanto a toString, su código podría obtener asociaciones «perezosas», concepto que también examinaremos en detalle. En este último caso, podemos anotar con @ToString.Exclude los atributos que no deben usarse.
Código de ejemplo
El código de ejemplo del capítulo se encuentra en GitHub (todos los proyectos son independientes pero están en un único repositorio). Para más información sobre cómo utilizar GitHub, consultar este artículo.