Ir al contenido

Tutorial: Crear tu primera entidad JPA y verla en H2

Al terminar este tutorial vas a ser capaz de:

  • Agregar las dependencias de JPA y H2 a tu pom.xml, incluida la de la consola web de H2, que en Spring Boot 4 es un starter aparte.
  • Crear BaseEntity, la superclase que centraliza @Id/@GeneratedValue para todas las entidades de tu módulo.
  • Convertir un DTO en una entidad @Entity, con un atributo de colección de String mapeado con @ElementCollection.
  • Arrancar la aplicación y confirmar, en la consola de H2, que Hibernate creó exactamente las tablas y columnas que esperabas.
  • Insertar y consultar filas a mano desde la consola de H2, y descargar la descripción de tus tablas como un script SQL.

Prerrequisitos: haber terminado el Tutorial: DTOs, repositorio y controller de tu API REST, y haber leído la teoría de Persistencia con JPA: este tutorial es la aplicación práctica de esa teoría, no la repite.


2. Por qué este es el primer paso de Release 6

Sección titulada «2. Por qué este es el primer paso de Release 6»

Antes de tocar el repositorio, el service o el controller de Release 6, necesitas la pieza que todos ellos van a usar: la entidad JPA, la clase que Hibernate mapea a una tabla real.

Este tutorial se queda solo en esa pieza: una entidad con sus atributos básicos y una colección de String, y la confirmación visual de que la tabla quedó como esperabas. Los siguientes tutoriales de esta serie (repositorio, service, controller) construyen sobre esta entidad ya verificada.


En el pom.xml de tu proyecto (el mismo que generaste en Crear el proyecto backend con Spring Boot), agrega estas tres dependencias dentro de <dependencies>:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-h2console</artifactId>
</dependency>

En src/main/resources/application.properties, agrega:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=create-drop
spring.h2.console.enabled=true

Ya viste el significado de cada una en la teoría de H2, la base de datos del curso. La única novedad acá es create-drop en vez de update: mientras estás diseñando y probando tu entidad, conviene que la tabla se recree desde cero cada vez que arrancas la aplicación, para no arrastrar una tabla vieja que ya no coincide con tus atributos actuales.


Todas las entidades del curso necesitan un identificador manejado de la misma forma. En vez de repetir @Id/@GeneratedValue en cada una, esta tutorial sigue la convención de Entidades y tablas: una superclase con @MappedSuperclass que las demás heredan.

Hasta ahora todo tu módulo vive en co.edu.uniandes.musical.artistas, incluida ArtistasApplication. Desde este release hay clases que no son de un módulo sino de todo el grupo, y tienen que quedar en paquetes hermanos de artistas. Para que Spring las encuentre, la clase principal tiene que estar un nivel más arriba: Spring solo busca componentes desde el paquete de la clase @SpringBootApplication hacia abajo. Si se queda dentro de artistas, ni los paquetes compartidos ni los módulos de tus compañeros quedan dentro de esa búsqueda, y el backend arranca sin avisar nada: el controller de un compañero responde 404.

Mueve ArtistasApplication.java de la carpeta artistas/ a la carpeta musical/ (arrástrala en el explorador de VS Code) y cambia su primera línea:

package co.edu.uniandes.musical;

Si en el repositorio de tu grupo la clase ya está en el paquete base, no hagas nada.

Hay una segunda clase que ya creaste y que también es del grupo: CorsConfig, del tutorial del API REST. Con varios módulos en el mismo repositorio, si cada uno tiene la suya el backend no arranca (Spring avisa que el bean corsConfig está definido dos veces). Crea una carpeta config/ dentro de musical/, al lado de artistas/, mueve allí CorsConfig.java y cambia su primera línea:

package co.edu.uniandes.musical.config;

Si config/CorsConfig.java ya existe en el repositorio de tu grupo, borra la tuya en vez de moverla.

Crea una carpeta nueva common/ dentro de musical/, al lado de artistas/, y en ella el archivo BaseEntity.java:

package co.edu.uniandes.musical.common;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import lombok.Data;
@Data
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}

@MappedSuperclass le dice a JPA que BaseEntity no es una entidad con tabla propia: sus atributos se agregan a la tabla de cada subclase que la extiende. Toda entidad de tu módulo va a extender BaseEntity en vez de declarar su propio id.


