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=DEBUG

Example 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).

MappingTablesVerdict
@ManyToOne onlyFK in child tablebest, simplest
@ManyToOne + @OneToMany(mappedBy)FK in child tablegood when you navigate parent → children
@OneToMany + @JoinColumnFK in child table, extra UPDATE statementsacceptable
@OneToMany aloneextra join tableavoid