How to use @Embeddable, @EmbeddedId, and @MapsId
When modeling relational databases with Jakarta Persistence (JPA), there are situations where a single column cannot uniquely identify a record. In these cases, a composite primary key is required.
Jakarta Persistence provides a clean and expressive way to model composite keys using:
@Embeddable@EmbeddedId@MapsId
These annotations allow developers to build more explicit domain models while keeping entity relationships consistent with the database design.
This article explores how to implement composite keys and how to map relationships when entities share parts of the primary key.
When do you need a composite key?
A composite key is useful when the identity of a row depends on multiple columns.
Consider the following example: an order_items table.
| order_id | product_id | quantity | price |
|---|---|---|---|
| 1 | 10 | 2 | 15.00 |
| 1 | 15 | 1 | 30.00 |
Neither order_id nor product_id alone is sufficient to uniquely identify a row.
But together they form a unique identifier.
In Jakarta Persistence, this scenario is typically modeled using an embedded primary key class.
Creating a composite key with @Embeddable
The first step is defining a class that represents the composite identifier.
import java.io.Serializable;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import java.util.Objects;
@Embeddable
public class OrderItemId implements Serializable {
@Column(name = "order_id")
private Long orderId;
@Column(name = "product_id")
private Long productId;
public OrderItemId() {}
public OrderItemId(Long orderId, Long productId) {
this.orderId = orderId;
this.productId = productId;
}
public Long getOrderId() {
return orderId;
}
public Long getProductId() {
return productId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof OrderItemId that)) return false;
return Objects.equals(orderId, that.orderId)
&& Objects.equals(productId, that.productId);
}
@Override
public int hashCode() {
return Objects.hash(orderId, productId);
}
}
Important requirements
A composite key class must:
- Implement
Serializable - Have a no-argument constructor
- Implement
equals()andhashCode()
These requirements ensure that JPA can correctly manage entity identity and caching.
Using the composite key with @EmbeddedId
Once the key class is created, it can be embedded into the entity using @EmbeddedId.
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.math.BigDecimal;
@Entity
@Table(name = "order_items")
public class OrderItem {
@EmbeddedId
private OrderItemId id;
private Integer quantity;
private BigDecimal price;
public OrderItem() {}
public OrderItem(OrderItemId id, Integer quantity, BigDecimal price) {
this.id = id;
this.quantity = quantity;
this.price = price;
}
public OrderItemId getId() {
return id;
}
}
At this point, the entity works correctly with a composite key. However, there is still an important improvement we can make when relationships are involved.
The limitation without @MapsId
In real-world scenarios, OrderItem would likely reference Order and Product entities.
A naive implementation might look like this:
@ManyToOne
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
The problem is that the order_id and product_id columns would now appear twice:
- once inside
OrderItemId - once in the relationship mapping
This duplication can lead to confusion and maintenance problems.
This is exactly where @MapsId becomes useful.
Using @MapsId to map relationships
@MapsId tells JPA that the identifier of a relationship should be used as part of the entity's primary key.
Updated entity
import jakarta.persistence.*;
@Entity
@Table(name = "order_items")
public class OrderItem {
@EmbeddedId
private OrderItemId id;
@ManyToOne
@MapsId("orderId")
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne
@MapsId("productId")
@JoinColumn(name = "product_id")
private Product product;
private Integer quantity;
private java.math.BigDecimal price;
public OrderItem() {}
public OrderItem(Order order, Product product, Integer quantity) {
this.order = order;
this.product = product;
this.id = new OrderItemId(order.getId(), product.getId());
this.quantity = quantity;
}
}
What @MapsId does
@MapsId("orderId") tells JPA:
The
order_idcolumn used in this relationship corresponds to theorderIdfield inside the composite key.
The same applies for:
@MapsId("productId")
As a result:
- The composite key stays consistent
- The relationship mapping becomes cleaner
- Duplicate column mappings are avoided
Persisting entities with @MapsId
Persisting an entity now looks like this:
Order order = entityManager.find(Order.class, 1L);
Product product = entityManager.find(Product.class, 10L);
OrderItem item = new OrderItem(order, product, 2);
entityManager.persist(item);
The ID is automatically aligned with the relationships because of @MapsId.
Using @Embedded for value objects
While @EmbeddedId is used for primary keys, @Embedded is used for value objects inside entities.
For example, an address can be embedded in a Customer.
Value object
@Embeddable
public class Address {
private String street;
private String city;
private String zipCode;
}
Entity
@Entity
public class Customer {
@Id
private Long id;
private String name;
@Embedded
private Address address;
}
This results in a single table containing:
id
name
street
city
zip_code
There is no separate address table.
This pattern works very well for value objects in domain-driven design.
Best practices when working with composite keys
1. Prefer @EmbeddedId over @IdClass
Both approaches exist, but @EmbeddedId is generally:
- cleaner
- easier to maintain
- more aligned with domain modeling
2. Use @MapsId for relationship-based keys
Whenever a composite key references other entities, @MapsId keeps the model consistent and avoids column duplication.
3. Keep key classes simple
Composite key classes should contain only identification fields, not business logic.
4. Implement equality carefully
Incorrect equals() and hashCode() implementations are one of the most common causes of subtle JPA bugs.
Final thoughts
Composite keys are common in many database designs, especially in:
- join tables
- many-to-many relationships with attributes
- domain models where identity depends on multiple fields
Jakarta Persistence provides powerful tools to model these scenarios cleanly:
-
@Embeddabledefines reusable identifier classes -
@EmbeddedIdembeds those identifiers as entity keys -
@MapsIdlinks relationships directly to the composite key
When used correctly, these annotations help create clear, maintainable, and expressive entity models that align well with the underlying database schema.
Understanding how they work together is an important step toward mastering Jakarta Persistence.
Top comments (0)