Tu DTO (ArtistaDTO, o el de la entidad principal de tu módulo) describe la forma de los datos que viajan por HTTP. La entidad JPA es una clase distinta que describe cómo esos datos se guardan en una tabla. No son la misma clase: a partir de Release 6 vas a tener las dos, y el controller va a convertir entre una y otra (eso lo ves en un tutorial posterior; por ahora, solo la entidad).

Por convención del curso, la clase de la entidad lleva el mismo nombre del DTO sin el sufijo DTO, más el sufijo Entity: ArtistaDTO → ArtistaEntity.

Dentro de la misma carpeta (artistas/), crea ArtistaEntity.java:

package co.edu.uniandes.musical.artistas;
import co.edu.uniandes.musical.common.BaseEntity;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import java.time.LocalDate;
import java.util.Set;
import lombok.Data;
@Data
@Entity
public class ArtistaEntity extends BaseEntity {
private String nombre;
private String fotoArtistaUrl;
private String paisOrigen;
private LocalDate fechaNacimiento;
@Column(length = 1000)
private String biografia;
@ElementCollection
@CollectionTable(
name = "artista_generos",
joinColumns = @JoinColumn(name = "artista_id")
)
@Column(name = "genero")
private Set<String> generos;
}

Fíjate en las piezas nuevas frente al DTO:

  • extends BaseEntity. Le da a ArtistaEntity su llave primaria id autogenerada, sin declararla de nuevo. Como BaseEntity está en otro paquete, la entidad la importa (import co.edu.uniandes.musical.common.BaseEntity;). No hace falta @Table aquí: sin esa anotación, JPA deriva el nombre de la tabla del nombre de la clase (ArtistaEntity se convierte en ARTISTA_ENTITY); ver cuándo el nombre por defecto no sirve para los casos donde sí conviene fijarlo a mano.
  • @Column(length = 1000) en biografia. Sin ninguna anotación, JPA crea cada String como VARCHAR(255), y una biografía de varias frases se pasa de 255 caracteres: el INSERT fallaría con Value too long. Es el caso de cuando el nombre o el tipo por defecto no sirven: @Column fija la longitud de esa columna puntual. El resto de atributos se queda con el valor por defecto.
  • @ElementCollection en generos. Un artista puede tener varios géneros musicales ("Rock", "Pop"), y cada género es solo texto, sin identidad propia: exactamente el caso que pide @ElementCollection en vez de una relación entre entidades. @CollectionTable nombra la tabla aparte donde van a vivir esos valores, y joinColumns nombra la columna que la conecta de vuelta con ARTISTA_ENTITY.
  • LocalDate en fechaNacimiento. Una fecha sin hora se declara con java.time.LocalDate, no con String: JPA la guarda en una columna de tipo DATE sin ninguna anotación, y la base puede ordenar y comparar fechas de verdad. En el JSON sigue viajando como texto yyyy-MM-dd ("1977-02-02"), así que el front no cambia. Por qué no java.util.Date, en Fechas en las entidades.

Compila para verificar que no hay errores de sintaxis:

Ventana de terminal
./mvnw compile

Con BaseEntity y ArtistaEntity ya en el proyecto, arranca la aplicación:

Ventana de terminal
./mvnw spring-boot:run

Revisa el log de arranque: antes de la línea Started ... in ... seconds deberían aparecer las sentencias create table que Hibernate ejecutó, algo como:

Hibernate:
create table artista_entity (
id bigint generated by default as identity,
biografia varchar(1000),
fecha_nacimiento varchar(255),
foto_artista_url varchar(255),
nombre varchar(255),
pais_origen varchar(255),
primary key (id)
)
Hibernate:
create table artista_generos (
artista_id bigint not null,
genero varchar(255)
)

Con la aplicación corriendo, abre en el navegador:

http://localhost:8080/h2-console

El formulario de login ya viene con los valores correctos, tomados de tu application.properties: JDBC URL jdbc:h2:mem:testdb, User Name sa, Password vacío. Haz clic en Connect.

En el panel izquierdo deberías ver dos tablas nuevas: ARTISTA_ENTITY y ARTISTA_GENEROS. Haz clic en el + junto a cada una para expandirla y confirmar sus columnas (deberían coincidir exactamente con las que viste en el log de arranque), o escribe una consulta en el panel de la derecha y ejecútala con Run:

