JPA @Embeddable and @Embedded with Spring Boot
Published Updated Spring Boot 13 min read
Flattening a value object into its owner's table, the AttributeOverrides you need the second time you embed the same type, and the null-component rule that decides whether the whole embeddable disappears.
An @Embeddable is a group of columns that belongs to another entity’s table. There is no second
table, no join and no identifier, which makes it the right tool for a value object such as an
address or a money amount, and the wrong tool the moment something else needs to reference it.
Written against Spring Boot 3.2, Hibernate 6.4 and Java 17.
The type and its owner
@Embeddable
public class Address {
@Column(name = "street")
private String street;
@Column(name = "city")
private String city;
@Column(name = "postal_code", length = 16)
private String postalCode;
protected Address() { } // required by JPA
public Address(String street, String city, String postalCode) {
this.street = street;
this.city = city;
this.postalCode = postalCode;
}
// getters only — no setters
}
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
@Embedded
private Address address;
}
One table:
CREATE TABLE users (
id BIGINT NOT NULL AUTO_INCREMENT,
email VARCHAR(255),
street VARCHAR(255),
city VARCHAR(255),
postal_code VARCHAR(16),
PRIMARY KEY (id)
);
@Embedded on the field is optional, Hibernate recognises a field whose type is @Embeddable, but
writing it makes the mapping obvious to a reader.
The no-argument constructor is required and protected is enough. Exposing no setters is a choice,
and the right one: an embeddable is a value, so replacing it wholesale (user.setAddress(new Address(...))) is clearer than mutating a component of a shared object.
Embedding the same type twice
The second Address collides. Both would map to street, city, postal_code. Hibernate reports
a repeated column and refuses to start.
@Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue private Long id;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street", column = @Column(name = "billing_street")),
@AttributeOverride(name = "city", column = @Column(name = "billing_city")),
@AttributeOverride(name = "postalCode", column = @Column(name = "billing_postal_code"))
})
private Address billingAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street", column = @Column(name = "shipping_street")),
@AttributeOverride(name = "city", column = @Column(name = "shipping_city")),
@AttributeOverride(name = "postalCode", column = @Column(name = "shipping_postal_code"))
})
private Address shippingAddress;
}
name is the property name inside the embeddable, not the column name it would otherwise take.
For a nested embeddable the path uses dots, name = "coordinates.latitude".
This is verbose, and it is the price of reusing the type. The alternative, two near-identical embeddable classes, is worse, because it duplicates the validation and the behaviour too.
Hibernate 6 offers a shorter form for the common case where the columns differ only by a prefix:
@Embedded
@AttributeOverride(name = "street", column = @Column(name = "billing_street"))
private Address billingAddress;
A single @AttributeOverride no longer needs the wrapping @AttributeOverrides because the
annotation is repeatable, so overriding two of five fields is two lines rather than five.
The null rule
Hibernate treats an embeddable whose every column is null as a null embeddable. Save a User with an
Address where all three fields are null, reload it, and user.getAddress() returns null rather
than an empty Address.
That is usually what you want and occasionally surprising, particularly in code that does
user.getAddress().getCity() after a round trip. Two defences:
public Optional<Address> getAddress() {
return Optional.ofNullable(address);
}
or make one component non-nullable in the schema, so the “all null” state cannot occur. The rule also
runs the other way on write: assigning null to the embedded field nulls every one of its columns
rather than leaving them untouched, which is worth knowing before a partial update reaches through
it.
Validation, and where the annotation goes
@Embeddable
public class Address {
@NotBlank
@Column(nullable = false)
private String city;
}
@Entity
public class User {
@Valid // without this, the embeddable's constraints are not checked
@Embedded
private Address address;
}
@Valid on the owning field is what makes Bean Validation descend into the embeddable. Without it
the constraints inside Address are inert: the same class of silent no-op as a validation
annotation on the wrong use-site target.
Queries and derived methods
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByAddressCity(String city);
List<User> findByAddress_PostalCode(String postalCode);
}
Spring Data traverses into the embeddable by property path. The explicit underscore is worth using
whenever a name could be parsed two ways: findByAddressCity is ambiguous if the entity also has a
field called addressCity.
In JPQL the path is the same:
@Query("SELECT u FROM User u WHERE u.address.city = :city AND u.address.postalCode LIKE :prefix%")
List<User> search(@Param("city") String city, @Param("prefix") String prefix);
Because the columns are on the owner’s table, none of these queries join. That is the performance argument for an embeddable over a one-to-one: filtering on the embedded fields is free.
equals and hashCode
Unlike an entity, an embeddable should implement value equality over all its fields:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Address other)) return false;
return Objects.equals(street, other.street)
&& Objects.equals(city, other.city)
&& Objects.equals(postalCode, other.postalCode);
}
@Override
public int hashCode() {
return Objects.hash(street, city, postalCode);
}
An embeddable has no identifier, so the values are the identity: the opposite of the
entity rule, where hashing over mutable
state breaks a HashSet. It also matters concretely for @ElementCollection, where a Set of
embeddables needs it to behave.
A collection of embeddables
@ElementCollection
@CollectionTable(name = "user_addresses", joinColumns = @JoinColumn(name = "user_id"))
private Set<Address> previousAddresses = new HashSet<>();
That gets its own table, but the rows are still values: they have no identifier, they cannot be
queried independently, and they are deleted and re-inserted as a block when the collection changes.
The @ElementCollection walkthrough covers
the write behaviour in more detail.
An embeddable as the primary key
The same annotation does double duty: an @Embeddable used with @EmbeddedId becomes a composite
primary key.
@Embeddable
public class OrderLineId implements Serializable {
private Long orderId;
private int lineNumber;
// no-arg constructor, equals, hashCode
}
@Entity
public class OrderLine {
@EmbeddedId
private OrderLineId id;
}
The requirements tighten in that role: the class must implement Serializable, and equals and
hashCode stop being good practice and become mandatory, without them findById returns empty for
rows that exist. The composite primary key
example covers that failure and the
@MapsId form that keeps a foreign key from being stored twice.
Worth being deliberate about which role a given embeddable plays. A key must be immutable because changing it changes which row the entity is; an ordinary embedded value is immutable by convention only, because replacing it wholesale is easier to reason about than mutating it in place.
When it should be an entity instead
Use an @Embeddable when the value has no identity of its own, is never shared, and is only ever
reached through its owner. Switch to an entity, usually a
one-to-one, as soon as another table needs
a foreign key to it. It needs to be queried on its own, or it is large enough that loading it with
every owner row is wasteful.
Frequently asked questions
Does an @Embeddable get its own table?
No. Its columns live in the owning entity’s table. A
collection of them mapped with @ElementCollection does get a table, but the rows are still values
with no identifier.
Do I need @Embedded on the field?
Not strictly, Hibernate recognises a field whose type is
annotated @Embeddable. Writing it makes the mapping explicit for anyone reading the entity.
Why does Hibernate complain about a repeated column?
The same embeddable type is used twice in
one entity, so both copies claim the same column names. Add @AttributeOverrides to at least one.
What does the name attribute of @AttributeOverride refer to?
The property name inside the embeddable, not a column name. Nested embeddables use a dotted path.
Why is my embedded object null after reloading?
Every column was null, and Hibernate maps an
all-null embeddable to null. Return an Optional from the getter, or make one component
non-nullable so the state cannot arise.
Why are the validation constraints inside my embeddable ignored?
The owning field needs @Valid.
Without it Bean Validation does not descend into the embedded type.
Should an embeddable implement equals and hashCode?
Yes, over all its fields. It is a value, so the fields are the identity. That is the opposite of the rule for entities, which should not hash over mutable state.
How do I query on an embedded field?
By property path: findByAddressCity(...) in a derived
query, u.address.city in JPQL. No join is generated, because the columns are on the owner’s table.
Can an embeddable contain an association?
Yes. A @ManyToOne inside an @Embeddable is legal
and maps a foreign key column into the owner’s table. Collections inside an embeddable are best
avoided; the mapping works and the write behaviour is hard to reason about.
Embeddable or one-to-one?
Embeddable while the value is owned exclusively and always loaded with its owner. One-to-one once it needs its own identity, its own queries, or references from elsewhere.