@OneToMany

Maps a parent's collection of children, such as an order's lines; use mappedBy to point at the child's @ManyToOne and keep both sides in sync.

Signature
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
  • Added in Spring Boot JPA 1.0 (javax.persistence)

Parameters

ParameterTypeDescription
$mappedByoptionalString= ""Name of the @ManyToOne field on the child that owns the foreign key. Without it Hibernate treats this side as the owner and uses a join table.
$cascadeoptionalCascadeType[]= {}Operations (PERSIST, MERGE, REMOVE, REFRESH, DETACH or ALL) applied to the children when applied to the parent.
$orphanRemovaloptionalboolean= falseDelete a child row when the child is removed from the collection.
$fetchoptionalFetchType= FetchType.LAZYWhen the collection is loaded.
$targetEntityoptionalClass<?>= the element typeChild type when the collection is not generic.

Return value

n/aMaps a collection-valued association from a parent to many children.

Errors

Join-fetching two List collections in one query throws MultipleBagFetchException. Touching the collection outside a transaction throws LazyInitializationException.

@OneToMany is the parent's view of a @ManyToOne: an order has many order lines. The foreign key still lives in the child's table, so the child's @ManyToOne owns the relationship and the parent's collection is the inverse side. You connect the two with mappedBy, naming the child's field.

@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderLine> lines = new ArrayList<>();

Forgetting mappedBy

Without mappedBy (and without a @JoinColumn), Hibernate assumes a unidirectional association and creates a separate join table such as orders_lines. Each insert then costs an extra row, and changing the collection can rewrite the whole join table. If your schema has no such table, ddl-auto=validate catches it at startup; with update it quietly appears.

Keep both sides in sync

Hibernate only reads the owning side. Adding a line to order.getLines() without setting line.order saves a line with a NULL foreign key, or nothing at all. Hide the collection behind helper methods on the parent, addLine and removeLine, that set both sides, and do not expose a setter for the list.

cascade and orphanRemoval

Use cascade = CascadeType.ALL with orphanRemoval = true when the children are part of the parent and have no life of their own: order lines, invoice items, a recipe's steps. Saving the order saves its lines, and removing a line from the list deletes its row. Do not cascade to things that are shared or independent; cascading REMOVE from a Category to its Products deletes the catalogue.

When not to map it at all

A collection is loaded whole. A customer with 40,000 orders should not have a List<Order> orders field: one careless access loads every row. Keep the unidirectional @ManyToOne on the child and query the children with a paginated repository method instead. Map @OneToMany for small, bounded, owned collections.

Other traps

  • The collection is lazy by default, which is right. Fetch it per query with join fetch; for paged parent queries, a batch fetch size avoids N+1 without join duplicates.
  • Join-fetching two List collections in one query throws MultipleBagFetchException. Fetch them in two queries, or use Set for one of them.
  • Serialising a bidirectional pair with Jackson recurses from order to line to order until the stack overflows. Return DTOs.

Example

Example · java
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Entity
class OrderLine {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "order_id", nullable = false)
    private PurchaseOrder order;

    private String sku;
    private int quantity;
    private BigDecimal unitPrice;

    protected OrderLine() { }

    OrderLine(String sku, int quantity, BigDecimal unitPrice) {
        this.sku = sku;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
    }

    void setOrder(PurchaseOrder order) { this.order = order; }

    BigDecimal subtotal() { return unitPrice.multiply(BigDecimal.valueOf(quantity)); }
}

@Entity
@Table(name = "purchase_order")
public class PurchaseOrder {
    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderLine> lines = new ArrayList<>();

    protected PurchaseOrder() { }

    public static PurchaseOrder create() { return new PurchaseOrder(); }

    public void addLine(String sku, int quantity, BigDecimal unitPrice) {
        OrderLine line = new OrderLine(sku, quantity, unitPrice);
        lines.add(line);
        line.setOrder(this); // owning side: this is what writes order_id
    }

    public void removeLine(OrderLine line) {
        lines.remove(line);   // orphanRemoval deletes the row
        line.setOrder(null);
    }

    public List<OrderLine> getLines() { return Collections.unmodifiableList(lines); }

    public BigDecimal total() {
        return lines.stream().map(OrderLine::subtotal).reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

When to use it

  • An order that owns its lines, so saving the order saves the lines and removing a line deletes it.
  • A recipe detail endpoint that loads a recipe with all its steps in a single query.
  • Deciding not to map a customer's order history as a collection and paging it from a repository instead.

Frequently asked questions

Why did Hibernate create an extra join table?

The @OneToMany has no mappedBy and no @JoinColumn, so Hibernate treats it as a unidirectional association with its own link table. Add mappedBy naming the child's @ManyToOne field.

What is the difference between cascade REMOVE and orphanRemoval?

Cascade REMOVE deletes the children when the parent is deleted. orphanRemoval also deletes a child when it is taken out of the collection while the parent stays. Owned children usually want both.

List or Set for a @OneToMany?

Either works. A List can be ordered with @OrderBy but two Lists cannot be join-fetched in one query. A Set avoids that but needs a stable equals and hashCode on the child.

More examples

Loading parents and children in one query

left join fetch loads the recipe and its steps in one SQL statement, and still returns a recipe that has no steps. Use it for a detail endpoint; for paged lists, fetch the children separately.

Example · java
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

@Entity
class Recipe {
    @Id @GeneratedValue
    private Long id;
    private String name;
    @OneToMany(mappedBy = "recipe")
    private List<Step> steps = new ArrayList<>();
    protected Recipe() { }
}

@Entity
class Step {
    @Id @GeneratedValue
    private Long id;
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Recipe recipe;
    private int position;
    private String text;
    protected Step() { }
}

public interface RecipeRepository extends JpaRepository<Recipe, Long> {

    @Query("select r from Recipe r left join fetch r.steps where r.id = :id")
    Optional<Recipe> findWithSteps(@Param("id") Long id);
}

Children without a collection on the parent

An account can collect millions of login events, so Account has no @OneToMany at all. The repository pages through them on demand, which is safe however large the history grows.

Example · java
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import java.time.Instant;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

@Entity
class Account {
    @Id @GeneratedValue
    private Long id;
    protected Account() { }
}

@Entity
class LoginEvent {
    @Id @GeneratedValue
    private Long id;
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Account account;
    private Instant at;
    protected LoginEvent() { }
}

public interface LoginEventRepository extends JpaRepository<LoginEvent, Long> {
    Page<LoginEvent> findByAccountIdOrderByAtDesc(Long accountId, Pageable pageable);
}

Batch-loading collections for a list page

When a page of 20 orders each touches its lines, Hibernate loads the lines for up to 50 orders in one IN query instead of 20 separate queries. It works with pagination, unlike join fetch on a collection.

Example · properties
spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.default_batch_fetch_size=50

Discussion

  • Be the first to comment on this lesson.