Для чего нужно помечать сущности Embedded в Spring?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Для чего нужно помечать сущности @Embedded в Spring
@Embedded — это аннотация JPA/Hibernate, которая используется для встраивания (embedding) одного объекта в другой без создания отдельной таблицы в базе данных. Она критически важна для проектирования правильной архитектуры приложений на Spring Boot.
Основное назначение @Embedded
Эта аннотация решает проблему: как хранить сложный объект в одной строке таблицы, не создавая отдельную таблицу и внешний ключ?
Пример без @Embedded (плохо)
@Entity
@Table(name = "users")
public class User {
@Id
private Long id;
private String firstName;
private String lastName;
private String street;
private String city;
private String postalCode;
private String country;
// 12 полей для одного адреса - нарушение DRY
private String street2;
private String city2;
private String postalCode2;
private String country2;
}
Таблица в БД:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
first_name VARCHAR,
last_name VARCHAR,
street VARCHAR,
city VARCHAR,
postal_code VARCHAR,
country VARCHAR,
street_2 VARCHAR,
city_2 VARCHAR,
postal_code_2 VARCHAR,
country_2 VARCHAR
);
Проблемы:
- Дублирование полей адреса
- Сложность кода
- Нарушение SRP (Single Responsibility Principle)
- Сложность тестирования
Решение: @Embedded
1. Сначала создаем Value Object
@Embeddable
public class Address {
@Column(name = "street")
private String street;
@Column(name = "city")
private String city;
@Column(name = "postal_code")
private String postalCode;
@Column(name = "country")
private String country;
// конструктор, getters, equals, hashCode
public Address(String street, String city, String postalCode, String country) {
this.street = street;
this.city = city;
this.postalCode = postalCode;
this.country = country;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Address)) return false;
Address address = (Address) o;
return Objects.equals(street, address.street) &&
Objects.equals(city, address.city) &&
Objects.equals(postalCode, address.postalCode) &&
Objects.equals(country, address.country);
}
@Override
public int hashCode() {
return Objects.hash(street, city, postalCode, country);
}
}
2. Встраиваем в сущность
@Entity
@Table(name = "users")
public class User {
@Id
private Long id;
private String firstName;
private String lastName;
@Embedded
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street", column = @Column(name = "work_street")),
@AttributeOverride(name = "city", column = @Column(name = "work_city")),
@AttributeOverride(name = "postalCode", column = @Column(name = "work_postal_code")),
@AttributeOverride(name = "country", column = @Column(name = "work_country"))
})
private Address workAddress;
// Можем иметь несколько embedded объектов одного типа
}
Таблица в БД:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
first_name VARCHAR,
last_name VARCHAR,
street VARCHAR,
city VARCHAR,
postal_code VARCHAR,
country VARCHAR,
work_street VARCHAR,
work_city VARCHAR,
work_postal_code VARCHAR,
work_country VARCHAR
);
Основные преимущества @Embedded
1. Переиспользование кода (DRY)
// Один класс Address используется везде
@Embeddable
public class Address {
// логика валидации в одном месте
public boolean isValid() {
return street != null && !street.isEmpty() &&
city != null && !city.isEmpty() &&
postalCode != null && !postalCode.isEmpty();
}
}
// Используем везде
public class User {
@Embedded
private Address address;
}
public class Company {
@Embedded
private Address headquarters;
}
public class Store {
@Embedded
private Address location;
}
2. Объектно-ориентированный дизайн
// Value Object с поведением
@Embeddable
public class Money {
private BigDecimal amount;
@Column(name = "currency")
private String currency;
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currencies must be the same");
}
return new Money(
this.amount.add(other.amount),
this.currency
);
}
public boolean isGreaterThan(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currencies must be the same");
}
return this.amount.compareTo(other.amount) > 0;
}
}
@Entity
public class Order {
@Embedded
private Money totalPrice;
@Embedded
private Money shippingCost;
public Money getFinalPrice() {
return totalPrice.add(shippingCost);
}
}
3. Валидация данных
@Embeddable
public class Email {
@Column(name = "email")
@Email // Jakarta Bean Validation
private String value;
public Email(String value) {
if (!isValidEmail(value)) {
throw new IllegalArgumentException("Invalid email: " + value);
}
this.value = value;
}
private boolean isValidEmail(String email) {
return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}
}
@Entity
public class User {
@Embedded
private Email email; // Гарантируем валидность
}
4. Множественные экземпляры одного типа
@Entity
public class Person {
@Embedded
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street", column = @Column(name = "office_street"))
})
private Address officeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street", column = @Column(name = "vacation_street"))
})
private Address vacationAddress;
}
Когда использовать @Embedded
Хорошие кейсы:
-
Value Objects — неизменяемые объекты без идентичности
@Embeddable public class Coordinates { private Double latitude; private Double longitude; } -
Агрегирование связанных полей
@Embeddable public class PersonalInfo { private String firstName; private String lastName; private LocalDate birthDate; } -
Бизнес-домены с поведением
@Embeddable public class Rating { private Integer score; // 1-5 private Integer votes; public Double getAverageRating() { return score / (double) votes; } }
Когда НЕ использовать:
-
Когда нужна независимость таблиц
// НЕ используй @Embedded, если Address нужна отдельная таблица @Entity public class Address { @Id private Long id; } -
Когда нужны связи "один-ко-многим"
// Используй @OneToMany вместо @Embedded @Entity public class User { @OneToMany(mappedBy = "user") private List<Address> addresses; }
Сравнение: @Embedded vs @Entity с @OneToOne
// Вариант 1: @Embedded (одна строка в таблице)
@Entity
public class User {
@Embedded
private Address address; // БД: street, city, postal_code в таблице users
}
// Вариант 2: @OneToOne (две таблицы)
@Entity
public class User {
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "address_id")
private Address address; // БД: отдельная таблица addresses
}
@Entity
public class Address {
@Id
private Long id;
}
Выбирай @Embedded если:
- Address не нужна отдельная идентичность (id)
- Address всегда существует только в контексте User
- Нет других сущностей, ссылающихся на Address
Выбирай @OneToOne если:
- Address может существовать независимо
- Address может быть общей для нескольких сущностей
- Нужна гибкость в будущем
Практический пример: E-commerce приложение
@Embeddable
public class Address {
private String street;
private String city;
private String postalCode;
private String country;
}
@Embeddable
public class Money {
private BigDecimal amount;
private String currency;
}
@Entity
public class Order {
@Id
private Long id;
@Embedded
private Address shippingAddress;
@Embedded
private Address billingAddress;
@Embedded
private Money totalPrice;
@Embedded
private Money taxAmount;
}
Таблица БД будет иметь одну строку с ясной структурой, без ненужных таблиц.
Заключение
@Embedded — это мощный инструмент для создания чистой, объектно-ориентированной архитектуры. Она позволяет моделировать Value Objects, переиспользовать код и упростить структуру базы данных, сохраняя информацию в одной таблице без создания лишних сущностей.