One To Many Example | Spring Data JPA


If your new to Spring Data JPA, we highly recommend you check out this intro to Spring Data JPA first...

@OneToMany Bidirectional Example (Best Approach)

Author.java

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Book> books = new HashSet<>();
    public void addBook(Book book){
        books.add(book);
        book.setAuthor(this);
    }
    public void removeBook(Book book){
        books.remove(book);
        book.setAuthor(null);
    }
    //getters & setters
}

Book.java

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private Long id;
    @ManyToOne(fetch = FetchType.LAZY)
    private Author author;
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Book )) return false;
        return id != null && id.equals(((Book) o).getId());
    }
    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
    //getters & setters
}

This creates a one-to-many relationship between an author and book table. An author has many books.

Using both @OneToMany and @ManyToOne makes this a bidirectional relationship.

The mappedBy = "author" attribute tells us the book table is the owning side of the relationship.

The cascade = CascadeType.ALL tells Hibernate to propagate changes from any book to its related entities. For example, if we remove an author then all of its books will also be deleted from the database.

The orphanRemoval = true tells Hibernate to automatically remove orphaned entities. A book would become an orphan if it's removed from the author's list of books. This option tells Hibernate to automatically delete orphans.

The helper methods addBook() and removeBook() are useful in keeping the persisted context in sync. Strange things can happen when both sides of the bidirectional relationship aren't updated. The database can get out of sync with the context and unexpected things can happen. These methods make it easy to update both sides of the relationship...

The fetch = FetchType.LAZY tells Hibernate to lazily load books for a given author. This means when we retrieve an author from the database, Hibernate won't return the associated books for that author in the same call. A separate request is made to the database only when the application explicitly asks for author.getBooks().

It's important to @Override the equals() and hashCode() method since the Author entity relies on checking equality for it's helper methods...

Persist to database...

Author author = new Author();
Book book = new Book();
Book book2 = new Book();
author.addBook(book);
author.addBook(book2);
authorRepo.save(author);

SQL Generated...

insert into author (id) values (?)
insert into book (author_id, id) values (?, ?)
insert into book (author_id, id) values (?, ?)

Retrieve from database...

Author author = authorRepo.findById(Long.valueOf(1)).get();
Set<Book> books = author.getBooks();

SQL Generated...

select author0_.id as id1_0_0_ from author author0_ where author0_.id=?
select books0_.author_id as author_i2_1_0_, books0_.id as id1_1_0_, books0_.id as id1_1_1_, books0_.author_id as author_i2_1_1_ from book books0_ where books0_.author_id=?

Why is this the best approach?

@OneToMany bidirectional is the best approach because it results in the least SQL. While you can implement the same relationship with other approaches (next examples), this is the recommended (most efficient) way to manage most one to many relationships with Spring Data JPA...

@OneToMany Unidirectional Example

Author.java

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @OneToMany(cascade = CascadeType.ALL)
    Set<Book> books = new HashSet<>();
    //getters & setters
}

Book.java

@Entity
public class Book {
    @Id    
    private Long id;    
    //getters & setters
}

Persist to database...

Author author = new Author();
Book book = new Book();
Book book2 = new Book();
author.getBooks().add(book);
author.getBooks().add(book2);
authorRepo.save(author);

SQL Generated...

insert into author (id) values (?)
insert into book (id) values (?)
insert into book (id) values (?)
insert into author_books (author_id, books_id) values (?, ?)
insert into author_books (author_id, books_id) values (?, ?)

Retrieve from database...

Author author = authorRepo.findById(Long.valueOf(1)).get();
Set<Book> books = author.getBooks();

SQL Generated...

select author0_.id as id1_0_0_ from author author0_ where author0_.id=?
select books0_.author_id as author_i1_1_0_, books0_.books_id as books_id2_1_0_, book1_.id as id1_2_1_ from author_books books0_ inner join book book1_ on books0_.books_id=book1_.id where books0_.author_id=?

Why is this not the best approach?

Notice how a join table is generated. This results in extra calls for inserting the relationship into the database. This can be handled more efficiently with the bidirectional example.

@OneToMany + @ManyToOne Example Without Using "mappedBy"

Author.java

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @OneToMany(cascade = CascadeType.ALL)
    Set<Book> books = new HashSet<>();
    //getters & setters
}

Book.java

@Entity
public class Book {
    @Id    
    private Long id;
    @ManyToOne
    Author author;    
    //getters & setters
}

Persist to database...

Author author = new Author();
Book book = new Book();
Book book2 = new Book();
author.getBooks().add(book);
author.getBooks().add(book2);
authorRepo.save(author);

SQL Generated...

insert into author (id) values (?)
insert into book (author_id, id) values (?, ?)
insert into book (author_id, id) values (?, ?)
insert into author_books (author_id, books_id) values (?, ?)
insert into author_books (author_id, books_id) values (?, ?)

Retrieving from database...

Author author = authorRepo.findById(Long.valueOf(1)).get();
Set<Book> books = author.getBooks();

SQL Generated...

select author0_.id as id1_0_0_ from author author0_ where author0_.id=?
select books0_.author_id as author_i1_1_0_, books0_.books_id as books_id2_1_0_, book1_.id as id1_2_1_, book1_.author_id as author_i2_2_1_, author2_.id as id1_0_2_ from author_books books0_ inner join book book1_ on books0_.books_id=book1_.id left outer join author author2_ on book1_.author_id=author2_.id where books0_.author_id=?

Why this is not the best approach...

Similar to the last example, this creates additional join table and populates an unused column author_id in the book table. While you can set the author_id, it isn't necessary because of the join column.

How @OneToMany Works Under the Hood

The Java Persistence API (JPA) defines how Java objects get persisted to database tables. ORM providers like Hibernate implement the JPA specification.

As an example, JPA provides the interface for the @OneToMany annotation...

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OneToMany {
    Class targetEntity() default void.class;
    CascadeType[] cascade() default {};
    FetchType fetch() default FetchType.LAZY;
    String mappedBy() default "";
    boolean orphanRemoval() default false;
}

and the @ManyToOne annotation...

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ManyToOne {
    Class targetEntity() default void.class;
    CascadeType[] cascade() default {};
    FetchType fetch() default FetchType.EAGER;
    boolean optional() default true;
}

ORM providers like Hibernate implement these annotations at runtime. They generate the necessary code to create database tables, relationships, foreign keys etc. so that you (as the developer) can simply use annotations to describe the behavior you want.

Spring Data JPA provides additional abstractions for working with Hibernate.

Confused? Check out Spring Data JPA vs Hibernate to understand the magic behind annotations like @OneToMany.

Your thoughts?