Skip to content
Advertisement

spring-data-jpa many to many findByID

i have many to many relationship between book and author, i have 3 tables: author, book and author_book.

@Entity()
@Table(name = "author")
public class Author implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinTable(
            name = "author_book",
            joinColumns = @JoinColumn(name = "author_id"),
            inverseJoinColumns = @JoinColumn(name = "book_id")
    )
    private List<Book> authorBooks = new ArrayList<Book>();

    public Author() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Book> getAuthorBooks() {
        return authorBooks;
    }

    public void setAuthorBooks(List<Book> authorBooks) {
        this.authorBooks = authorBooks;
    }

    @Override
    public String toString() {
        return "Author{" + "name=" + name + ", authorBooks=" + authorBooks + '}';
    }

}


@Entity()
@Table(name = "book")
public class Book implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;

    @ManyToMany(mappedBy = "authorBooks", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Author> bookAuthors = new ArrayList<Author>();

    public Book() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Author> getBookAuthors() {
        return bookAuthors;
    }

    public void setBookAuthors(List<Author> bookAuthors) {
        this.bookAuthors = bookAuthors;
    }

    @Override
    public String toString() {
        return "Book{" + "name=" + name + ", bookAuthors=" + bookAuthors + '}';
    }

}

i can add data to db without a problem, but when i want to get an author or a book by its id

Optional<Author> optionalAuthor = authorReposi.findById(1L);
System.out.println("Author: " + optionalAuthor.get().toString());

i get an error: LazyInitialization failed to lazily …

I want to use FetchType.LAZY and get the instance of author or book.

Thank you for your time.

Advertisement

Answer

So, read the fine manual: 6.3.10. Configuring Fetch- and LoadGraphs.

Your issue is simply that your toString() methods are recursive. Authors says to print Books, and Books says to print Authors. Pro tip: success is in the details.

For a load or fetch you need to use an EntityGraph from JPA to specify the joined attributes. So:

@Entity()
@Table(name = "author")
@NamedEntityGraph(name = "Book.detail", attributeNodes = @NamedAttributeNode("books"))
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;

    @ManyToMany
    private List<Book> books;
}

and

@Entity()
@Table(name = "book")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;

    @ManyToMany(mappedBy = "books")
    private List<Author> authors;

}

With repositories:

public interface AuthorRepository extends JpaRepository<Author, Long>{
    @EntityGraph(value = "Book.detail", type = EntityGraphType.LOAD)
    Author getById(Long id);
}

and

public interface BookRepository extends JpaRepository<Book, Long>{

}

Then you must print what you want yourself. Basically, putting toStrings in Entities generally causes problems, but you should also RTFM.

private void read(Long id) {
    Author a = authorRepository.getById(id);
    System.out.println("Author: " + a.getName());
    for( Book b: a.getBooks()) {
        System.out.println("tBook: " + b.getName());
    }
}

Finally, I avoid using Cascade annotations like the plague. They are complicated annotations. Also, ManyToMany is FetchType.LAZY by default. The important annotation is the mappedBy annotation. This tells you which entity owns the relationship. The owning entity is the one responsible for persisting relations. The other side of bidirectional annotations are really only for queries. There is no need to make new ArrayList in the entities since they will be thrown away anyway during queries. Just create a list when you need to persist a new Author entity with relations, otherwise use the lists returned by the queries.

private Author save() {
    Book b = bookRepository.save(Book.builder().name("b1").build());
    return authorRepository.save(Author.builder().name("a1").books(Collections.singletonList(b)).build());
}

User contributions licensed under: CC BY-SA
2 People found this is helpful
Advertisement