In a relational database, tables are connected through foreign keys. In JPA the same thing is expressed with annotations on entity fields. Here we'll look at how it works, what to pay attention to, and where it's easy to slip up.
For the database an association is just the value of the customer_id column, and that value is taken from the @ManyToOne side only. While the order has merely been added to the orders collection, there is nothing to put in the column: the side carrying mappedBy is read-only as far as Hibernate is concerned.
Why associations are more than just annotations
When a developer first sees @OneToMany, it looks simple: add the annotation and you're done. In reality every association has an owning side — the one that manages the foreign key in the database. If you don't specify it correctly, Hibernate will either create extra columns or simply won't save the association.
The second difficulty: by default some associations load immediately (EAGER), others load lazily (LAZY). Getting this expectation wrong is a straight path to LazyInitializationException and the N+1 query problem.
@ManyToOne — the simplest side
@ManyToOne is the owning side by default. This is exactly where the foreign key lives in the table.
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // default is EAGER, better to set LAZY explicitly
@JoinColumn(name = "customer_id") // name of the FK column in the orders table
private Customer customer;
}
@JoinColumn specifies which column of the orders table holds the reference to customers. Without this annotation Hibernate builds the name itself: the field name plus the name of the target key column, that is customer_id. Better to be explicit.
Important: for @ManyToOne the default strategy is EAGER. This means that when loading Order, Hibernate will immediately pull in Customer — even if it isn't needed. An explicit fetch = FetchType.LAZY fixes that.
@OneToMany — the inverse side and mappedBy
@OneToMany describes "one to many" from the "one" side. But on its own this annotation does not create a foreign key — it merely tells Hibernate where to look for the reverse link.
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "customer") // "customer" — the field name in Order
private List<Order> orders = new ArrayList<>();
}
mappedBy = "customer" is an instruction: "the owning side is the customer field in the Order class; don't create any additional table or column, just read through it."
If the @OneToMany has neither mappedBy nor @JoinColumn, Hibernate will create an intermediate join table (customer_orders) — just like with @ManyToMany. This is a common mistake.
The short rule:
mappedBygoes on the side that does not own the foreign key.
Unidirectional vs bidirectional association
Unidirectional — there is only one annotation on a single entity:
// Only in Order — Customer knows nothing about orders
@ManyToOne
@JoinColumn(name = "customer_id")
private Customer customer;
Bidirectional — both blocks from the sections above at once: @ManyToOne with @JoinColumn in Order and @OneToMany(mappedBy = "customer") in Customer. Navigation gets easier both ways, but it requires care: Hibernate saves only what it sees on the owning side. If you add an order to the customer.getOrders() list but don't call order.setCustomer(customer) — the change won't be saved.
Helper methods for synchronization
The solution is to add and remove through helper methods that update both sides at once:
@Entity
public class Customer {
@OneToMany(mappedBy = "customer")
private List<Order> orders = new ArrayList<>();
public void addOrder(Order order) {
orders.add(order);
order.setCustomer(this); // synchronize the owning side
}
public void removeOrder(Order order) {
orders.remove(order);
order.setCustomer(null);
}
}
The calling code works only through customer.addOrder(order) — and both sides are always consistent.
The same mechanics show up without a database — the insert statement is built from the owning side alone:
live example
import java.util.ArrayList;
import java.util.List;
public class OwningSideDemo {
static class Customer {
long id = 7;
List<Order> orders = new ArrayList<>();
void addOrder(Order order) {
orders.add(order);
order.customer = this;
}
}
static class Order {
long id;
Customer customer;
Order(long id) {
this.id = id;
}
}
static String insert(Order order) {
Long fk = order.customer == null ? null : order.customer.id;
return "INSERT INTO orders (id, customer_id) VALUES (" + order.id + ", " + fk + ")";
}
public static void main(String[] args) {
Customer customer = new Customer();
Order lost = new Order(101);
customer.orders.add(lost);
Order saved = new Order(102);
customer.addOrder(saved);
System.out.println(insert(lost));
System.out.println(insert(saved));
System.out.println("orders in the collection: " + customer.orders.size());
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Both orders sit in the collection, but they reach the database differently: the first one has an empty customer_id, the second one has 7. In memory the picture looks right; in the table it doesn't.
Default FetchType
Hibernate picks a loading strategy based on the type of association:
| Annotation | Default strategy |
|---|---|
@ManyToOne | EAGER |
@OneToOne | EAGER |
@OneToMany | LAZY |
@ManyToMany | LAZY |
ToOne associations load immediately — this often catches people off guard. If an entity has several @ManyToOne fields, every SELECT will drag in additional queries. Practical rule: always set FetchType.LAZY explicitly on @ManyToOne and @OneToOne, and load the data you need via JOIN FETCH in the query.
For more on loading strategies, see the article Lazy and eager loading.
@ManyToMany
@ManyToMany creates an intermediate table. You choose the owning side yourself — put @JoinTable on it and mappedBy on the inverse side.
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
@JoinTable(
name = "student_course", // name of the intermediate table
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
}
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany(mappedBy = "courses") // the inverse side
private Set<Student> students = new HashSet<>();
}
@ManyToMany is handy for simple cases, but as soon as the intermediate table gains additional columns (enrollment date, status) — you need to turn it into a separate entity with two @ManyToOne fields.
cascade and orphanRemoval
cascade determines which operations on the parent are automatically applied to the child entities. The most commonly used are:
CascadeType.PERSIST— children are saved together with the parent.CascadeType.MERGE— children are updated together with the parent.CascadeType.REMOVE— children are deleted when the parent is deleted.CascadeType.ALL— all operations.
orphanRemoval = true complements cascade: if a child entity is removed from the collection — it is deleted from the database automatically. Useful when a child entity has no meaning without its parent.
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
Typical cascading traps are covered in common Hibernate pitfalls.
In short
@ManyToOne— the owning side; this is where the FK is stored and where@JoinColumngoes.@OneToMany(mappedBy = "...")— the inverse side;mappedBypoints to the field on the owning side.- Without
mappedByand without@JoinColumnHibernate will create an intermediate table — just like with@ManyToMany. - For bidirectional associations you need to update both sides — use helper methods.
@ManyToOneand@OneToOneareEAGERby default — switch them toLAZYand load viaJOIN FETCH.cascade = CascadeType.ALL+orphanRemoval = true— a convenient duo for aggregates where child entities live only inside the parent.
What to read next
- Entity mapping — the
@Column,@Tableannotations, types, and converters. - Lazy and eager loading — when and how related objects are loaded.
- The N+1 query problem — how associations spawn an avalanche of queries and how to stop it.
- Spring Data JPA — repositories and derived queries on top of Hibernate.