Bases de datos
JPA (Java Persistence API)
Especificación estándar para mapeo objeto-relacional (ORM) en Java.
Hibernate es la implementación más popular de JPA.
Dependencias
Maven:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
<!-- Base de datos --><dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId></dependency>Gradle:
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'implementation 'org.postgresql:postgresql'Configuración
application.properties:
spring.datasource.url=jdbc:postgresql://localhost:5432/midbspring.datasource.username=postgresspring.datasource.password=secret
# JPAspring.jpa.hibernate.ddl-auto=updatespring.jpa.show-sql=truespring.jpa.properties.hibernate.format_sql=truespring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialectddl-auto options:
none: No hace nada (producción)validate: Valida que el esquema coincidaupdate: Actualiza el esquema sin borrar datos (desarrollo)create: Crea el esquema, borra datos existentescreate-drop: Crea al iniciar, elimina al cerrar (tests)
Entidades
@Entity
@Entity@Table(name = "usuarios")public class Usuario {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Column(nullable = false, length = 50) private String nombre;
@Column(unique = true, nullable = false) private String email;
private Integer edad;
@Column(name = "fecha_creacion") private LocalDateTime fechaCreacion;
// Constructores, getters, setters}Con Lombok
@Entity@Table(name = "usuarios")@Data@NoArgsConstructor@AllArgsConstructorpublic class Usuario {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String nombre; private String email; private Integer edad; private LocalDateTime fechaCreacion;}Tipos de generación de ID
IDENTITY
Autoincremento de la base de datos.
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;SEQUENCE
Usa secuencias de la base de datos.
@Id@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "usuario_seq")@SequenceGenerator(name = "usuario_seq", sequenceName = "usuario_seq", allocationSize = 1)private Long id;UUID
@Id@GeneratedValue(strategy = GenerationType.UUID)private UUID id;Relaciones
@OneToMany
Un usuario tiene muchos posts.
@Entitypublic class Usuario { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String nombre;
@OneToMany(mappedBy = "usuario", cascade = CascadeType.ALL) private List<Post> posts = new ArrayList<>();}
@Entitypublic class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String titulo;
@ManyToOne @JoinColumn(name = "usuario_id") private Usuario usuario;}@ManyToOne
Muchos posts pertenecen a un usuario.
@Entitypublic class Post { @ManyToOne @JoinColumn(name = "usuario_id", nullable = false) private Usuario usuario;}@ManyToMany
Muchos usuarios, muchos roles.
@Entitypublic class Usuario { @ManyToMany @JoinTable( name = "usuario_rol", joinColumns = @JoinColumn(name = "usuario_id"), inverseJoinColumns = @JoinColumn(name = "rol_id") ) private Set<Rol> roles = new HashSet<>();}
@Entitypublic class Rol { @ManyToMany(mappedBy = "roles") private Set<Usuario> usuarios = new HashSet<>();}@OneToOne
Un usuario, un perfil.
@Entitypublic class Usuario { @OneToOne(mappedBy = "usuario", cascade = CascadeType.ALL) private Perfil perfil;}
@Entitypublic class Perfil { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@OneToOne @JoinColumn(name = "usuario_id") private Usuario usuario;}Spring Data JPA
Repositorio
public interface UsuarioRepositorio extends JpaRepository<Usuario, Long> { // Métodos CRUD incluidos automáticamente}Métodos heredados:
save(Usuario): Guardar/actualizarfindById(Long): Buscar por IDfindAll(): Listar todosdelete(Usuario): Eliminarcount(): Contar registrosexistsById(Long): Verificar existencia
Query Methods
Spring Data genera consultas automáticamente.
public interface UsuarioRepositorio extends JpaRepository<Usuario, Long> {
// WHERE email = ? Usuario findByEmail(String email);
// WHERE nombre = ? AND edad = ? List<Usuario> findByNombreAndEdad(String nombre, Integer edad);
// WHERE edad > ? List<Usuario> findByEdadGreaterThan(Integer edad);
// WHERE nombre LIKE %?% List<Usuario> findByNombreContaining(String nombre);
// WHERE email LIKE ?% List<Usuario> findByEmailStartingWith(String prefix);
// ORDER BY nombre ASC List<Usuario> findAllByOrderByNombreAsc();
// LIMIT 10 List<Usuario> findTop10ByOrderByFechaCreacionDesc();}@Query
Consultas personalizadas.
JPQL
public interface UsuarioRepositorio extends JpaRepository<Usuario, Long> {
@Query("SELECT u FROM Usuario u WHERE u.email = :email") Usuario buscarPorEmail(@Param("email") String email);
@Query("SELECT u FROM Usuario u WHERE u.edad > :edad ORDER BY u.nombre") List<Usuario> buscarMayoresQue(@Param("edad") Integer edad);
@Query("SELECT u FROM Usuario u JOIN u.roles r WHERE r.nombre = :rol") List<Usuario> buscarPorRol(@Param("rol") String rol);}SQL nativo
@Query(value = "SELECT * FROM usuarios WHERE email = :email", nativeQuery = true)Usuario buscarPorEmailNativo(@Param("email") String email);Modificación de datos
@Modifying
@Modifying@Transactional@Query("UPDATE Usuario u SET u.nombre = :nombre WHERE u.id = :id")int actualizarNombre(@Param("id") Long id, @Param("nombre") String nombre);
@Modifying@Transactional@Query("DELETE FROM Usuario u WHERE u.edad < :edad")void eliminarMenoresQue(@Param("edad") Integer edad);Transacciones
@Transactional
@Servicepublic class UsuarioServicio {
@Autowired private UsuarioRepositorio repositorio;
@Transactional public void transferir(Long deId, Long aId, BigDecimal monto) { Usuario de = repositorio.findById(deId).orElseThrow(); Usuario a = repositorio.findById(aId).orElseThrow();
de.setSaldo(de.getSaldo().subtract(monto)); a.setSaldo(a.getSaldo().add(monto));
repositorio.save(de); repositorio.save(a);
// Si algo falla, se hace rollback }}Paginación
@Servicepublic class UsuarioServicio {
@Autowired private UsuarioRepositorio repositorio;
public Page<Usuario> listar(int page, int size) { Pageable pageable = PageRequest.of(page, size, Sort.by("nombre")); return repositorio.findAll(pageable); }}En el repositorio:
public interface UsuarioRepositorio extends JpaRepository<Usuario, Long> { Page<Usuario> findByEdadGreaterThan(Integer edad, Pageable pageable);}Auditoría
Guardar quién y cuándo modificó un registro.
Configuración
@Configuration@EnableJpaAuditingpublic class JpaConfig {}Entidad
@Entity@EntityListeners(AuditingEntityListener.class)public class Usuario {
@CreatedDate private LocalDateTime fechaCreacion;
@LastModifiedDate private LocalDateTime fechaModificacion;
@CreatedBy private String creadoPor;
@LastModifiedBy private String modificadoPor;}Proyecciones
Obtener solo algunos campos.
Interface-based
public interface UsuarioResumen { String getNombre(); String getEmail();}
public interface UsuarioRepositorio extends JpaRepository<Usuario, Long> { List<UsuarioResumen> findAllBy();}DTO
public record UsuarioDTO(String nombre, String email) {}
@Query("SELECT new com.ejemplo.dto.UsuarioDTO(u.nombre, u.email) FROM Usuario u")List<UsuarioDTO> obtenerResumen();Migrations con Flyway
Versionado de base de datos.
Dependencia
<dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId></dependency>Configuración
spring.jpa.hibernate.ddl-auto=validatespring.flyway.enabled=trueMigrations
src/main/resources/db/migration/V1__crear_tabla_usuarios.sql:
CREATE TABLE usuarios ( id BIGSERIAL PRIMARY KEY, nombre VARCHAR(50) NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, edad INT, fecha_creacion TIMESTAMP);V2__agregar_telefono.sql:
ALTER TABLE usuarios ADD COLUMN telefono VARCHAR(20);N+1 Problem
❌ Problema
List<Usuario> usuarios = repositorio.findAll();for (Usuario u : usuarios) { System.out.println(u.getPosts().size()); // 1 query por usuario}// 1 query para usuarios + N queries para posts = N+1 queries✅ Solución: @EntityGraph
@EntityGraph(attributePaths = {"posts"})List<Usuario> findAll();// 1 sola query con JOIN✅ Solución: JOIN FETCH
@Query("SELECT u FROM Usuario u LEFT JOIN FETCH u.posts")List<Usuario> findAllWithPosts();Buenas prácticas
ddl-auto=noneen producción: Usa Flyway/Liquibase.- Lazy loading por defecto: Evita cargar relaciones innecesarias.
- DTOs para APIs: No expongas entidades directamente.
- Transacciones en servicios: No en repositorios.
- Índices: Crea índices en columnas de búsqueda frecuente.
- Connection pool: HikariCP (incluido en Spring Boot).
Próximo paso
Aprende patrones de arquitectura backend: Arquitectura →