
Introducción
Los tipos de datos embebidos, también conocidos como tipos "Embeddable", son clases en Java que no se mapean directamente a una tabla en la base de datos, sino que se incluyen en otras entidades como si fueran parte de ellas. Esto permite modelar de manera más natural algunas relaciones entre los datos, agrupando en una clase atributos que lógicamente pertenecen juntos.
Los tipos embebidos se utilizan para representar un conjunto de atributos que comúnmente van juntos y son reutilizados por varias entidades. Por ejemplo, una dirección postal que incluye calle, número, código postal, etc., puede ser parte de una entidad Persona y también de una entidad Empresa. En lugar de repetir estos campos en cada entidad, se crea una clase Dirección marcada como @Embeddable, y luego se incluye en las entidades donde se necesite con la etiqueta @Embedded.
Definición de un tipo embebido
Primero, puede definirse una clase Dirección como un tipo embebido:
package com.ejemplo.hibernate;
import jakarta.persistence.Embeddable;
@Embeddable
public class Direccion {
private String calle;
private String numero;
private String codigoPostal;
private String ciudad;
// Constructores, getters y setters
}
Luego, podrá utilizarse la clase Dirección en la entidad Persona:
package com.ejemplo.hibernate;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Embedded;
@Entity
public class Persona {
@Id
private Long id;
private String nombre;
@Embedded
private Direccion direccion; // Aquí se utiliza el tipo embebido
// Constructores, getters y setters
}
Configuración avanzada
Hibernate también permite personalizar cómo se mapean los atributos de un tipo embebido a columnas en la base de datos. Por ejemplo, si queremos prefijar los nombres de las columnas relacionadas con la dirección, puede hacerse de la siguiente manera:
@Embedded
@AttributeOverrides({
@AttributeOverride(name="calle", column=@Column(name="dir_calle")),
@AttributeOverride(name="numero", column=@Column(name="dir_numero")),
@AttributeOverride(name="codigoPostal", column=@Column(name="dir_cp")),
@AttributeOverride(name="ciudad", column=@Column(name="dir_ciudad"))
})
private Direccion direccion;
Esto resulta en que los campos de Direccion se mapeen a columnas dir_calle, dir_numero, dir_cp, dir_ciudad, respectivamente, en la tabla correspondiente a la entidad Persona.
Reutilización del mismo tipo en varias entidades
Una de las ventajas clave de los tipos embebidos es poder reutilizar el mismo componente en entidades distintas sin duplicar código ni columnas físicas. Si una entidad requiere dos direcciones (facturación y envio), se aplican AttributeOverrides distintos en cada una:
@Entity
public class Pedido {
@Id
@GeneratedValue
private Long id;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "calle", column = @Column(name = "fact_calle")),
@AttributeOverride(name = "numero", column = @Column(name = "fact_numero")),
@AttributeOverride(name = "codigoPostal", column = @Column(name = "fact_cp")),
@AttributeOverride(name = "ciudad", column = @Column(name = "fact_ciudad"))
})
private Direccion direccionFacturacion;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "calle", column = @Column(name = "envio_calle")),
@AttributeOverride(name = "numero", column = @Column(name = "envio_numero")),
@AttributeOverride(name = "codigoPostal", column = @Column(name = "envio_cp")),
@AttributeOverride(name = "ciudad", column = @Column(name = "envio_ciudad"))
})
private Direccion direccionEnvio;
}
Hibernate generara una sola tabla pedido con ocho columnas, evitando la sobrecarga de una tabla direccion adicional y el coste de los joins que traeria.
Embeddable con lógica propia
Un @Embeddable puede incluir métodos de negocio y validaciones, convirtiendo el tipo en un verdadero value object del dominio. Esto encaja bien con patrones de Domain-Driven Design habituales en banca, fintech e insuretech.
@Embeddable
public class Dinero {
@Column(precision = 19, scale = 4)
private BigDecimal importe;
@Column(length = 3)
private String moneda;
protected Dinero() {}
public Dinero(BigDecimal importe, String moneda) {
if (importe == null || importe.signum() < 0) {
throw new IllegalArgumentException("Importe invalido");
}
this.importe = importe;
this.moneda = moneda;
}
public Dinero sumar(Dinero otro) {
if (!moneda.equals(otro.moneda)) {
throw new IllegalStateException("Monedas distintas");
}
return new Dinero(importe.add(otro.importe), moneda);
}
}
@Entity
public class Factura {
@Id @GeneratedValue private Long id;
@Embedded
private Dinero subtotal;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "importe", column = @Column(name = "iva_importe")),
@AttributeOverride(name = "moneda", column = @Column(name = "iva_moneda"))
})
private Dinero iva;
}
Diferencias con @ElementCollection y asociaciones
El criterio habitual es: si el valor no tiene identidad propia y no se comparte entre agregados, es un @Embeddable. Si tiene identidad y ciclo de vida propios, es una entidad asociada.
Casos de uso B2B
- Banca y fintech: tipos
Dinero,Iban,TarjetaPagoEnmascaradacomo@Embeddablepara encapsular reglas de formato, validación y operaciones aritmeticas sin contaminar las entidades principales. - Logistica y ecommerce:
Direccion,RangoFechas,Coordenadasembebidos en entidadesEnvio,Pedido,Almacenpara evitar joins innecesarios. - Sector público:
Nif,Codigo,Periodocomo value objects reutilizables en expedientes y resoluciones, con validaciones centralizadas en el embeddable.
Reutilización del mismo @Embeddable varias veces
Cuando una entidad tiene varias direcciones, @AttributeOverrides evita colisiones:
@Embeddable
public class Direccion {
@Column(length = 100)
private String calle;
@Column(length = 50)
private String ciudad;
@Column(name = "codigo_postal", length = 10)
private String codigoPostal;
@Column(length = 50)
private String pais;
}
@Entity
public class Empresa {
@Id @GeneratedValue
private Long id;
@Embedded
private Direccion direccionFiscal;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "calle", column = @Column(name = "envio_calle")),
@AttributeOverride(name = "ciudad", column = @Column(name = "envio_ciudad")),
@AttributeOverride(name = "codigoPostal", column = @Column(name = "envio_codigo_postal")),
@AttributeOverride(name = "pais", column = @Column(name = "envio_pais"))
})
private Direccion direccionEnvio;
}
Resultado en BD: la tabla empresa tiene columnas calle, ciudad, codigo_postal, pais para la fiscal y envio_calle, envio_ciudad, ... para la de envío.
Embeddable con asociaciones
Un @Embeddable puede contener asociaciones, pero con cuidado:
@Embeddable
public class Auditoria {
private LocalDateTime creadoEn;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "creado_por")
private Usuario creadoPor;
private LocalDateTime modificadoEn;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "modificado_por")
private Usuario modificadoPor;
}
@Entity
public class Pedido {
@Id @GeneratedValue
private Long id;
@Embedded
private Auditoria auditoria;
private BigDecimal total;
}
Auditoria se reutiliza en cualquier entidad que necesite trazabilidad. Las dos FK (creado_por, modificado_por) viven en la tabla del padre.
Embeddable como clave compuesta (@EmbeddedId)
Cuando la PK es compuesta:
@Embeddable
public class MatriculaId implements Serializable {
@Column(name = "estudiante_id")
private Long estudianteId;
@Column(name = "curso_id")
private Long cursoId;
// equals y hashCode obligatorios
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof MatriculaId other)) return false;
return Objects.equals(estudianteId, other.estudianteId)
&& Objects.equals(cursoId, other.cursoId);
}
@Override
public int hashCode() {
return Objects.hash(estudianteId, cursoId);
}
}
@Entity
public class Matricula {
@EmbeddedId
private MatriculaId id;
@ManyToOne
@MapsId("estudianteId")
private Estudiante estudiante;
@ManyToOne
@MapsId("cursoId")
private Curso curso;
private LocalDate fechaMatricula;
}
@MapsId indica que el campo de la asociación contribuye a la PK.
Embeddables vs entidades: ¿cuándo cada uno?
| Característica | Embeddable | Entidad | |----------------|-----------|---------| | Tabla propia | No | Sí | | Identidad propia (id) | No | Sí | | Ciclo de vida | Junto al padre | Independiente | | Reutilizable en varias entidades | Sí | Sí (vía asociación) | | Consultable directamente | No | Sí | | Auditable con Envers | A través del padre | Independiente |
Regla general: usa @Embeddable para value objects (concepto Domain-Driven Design): tipos sin identidad, identificados por sus atributos (Direccion, Money, IBAN, RangoFechas).
Inmutabilidad y records
Java 21 permite usar record como @Embeddable con cuidado:
@Embeddable
public record Money(
@Column(name = "amount", precision = 10, scale = 2) BigDecimal amount,
@Column(name = "currency", length = 3) String currency
) {
public Money {
Objects.requireNonNull(amount);
Objects.requireNonNull(currency);
if (currency.length() != 3) {
throw new IllegalArgumentException("Currency code debe tener 3 letras (ISO 4217)");
}
}
}
Los records son inmutables: cualquier "modificación" devuelve una nueva instancia. Encajan perfectamente con el patrón Value Object.
Buenas prácticas
@Embeddablepara value objects sin identidad propia.equalsyhashCodesiempre, especialmente si se usa como@EmbeddedIdo como elemento de colección.@AttributeOverridescuando se reutiliza el mismo tipo varias veces en la misma entidad.- Inmutabilidad si es posible: con records o
finalen los campos. - Cuidado con asociaciones LAZY dentro de Embeddable: pueden requerir bytecode enhancement.
- No abusar: si el value object adquiere identidad o ciclo de vida propio, promociónalo a entidad.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en Hibernate
Documentación oficial de Hibernate
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Hibernate es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Hibernate
Explora más contenido relacionado con Hibernate y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Modelar atributos compuestos reutilizables con @Embeddable y @Embedded para agrupar campos relacionados sin crear tablas adicionales.
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje