ManyToOne and OneToMany
Mapping Product to Category
The most common relationship. Start with @ManyToOne on the child, and add the @OneToMany collection on the parent only if you really need to navigate that way.
package com.shopease.catalog;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private BigDecimal price;
private int stock;
@ManyToOne(fetch = FetchType.LAZY, optional = false) // owning side
@JoinColumn(name = "category_id", nullable = false) // FK column in products
private Category category;
// getters and setters
}package com.shopease.catalog;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "categories")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name;
@OneToMany(mappedBy = "category") // inverse side
private List<Product> products = new ArrayList<>();
// getters and setters
}The Schema Hibernate Generates
Example
With spring.jpa.hibernate.ddl-auto=create and SQL logging on, Hibernate 6 prints this DDL on MySQL. Notice there is no column for Category.products: the inverse side has no column.
application.properties
spring.jpa.hibernate.ddl-auto=create
logging.level.org.hibernate.SQL=DEBUGExample output
DEBUG org.hibernate.SQL : create table categories (id bigint not null auto_increment, name varchar(255) not null, primary key (id)) engine=InnoDB DEBUG org.hibernate.SQL : create table products (price decimal(38,2), stock integer not null, category_id bigint not null, id bigint not null auto_increment, description varchar(255), name varchar(255), primary key (id)) engine=InnoDB DEBUG org.hibernate.SQL : alter table categories add constraint UKt8o6pivur7nn124jehx7cygw5 unique (name) DEBUG org.hibernate.SQL : alter table products add constraint FKog2rp4qthbtt2lfyhfo32lsw9 foreign key (category_id) references categories (id)
Order and OrderItem (Bidirectional)
@Entity
@Table(name = "orders") // "order" is a reserved SQL word, so use "orders"
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id")
private User user;
@Enumerated(EnumType.STRING)
private OrderStatus status = OrderStatus.CREATED;
private BigDecimal total = BigDecimal.ZERO;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
}
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "order_id")
private Order order; // owning side
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "product_id")
private Product product;
private int quantity;
private BigDecimal price; // price at the time of purchase
}Tip: Always initialise collections (= new ArrayList<>()). A null collection causes NullPointerException in helper methods and in your own code.
Unidirectional OneToMany: Avoid It
A @OneToMany without mappedBy and without @JoinColumn makes Hibernate create an extra join table (orders_items) and run extra statements. If you only need one direction, map the @ManyToOne side and query the children with a repository method such as findByOrderId(Long id).
| Mapping | Tables | Verdict |
|---|---|---|
@ManyToOne only | FK in child table | best, simplest |
@ManyToOne + @OneToMany(mappedBy) | FK in child table | good when you navigate parent → children |
@OneToMany + @JoinColumn | FK in child table, extra UPDATE statements | acceptable |
@OneToMany alone | extra join table | avoid |