Introduction
This guide provides an in-depth look at three powerful annotations in Spring Data JPA that help model complex entity relationships and value objects: @Embeddable
, @Embedded
, and @ElementCollection
.
1. @Embeddable and @Embedded
Concept
The @Embeddable
and @Embedded
annotations allow you to model composition relationships where one object is logically part of another, but doesn't need its own identity.
@Embeddable
Use @Embeddable
to mark a class whose instances are stored as part of an owning entity.
@Embeddable
public class Address {
private String street;
private String city;
private String state;
private String zipCode;
// constructors, getters, setters
}
@Embedded
Use @Embedded
in your entity to include an embeddable object:
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Embedded
private Address address;
// constructors, getters, setters
}
Customizing Column Names
You can override column names from the embeddable:
@Entity
public class Customer {
// ...
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street", column = @Column(name = "home_street")),
@AttributeOverride(name = "city", column = @Column(name = "home_city"))
})
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street", column = @Column(name = "work_street")),
@AttributeOverride(name = "city", column = @Column(name = "work_city"))
})
private Address workAddress;
}
Best Practices
- Embeddable classes should be simple value objects
- They shouldn't have their own identity (@Id)
- They should represent a logical part of the owning entity
- Consider making them immutable
2. @ElementCollection
Concept
@ElementCollection
allows you to map a collection of simple types or embeddables without creating a separate entity.
Basic Usage with Simple Types
@Entity
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
@ElementCollection
@CollectionTable(name = "product_tags", joinColumns = @JoinColumn(name = "product_id"))
@Column(name = "tag")
private Set<String> tags = new HashSet<>();
}
With Embeddable Types
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String username;
@ElementCollection
@CollectionTable(name = "user_phone_numbers", joinColumns = @JoinColumn(name = "user_id"))
private Set<PhoneNumber> phoneNumbers = new HashSet<>();
}
@Embeddable
public class PhoneNumber {
private String type; // home, work, mobile
private String number;
// constructors, getters, setters
}
Customizing the Collection Table
@ElementCollection
@CollectionTable(
name = "user_emails",
joinColumns = @JoinColumn(name = "user_id"),
uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "email_type"})
)
@MapKeyColumn(name = "email_type")
@Column(name = "email_address")
private Map<String, String> emails = new HashMap<>();
Fetch Strategies
// Default is LAZY for ElementCollections
@ElementCollection(fetch = FetchType.EAGER)
private Set<String> tags;
// For better performance with large collections
@ElementCollection
@BatchSize(size = 10)
private Set<PhoneNumber> phoneNumbers;
Best Practices
- Use for simple value collections that don't need their own identity
- Prefer LAZY loading for performance
- Consider @OneToMany for more complex relationships
- Be mindful of performance with large collections
3. Advanced Topics
Nested Embeddables
@Embeddable
public class Address {
private String street;
private String city;
@Embedded
private Coordinates coordinates;
}
@Embeddable
public class Coordinates {
private Double latitude;
private Double longitude;
}
Collections of Collections
JPA doesn't directly support collections of collections, but you can work around this:
@Embeddable
public class ContactInfo {
@ElementCollection
private Set<String> emails;
@ElementCollection
private Set<PhoneNumber> phoneNumbers;
}
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
@Embedded
private ContactInfo contactInfo;
}
Immutable Embeddables
@Embeddable
public class Address {
@Column(nullable = false)
private final String street;
@Column(nullable = false)
private final String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
// Only getters, no setters
}
Performance Considerations
- ElementCollections can lead to N+1 query problems
- Consider using @Entity with @OneToMany for large collections
- Use @BatchSize to mitigate performance issues
- For read-heavy scenarios, consider denormalization
4. Comparison Table
Feature | @Embeddable/@Embedded | @ElementCollection |
---|---|---|
Purpose | Single object composition | Collection of values |
Database Structure | Columns in owner table | Separate table |
Identity | No own identity | No own identity |
Querying | Part of owner | Separate queries |
Performance | Generally good | Watch for N+1 |
5. Common Pitfalls and Solutions
- Modifying collections: Always modify collections through getters to ensure proper tracking
// Bad - changes might not be tracked
user.getPhoneNumbers().add(new PhoneNumber());
// Good
Set<PhoneNumber> numbers = user.getPhoneNumbers();
numbers.add(new PhoneNumber());
user.setPhoneNumbers(numbers);
- Equals/HashCode: Implement properly in embeddables
@Embeddable
public class Address {
// ...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return Objects.equals(street, address.street) &&
Objects.equals(city, address.city);
}
@Override
public int hashCode() {
return Objects.hash(street, city);
}
}
- Null handling: Consider making embeddables non-nullable
@Embedded
@NotNull
private Address address;
6. Real-World Examples
Example 1: E-Commerce Domain
@Entity
public class Order {
@Id
@GeneratedValue
private Long id;
@Embedded
private Money total;
@ElementCollection
@CollectionTable(name = "order_items", joinColumns = @JoinColumn(name = "order_id"))
private List<OrderItem> items;
}
@Embeddable
public class Money {
private BigDecimal amount;
private String currency;
}
@Embeddable
public class OrderItem {
private String productId;
private String productName;
private int quantity;
@Embedded
private Money price;
}
Example 2: User Profile
@Entity
public class UserProfile {
@Id
@GeneratedValue
private Long id;
@Embedded
private PersonalInfo personalInfo;
@ElementCollection
@CollectionTable(name = "user_skills", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "skill")
private Set<String> skills;
}
@Embeddable
public class PersonalInfo {
private String firstName;
private String lastName;
@Embedded
private Address address;
@ElementCollection
@CollectionTable(name = "user_contact_methods", joinColumns = @JoinColumn(name = "user_id"))
private Set<ContactMethod> contactMethods;
}
@Embeddable
public class ContactMethod {
private String type;
private String value;
private boolean preferred;
}
Conclusion
The @Embeddable
, @Embedded
, and @ElementCollection
annotations provide powerful tools for modeling complex domain objects in Spring Data JPA:
- Use
@Embeddable
/@Embedded
for value objects that are logically part of an entity - Use
@ElementCollection
for simple collections of values or embeddables - Be mindful of performance implications, especially with large collections
- Properly implement equals/hashCode for embeddables
- Consider immutability for value objects
These annotations help create a more expressive domain model while maintaining good database design principles.