Model how tables connect — one-to-many, many-to-one, many-to-many, and one-to-one — and control fetching, cascading, and the N+1 query problem.
Why: the most common relationship — many books belong to one author. When @ManyToOne: on the "many" side, it owns the foreign key column. When @OneToMany(mappedBy): the inverse collection on the "one" side. Note: mappedBy names the field on the other side that owns the link.
@Entity
public class Author {
@Id @GeneratedValue private Long id;
private String name;
@OneToMany(mappedBy = "author") // inverse side, no FK column
private List<Book> books = new ArrayList<>();
}
@Entity
public class Book {
@Id @GeneratedValue private Long id;
@ManyToOne // owning side, holds author_id
private Author author;
}When @ManyToMany: both sides can have many of the other — a book has several tags, a tag labels many books. Note: it needs a join table; @JoinTable names it. Pick one side to own the relationship and use mappedBy on the other.
@Entity
public class Book {
@ManyToMany
@JoinTable(
name = "book_tag",
joinColumns = @JoinColumn(name = "book_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id"))
private Set<Tag> tags = new HashSet<>();
}
@Entity
public class Tag {
@ManyToMany(mappedBy = "tags")
private Set<Book> books = new HashSet<>();
}When @OneToOne: a strict one-per-one link — a book has exactly one detail record. Note: the side with @JoinColumn owns the foreign key. Use it sparingly; often the fields just belong on the same entity.
@Entity
public class Book {
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "detail_id") // owns the FK
private BookDetail detail;
}
@Entity
public class BookDetail {
@Id @GeneratedValue private Long id;
@Lob private String description; // long text
private int pageCount;
}Why: @ManyToOne defaults to EAGER (loaded immediately) and @OneToMany to LAZY (loaded on first access). When you loop over books and touch book.getAuthor(), lazy loading can fire one query per row — the N+1 problem. Note: a JOIN FETCH query loads everything in one go.
// LAZY by default — load the collection only when used
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Book> books;
// avoid N+1: fetch authors and their books in a single query
@Query("select a from Author a join fetch a.books")
List<Author> findAllWithBooks();When cascade: operations flow from parent to children — saving an Author with CascadeType.ALL also saves its new Books. When orphanRemoval: removing a child from the collection deletes its row. Note: combine them when children cannot exist without the parent.
@Entity
public class Author {
@OneToMany(
mappedBy = "author",
cascade = CascadeType.ALL, // save/delete books with the author
orphanRemoval = true) // removing from the list deletes the row
private List<Book> books = new ArrayList<>();
}