Для чего используются аннотации @Embeddable и @Embedded?
@Embeddable помечает класс-value-object без собственного первичного ключа; @Embedded встраивает его поля в таблицу владельца. Lifecycle встроенного объекта полностью зависит от родительской сущности.
@Embeddable и @Embedded в Hibernate
@Embeddable — аннотация на классе, которая говорит Hibernate: этот класс не является самостоятельной сущностью и не имеет собственного первичного ключа. @Embedded — аннотация на поле владельца, указывающая встроить колонки @Embeddable-класса напрямую в таблицу владельца.
Это реализация паттерна Value Object из Domain-Driven Design: объект определяется своими атрибутами, а не идентичностью.
Базовый пример
@Embeddable
public class Address {
@Column(name = "street", nullable = false)
private String street;
@Column(name = "city", nullable = false)
private String city;
@Column(name = "zip_code", length = 10)
private String zipCode;
// Hibernate требует no-arg конструктор
protected Address() {}
public Address(String street, String city, String zipCode) {
this.street = street;
this.city = city;
this.zipCode = zipCode;
}
}
@Entity
@Table(name = "customers")
public class Customer {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Embedded
private Address address; // колонки street, city, zip_code попадут в таблицу customers
}
SQL-результат: таблица customers получит колонки id, name, street, city, zip_code — без отдельной таблицы addresses.
@AttributeOverride при повторном использовании
Если одна сущность дважды использует один @Embeddable-тип (например, адрес доставки и адрес выставления счёта), имена колонок конфликтуют — нужен @AttributeOverride.
@Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street", column = @Column(name = "ship_street")),
@AttributeOverride(name = "city", column = @Column(name = "ship_city")),
@AttributeOverride(name = "zipCode", column = @Column(name = "ship_zip"))
})
private Address shippingAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street", column = @Column(name = "bill_street")),
@AttributeOverride(name = "city", column = @Column(name = "bill_city")),
@AttributeOverride(name = "zipCode", column = @Column(name = "bill_zip"))
})
private Address billingAddress;
}
Lifecycle и dirty checking
Встроенный объект не имеет собственного lifecycle — он сохраняется, обновляется и удаляется вместе с владельцем. Hibernate включает поля @Embeddable в dirty checking: при изменении customer.getAddress().setCity("Moscow") Hibernate сгенерирует UPDATE для таблицы customers.
Использование в @ElementCollection
@Entity
public class Employee {
@Id
private Long id;
// Список телефонов — коллекция value objects
@ElementCollection
@CollectionTable(name = "employee_phones",
joinColumns = @JoinColumn(name = "employee_id"))
private List<Phone> phones;
}
@Embeddable
public class Phone {
private String type; // MOBILE, HOME, WORK
private String number;
}
Подводные камни
- Если поле типа
@Embeddableравноnull, Hibernate по умолчанию хранит все его колонки как NULL — при загрузке вернётnull, а не пустой объект. Это ломает цепочки вызовов без null-проверок. - Повторное использование одного
@Embeddable-типа в сущности без@AttributeOverrideвызываетMappingException: Repeated column in mappingпри старте приложения. @Embeddable-класс обязан иметь no-arg конструктор (может бытьprotected), иначе Hibernate не сможет создать экземпляр при загрузке.- Изменяемый
@Embeddableнарушает семантику Value Object; если объект должен быть неизменяемым (как в DDD), поля следует делатьfinalи убирать сеттеры. - JPQL-запросы обращаются к полям через точечную нотацию:
WHERE c.address.city = :city— это работает, но индекс нужно создавать явно через@Indexна@Table. - При использовании
@ElementCollectionс@Embeddableкаждая операция добавления/удаления элемента перезаписывает всю коллекцию в БД (orphan removal на уровне коллекции). - Hibernate Envers корректно версионирует встроенные объекты, но при изменении структуры
@Embeddableстарые ревизии могут стать несовместимы со схемой.
Common mistakes
- Путать термин «embeddable embedded» с соседним механизмом Hibernate.
- Не называть границу lifecycle, transaction, thread или request для «embeddable embedded».
- Игнорировать production-эффекты «embeddable embedded»: latency, SQL shape, memory, security или observability.
What the interviewer is testing
- Попросить объяснить механизм «embeddable embedded» на минимальном примере.
- Проверить, видит ли кандидат failure mode и диагностику для «embeddable embedded».
- Уточнить, какие настройки или API меняют «embeddable embedded» в реальном сервисе.