0

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.

K.Nicholas
  • 10,956
  • 4
  • 46
  • 66
Jake
  • 45
  • 8

1 Answers1

1

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());
}
K.Nicholas
  • 10,956
  • 4
  • 46
  • 66
  • thank you for your explanation and it is working. I will use EntityGraph. is it possible to do this call: author.getBooks().get(0).getAuthors() – Jake Jan 13 '21 at 20:30
  • i had to add cascade = CascadeType.ALL to be able to add new author to db. – Jake Jan 13 '21 at 20:41
  • 1
    1) I assume it's possible but you'll have to try. Look into subgraphs of entity graphs. 2) I save a new author to the DB as shown above. Maybe you added CASCADE to the Books entity to save authors from there though, again, personally I would not recommend going that route. I think it is better to keep the JPA annotations basic and create a service layer that handles various different cases as needed. – K.Nicholas Jan 13 '21 at 21:33