SELECT * FROM ARTISTA_ENTITY;
SELECT * FROM ARTISTA_GENEROS;

Las dos tablas están vacías todavía, no has guardado ningún artista, pero eso confirma exactamente lo que este paso necesitaba probar: que la entidad se mapea a la estructura de columnas y tablas que esperabas, antes de construir nada más encima.


Todavía no tienes un repositorio ni un controller que guarden datos por ti, pero puedes insertar filas directamente con SQL, en el mismo panel de la derecha, para confirmar que las dos tablas realmente se relacionan como esperabas.

Primero, un artista en ARTISTA_ENTITY. No incluyas ID en el INSERT: es autogenerado, así que la base de datos lo asigna sola.

INSERT INTO ARTISTA_ENTITY (NOMBRE, FOTO_ARTISTA_URL, PAIS_ORIGEN, FECHA_NACIMIENTO, BIOGRAFIA)
VALUES ('Shakira', 'https://example.com/shakira.jpg', 'Colombia', '1977-02-02', 'Cantante colombiana.');

Consulta la tabla para ver qué ID le tocó:

SELECT * FROM ARTISTA_ENTITY;

Con ese ID (si es tu primer insert, va a ser 1), agrega sus géneros en ARTISTA_GENEROS:

INSERT INTO ARTISTA_GENEROS (ARTISTA_ID, GENERO) VALUES (1, 'Pop'), (1, 'Latin');
SELECT * FROM ARTISTA_GENEROS;

Deberías ver dos filas, las dos con ARTISTA_ID = 1: la colección de String en Java se traduce en varias filas en la tabla aparte, una por cada valor del Set.


La consola de H2 no tiene un botón de “descargar”, pero el comando SCRIPT genera el CREATE TABLE de cada tabla, exactamente lo que Hibernate generó por ti, y TO lo guarda en un archivo. Ejecuta esto en el panel de la derecha:

SCRIPT NODATA NOPASSWORDS TO 'artista_schema.sql';

NODATA deja fuera los INSERT de las filas que ya tienes (te interesa la estructura, no los datos de prueba) y NOPASSWORDS deja fuera la línea CREATE USER con el hash de la contraseña. El archivo aparece en la raíz de tu proyecto Java (la misma carpeta donde corriste ./mvnw spring-boot:run), no en una descarga del navegador: la consola de H2 corre dentro de tu propia aplicación, así que “guardar” un archivo significa escribirlo en el disco donde esa aplicación está corriendo.

-- artista_schema.sql
CREATE MEMORY TABLE "PUBLIC"."ARTISTA_GENEROS"(
"ARTISTA_ID" BIGINT NOT NULL,
"GENERO" CHARACTER VARYING(255)
);
CREATE MEMORY TABLE "PUBLIC"."ARTISTA_ENTITY"(
"ID" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
"BIOGRAFIA" CHARACTER VARYING(1000),
"FECHA_NACIMIENTO" DATE,
"FOTO_ARTISTA_URL" CHARACTER VARYING(255),
"NOMBRE" CHARACTER VARYING(255),
"PAIS_ORIGEN" CHARACTER VARYING(255)
);
ALTER TABLE "PUBLIC"."ARTISTA_ENTITY" ADD CONSTRAINT "PUBLIC"."CONSTRAINT_1" PRIMARY KEY("ID");
ALTER TABLE "PUBLIC"."ARTISTA_GENEROS" ADD CONSTRAINT "PUBLIC"."FK..." FOREIGN KEY("ARTISTA_ID") REFERENCES "PUBLIC"."ARTISTA_ENTITY"("ID");

Al final de este tutorial tienes una entidad @Entity real, con su llave primaria heredada de BaseEntity y una colección de String en su propia tabla, y la confirmaste mirando directamente la base de datos en la consola de H2, insertando filas a mano y descargando la estructura real, en vez de solo asumir que el código compiló y por lo tanto funciona.

Lo que viene después, siguiendo el orden de Release 6:

  • Repetir este mismo patrón con las demás entidades de tu módulo, todas extendiendo BaseEntity.
  • Agregar las relaciones intra-módulo e inter-módulo (@OneToMany/ @ManyToOne) una vez que el equipo apruebe el diagrama de clases consolidado en la wiki.
  • Convertir la interfaz ArtistaRepository en JpaRepository<ArtistaEntity, Long>.