Skip to content
CalliCoder

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.