Embeddable Value Objects

@Embeddable and @Embedded

Some groups of fields belong together but have no identity of their own: an address, a money amount with currency, a GPS location. Map them as an @Embeddable. Their columns live in the owner's table — there is no separate table and no id.

@Embeddable
public class Address {
    private String line1;
    private String city;
    private String state;
    @Column(length = 6)
    private String pincode;

    protected Address() { }
    public Address(String line1, String city, String state, String pincode) { ... }
    // getters
}

@Entity
@Table(name = "users")
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    private String fullName;

    @Embedded
    private Address address;
}
create table users (
    id bigint not null auto_increment,
    email varchar(255) not null,
    full_name varchar(255),
    line1 varchar(255),
    city varchar(255),
    state varchar(255),
    pincode varchar(6),
    primary key (id),
    constraint uk_users_email unique (email)
)

The Same Embeddable Twice: @AttributeOverrides

An Order has a shipping address and a billing address. Both would want the column city, so rename the columns of one (or both):

@Entity
@Table(name = "orders")
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "line1",   column = @Column(name = "ship_line1")),
        @AttributeOverride(name = "city",    column = @Column(name = "ship_city")),
        @AttributeOverride(name = "state",   column = @Column(name = "ship_state")),
        @AttributeOverride(name = "pincode", column = @Column(name = "ship_pincode"))
    })
    private Address shippingAddress;

    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "line1",   column = @Column(name = "bill_line1")),
        @AttributeOverride(name = "city",    column = @Column(name = "bill_city")),
        @AttributeOverride(name = "state",   column = @Column(name = "bill_state")),
        @AttributeOverride(name = "pincode", column = @Column(name = "bill_pincode"))
    })
    private Address billingAddress;
}

Tip: Since Hibernate 6.2 an embeddable can be a Java record: @Embeddable public record Address(String line1, String city, String state, String pincode) {}. Records are immutable, which suits value objects well.

Flattening, Simulated

Example

This is how an embedded value becomes prefixed columns in the owner row:

import java.lang.reflect.RecordComponent;
import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        Address ship = new Address("12 MG Road", "Bengaluru", "Karnataka", "560001");
        Address bill = new Address("4 Park Street", "Kolkata", "West Bengal", "700016");

        Map<String, Object> row = new LinkedHashMap<>();
        row.put("id", 1001L);
        flatten("ship_", ship, row);
        flatten("bill_", bill, row);
        row.forEach((col, val) -> System.out.printf("%-13s = %s%n", col, val));
    }

    static void flatten(String prefix, Record value, Map<String, Object> row) throws Exception {
        for (RecordComponent rc : value.getClass().getRecordComponents()) {
            row.put(prefix + rc.getName(), rc.getAccessor().invoke(value));
        }
    }
}

record Address(String line1, String city, String state, String pincode) {}

Output

id            = 1001
ship_line1    = 12 MG Road
ship_city     = Bengaluru
ship_state    = Karnataka
ship_pincode  = 560001
bill_line1    = 4 Park Street
bill_city     = Kolkata
bill_state    = West Bengal
bill_pincode  = 700016