ZeroOne
ZeroOne

Reputation: 3171

Hibernate not saving parent ids for OneToMany children

I'm trying to replicate the Hibernate example from here in my own project using H2 database -- initialized using Flyway migration -- and Spring Data. The problem is that when I save the parent object, the BookCategory, it also saves the childen (Book objects), but it does not save the book_category_id, i.e. the "parent id" of the "child" objects!

My SQL looks like this:

CREATE SEQUENCE SEQ_IDNUMS START WITH 1;

CREATE TABLE book_category (
  id int(11) NOT NULL,
  name varchar(255) NOT NULL,
  PRIMARY KEY (id,name)
);

CREATE TABLE book (
  id int(11) NOT NULL,
  name varchar(255) DEFAULT NULL,
  book_category_id int(11) DEFAULT NULL
);
alter table book add constraint pk_book_id PRIMARY KEY (id);
alter table book add constraint fk_book_bookcategoryid FOREIGN KEY(book_category_id) REFERENCES book_category(id) ON DELETE CASCADE;

Then, my Book class:

import javax.persistence.*;

@Entity
public class Book{
  private int id;
  private String name;
  private BookCategory bookCategory;

  public Book() { }

  public Book(String name) { this.name = name; }

  @Id
  @SequenceGenerator(name = "seq_idnums", sequenceName = "seq_idnums", allocationSize = 1)
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_idnums")
  public int getId() { return id; }

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

  public String getName() { return name; }

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

  @ManyToOne
  @JoinColumn(name = "book_category_id")
  public BookCategory getBookCategory() { return bookCategory; }

  public void setBookCategory(BookCategory bookCategory) { this.bookCategory = bookCategory; }
}

And my BookCategory class:

import java.util.Set;

import javax.persistence.*;

@Entity
@Table(name = "book_category")
public class BookCategory {
  private int id;
  private String name;
  private Set<Book> books;

  public BookCategory(){ }

  public BookCategory(String name, Set<Book> books) {
    this.name = name;
    this.books = books;
  }

  @Id
  @SequenceGenerator(name = "seq_idnums", sequenceName = "seq_idnums", allocationSize = 1)
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_idnums")
  public int getId() { return id; }

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

  public String getName() { return name; }

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

  @OneToMany(mappedBy = "bookCategory", cascade = CascadeType.ALL)
  public Set<Book> getBooks() { return books; }

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

As for the saving part, I'm doing it exactly as in the example:

BookCategory categoryA = new BookCategory("Category A", new HashSet<Book>(){{
  add(new Book("Book A1"));
  add(new Book("Book A2"));
  add(new Book("Book A3"));
}});

BookCategory categoryB = new BookCategory("Category B", new HashSet<Book>(){{
  add(new Book("Book B1"));
  add(new Book("Book B2"));
  add(new Book("Book B3"));
}});

bookCategoryRepository.save(new HashSet<BookCategory>() {{
  add(categoryA);
  add(categoryB);
}});

Finally, my BookCategoryRepository looks only like this, thanks to Spring Data magic:

import org.springframework.data.repository.CrudRepository;

public interface BookCategoryRepository extends CrudRepository<BookCategory, Long> {
}

This question has been asked a few times, such as here. There, the accepted answer basically says that the side with the @JoinColumn is the side that should be persisted. As you'll see in the code above, the example that I'm replicating does not do that, but instead persists the other side, and that apparently worked fine for the author. If I do try to persist the Book object instead, I get this exception:

[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : Book.bookCategory -> BookCategory; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : Book.bookCategory -> BookCategory] with root cause org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : Book.bookCategory -> BookCategory

...so that's not the solution.

Now, another question here and its answer suggest that the issue is that the id column must be nullable. That, however, leads to a Flyway error then, and I really wouldn't want to have my id fields as nullable anyway:

Invocation of init method failed; nested exception is org.flywaydb.core.internal.dbsupport.FlywaySqlScriptException: Error executing statement at line 14: alter table book add constraint pk_book_id PRIMARY KEY (id)

Yet another question here suggests that "nullable = false" must be removed, but I don't have that in the first place.

I'm completely at loss. What can there possibly be wrong and how to fix it?

Upvotes: 2

Views: 7223

Answers (2)

With your current mapping @ManyToOne is the owner but while creating a new book object using new Book("Book A1") but not setting bookCategory field (owner) in this new book object.

Can you set that field and try saving it.

Tested with below code and able to save using session.save(categoryA) directly instead of repository. But that shouldn't be any different.

BookCategory categoryA = new BookCategory("Category A");
categoryA.setBooks(new HashSet<Book>(){{
                  add(new Book("Book A1", categoryA));
                  add(new Book("Book A2", categoryA));
                  add(new Book("Book A3", categoryA));
                }});

And as you might have guessed, to set the bookCategory field I have added a new constructors to the classes as below to keep it simple. You might use setters as well.

public BookCategory(String name) {
        this.name = name;
      }

  public Book(String name, BookCategory category) {
      this.name = name;
      this.bookCategory = category;
  }

Upvotes: 5

Teemu Ilmonen
Teemu Ilmonen

Reputation: 316

I believe your problem is that the save-operation is not cascading from Book to BookCategory. BookCategory is an unsaved transient instance, so you must either explicitly save it before saving the books or then add cascade option to Book.

Try this:

@ManyToMany(cascade=CascadeType.ALL)
@JoinColumn(name = "book_category_id")
public BookCategory getBookCategory() { return bookCategory; }

Edit: I think I misread the problem. On that note: your copy-paste did not include the start of your test-method; are you inside a @Transactional method like the original example? It seems like you followed it to the letter and everything should be working.

Upvotes: 0

Related Questions