1

I am developing Spring Boot app with Spring Data JPA and H2 database. I am using spring-data-jpa. When I use ManyToMany mapper class to get the data of another class. But I found it is NULL.

The code is on github

Book.class

@Entity
public class Book implements Serializable {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue
private Integer id;

private String name;

@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "BOOK_AUTHOR", joinColumns = {
        @JoinColumn(name = "BOOK_ID", referencedColumnName = "ID")}, inverseJoinColumns = {
        @JoinColumn(name = "AUTHOR_ID", referencedColumnName = "ID")})
private Set<Author> authors;

public Book() {
    super();
}

public Book(String name) {
    super();
    this.name = name;
    this.authors = new HashSet<>();
}

public Book(String name, Set<Author> authors) {
    super();
    this.name = name;
    this.authors = authors;
}

public Integer getId() {
    return id;
}

public void setId(Integer id) {
    this.id = id;
}

public String getName() {
    return name;
}

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

public Set<Author> getAuthors() {
    return authors;
}

public void setAuthors(Set<Author> authors) {
    this.authors = authors;
}

@Override
public String toString() {
    return String.format("Book [id=%s, name=%s, authors=%s]", id, name, authors);
}

}

Author.class

@Entity
public class Author implements Serializable {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue
private Integer id;

private String name;

@ManyToMany(mappedBy = "authors")
private Set<Book> books;


//why CAN NOT GET the data when using these code else ?
//    @ManyToMany(cascade = CascadeType.ALL)
//    @JoinTable(name = "BOOK_AUTHOR", joinColumns = {
//            @JoinColumn(name = "AUTHOR_ID", referencedColumnName = "ID")}, 
//inverseJoinColumns = {
//            @JoinColumn(name = "BOOK_ID", referencedColumnName = "ID")})
//    private Set<Book> books;

public Author() {
    super();
}

public Author(String name) {
    super();
    this.name = name;
}

public Integer getId() {
    return id;
}

public void setId(Integer id) {
    this.id = id;
}

public String getName() {
    return name;
}

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

public Set<Book> getBooks() {
    return books;
}

public void setBooks(Set<Book> books) {
    this.books = books;
}

@Override
public String toString() {
    return String.format("Author [id=%s, name=%s, books=%s]", id, name, books);
}

}

Test code snapper in test.class

    List<Book> books = bookRepository.findAll();
    for (Book it : books) {
        Set<Author> authors = it.getAuthors();
        //CAN get authors data.
        System.out.println(authors.size());
    }

    assertThat(bookRepository.findAll()).hasSize(2);

    List<Author> authors = authorRepository.findAll();
    for (Author it : authors) {
        //CAN NOT get books data ? Why and HOW ?
        //HOW can I get the books data ? Or other ways ? 
        // thanks 
        Set<Book> books1 = it.getBooks();
        assertThat(books1 == null);
    }

    assertThat(authorRepository.findAll()).hasSize(3);

Is there some error in my code ? Or other ways ?

Thanks vary much.

Neil Stockton
  • 11,383
  • 3
  • 34
  • 29
larntin
  • 110
  • 1
  • 10
  • What lifecycle state is the `Author` object in when accessing its `getBooks` method? and was the data loaded prior to that? The JPA provider has a log that tells you such things. Is the data correct in the database? – Neil Stockton Jul 08 '17 at 16:50
  • Your code should work, unless you just created the books and the authors in the same transaction, and forgot to set the books inside the authors. JPA will **never** set a collection to null, and you shouldn't either. Initialize your sets to empty sets. – JB Nizet Jul 08 '17 at 16:55

3 Answers3

2

It's not good to set FetchType to EAGER cause it's not efficiently. And of course you have to initialize authors and books first.


You are using a bidirectional ManyToMany relation. So you have to manually 'link' Authors with the Book when you added them to this Book :

@ManyToMany(cascade = CascadeType.ALL)
private final Set<Author> authors = new HashSet<>();
// ...
public void setAuthors(Set<Author> authors) {
    for (Author author : authors) {
        author.getBooks().add(this);
        this.authors.add(author);
    }
}

(pay attention on author.getBooks().add(this);)

Respectively you need to change your constructor:

public Book(String name, Set<Author> authors) {
    this.name = name;
    setAuthors(authors);
}

Also I suggest to correct toString method - remove authors from it, to avoid occurrence of Stackoverflow exception (you have to do this in Author class too):

@Override
public String toString() {
    return String.format("Book [id=%s, name=%s]", id, name);
}

Than your test will work fine:

@Before
public void init() {
    Author lewis = new Author("Lewis");
    Author mark = new Author("Mark");
    Author peter = new Author("Peter");

    Book spring = new Book("Spring in Action", new HashSet<>(asList(lewis, mark)));
    Book springboot = new Book("Spring Boot in Action", new HashSet<>(asList(lewis, peter)));

    bookRepository.save(Arrays.asList(spring, springboot));
}

@Test
public void findAll() {
    List<Book> books = bookRepository.findAll();
    assertThat(books).hasSize(2);
    for (Book book : books) {
        Set<Author> bookAuthors = book.getAuthors();
        assertThat(bookAuthors).isNotNull();
        assertThat(bookAuthors.size()).isGreaterThan(0);

        System.out.println(book);
        bookAuthors.forEach(System.out::println);
    }

    List<Author> authors = authorRepository.findAll();
    assertThat(authors).hasSize(3);
    for (Author author : authors) {
        Set<Book> authorBooks = author.getBooks();
        assertThat(authorBooks).isNotNull();
        assertThat(authorBooks.size()).isGreaterThan(0);

        System.out.println(author);
        authorBooks.forEach(System.out::println);
    }    
}

See Hibernate User Guide for more info.


In general don't use cascade = CascadeType.ALL for ManyToMany because in this case entities are independent. Use cascade = {CascadeType.PERSIST, CascadeType.MERGE} instead.

Cepr0
  • 28,144
  • 8
  • 75
  • 101
  • I commit it in my github and commet. [project code](https://github.com/larntin/jpa-manytomany-springboot-h2) – larntin Jul 09 '17 at 16:20
0

you can use mappedBy = "", cascade = CascadeType.ALL, fetch = FetchType.LAZY into entity. use @Transactional(propagation = Propagation.REQUIRED) annotation before method and use @Transactional on class. when you are fetching data use .size() method on many to many relation entity lilnk : when you fetch Author use Author.getBooks().size(); you can get the boot detail of author.

example

FOrd fOrd = orderRepo.findOne(id);
    fOrd.getFOrdItems().size();
    fOrd.getFOrdDetails().size();
Anshul Sharma
  • 3,432
  • 1
  • 12
  • 17
0

When I use this code iterator the Set, crash on console:

org.hibernate.LazyInitializationException: could not initialize proxy - no Session, encore un fois

So, I search this tips on Stackoverflow.com.

This answer help me

Just add

FISRT - Initial books

@ManyToMany(mappedBy = "authors")
private Set<Book> books = new HashSet<Book>();

Using @JB Nizet advise, thanks!

SECOND - Add FetchType.EAGER in end links

@ManyToMany(mappedBy = "posts", fetch = FetchType.EAGER)
@JsonBackReference

Thanks for all.

Community
  • 1
  • 1
larntin
  • 110
  • 1
  • 10