@OneToMany & @ManyToOne
@OneToMany & @ManyToOne
The @OneToMany / @ManyToOne pair is the most frequently used relationship in any relational domain model. A Customer places many Orders; a Department employs many Employees; a Post has many Comments. Learning to map this relationship correctly — and to avoid the common pitfalls — will serve you in virtually every Spring Boot project you build.
The Domain Model
Throughout this lesson we will use an e-commerce domain. A Customer can place many Order objects, and every Order knows which Customer it belongs to. In the relational database this is represented by a foreign-key column customer_id on the orders table — a single column that points back to the customers table.
Mapping the Many Side: @ManyToOne
The many side — the entity that holds the foreign key — is always the owning side of the relationship. Map it with @ManyToOne plus @JoinColumn:
Key decisions on the @ManyToOne:
fetch = FetchType.LAZY— do not load theCustomeruntil it is actually accessed. This is the single most important performance setting and is covered in depth in Lesson 6. Always set it explicitly; do not rely on the default.optional = false— tells Hibernate an order always has a customer, allowing it to use an inner join instead of a left outer join when queries involve this path.@JoinColumn(name = "customer_id")— names the foreign-key column. Without this Hibernate would infer a name that may not match your schema conventions.
entityManager.persist(order), Hibernate writes the value of customer_id based on the customer field on the Order entity — not on any collection in Customer. This is the most common source of "I added to the list but nothing was saved" bugs.
Mapping the One Side: @OneToMany
The one side is the inverse side of the relationship. It is mapped with @OneToMany(mappedBy = ...), where mappedBy refers to the field name on the owning entity:
Critical points on the @OneToMany side:
mappedBy = "customer"is mandatory. Without it Hibernate creates a separate join table instead of using the existing foreign key — doubling the schema complexity for no reason.- Always initialise the collection (
new ArrayList<>()). Anullcollection causesNullPointerExceptionthe first time Hibernate tries to initialise the proxy. - Convenience methods (
addOrder/removeOrder) synchronise both sides within the same persistence context. Forgetting to set the owning side is the cause of phantom rows and stale in-memory state. - Return an unmodifiable view from the getter so callers cannot bypass the convenience methods and break the invariant.
Unidirectional vs Bidirectional
You do not always need both sides. A unidirectional mapping exposes only one navigable direction:
A unidirectional @OneToMany without mappedBy should almost always be avoided — Hibernate will generate a join table even though a foreign key column would suffice, and it produces inefficient SQL. If you need the collection side only, still define the @ManyToOne on the child entity and use mappedBy on the parent.
@ManyToOne on the child and use mappedBy.
Persisting and Querying
In a typical Spring Data JPA repository pattern:
Fetching orders for a customer in a repository:
Performance Trade-offs to Know
The @OneToMany collection is a Hibernate collection proxy that is initialised lazily by default. This means:
- Accessing
customer.getOrders()outside an active transaction will throw aLazyInitializationException. Always load what you need inside the transaction. - If you load a list of customers and then access their orders one by one, you trigger the N+1 select problem — one query per customer. This is covered in Lessons 7 and 8.
- For large collections consider whether you really need
@OneToManyon the parent at all, or whether querying from the child with@ManyToOneis sufficient.
SELECT o FROM Order o WHERE o.customer.id = :id uses the indexed foreign-key column and never loads the Customer entity or its collection proxy. It is almost always faster and simpler than navigating a lazy collection.
Summary
The @ManyToOne / @OneToMany pair maps a single foreign-key column to a bidirectional object graph. The many side (@ManyToOne) is the owning side — it controls what Hibernate writes to the database. The one side (@OneToMany(mappedBy = ...)) is the inverse side — it provides convenient navigation but has no effect on persistence unless you also set the owning side. Always use fetch = FetchType.LAZY on @ManyToOne, always initialise your collections, and always write convenience methods that keep both sides in sync. The next lesson covers the other side of the cardinality spectrum: @ManyToMany.