Гибернация — JPA — OneToMany и CascadeType.ALL

#java #spring #hibernate #jpa #cascade

#java #весна #спящий режим #jpa #каскад

Вопрос:

У меня проблема с JPA с @OneToMany и, в частности, с функциональностью CascadeType.ALL. У меня есть две сущности, отец и потомок.

Я показываю вам пример того, что у меня есть: КНИГА

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

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY)
    @JoinColumn(name="id_page")
    private List<Page> pages;
    ...
    ...
}
  

Страница

 @Entity
@Table(name = "pages")
public class Page implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    @Column(name = "id")
    private Integer id;

    @Column(name = "id_book")
    private int idBook;
    ...
    ...
}
  

Итак, у меня просто есть книга и несколько страниц. Когда я сохраняю книгу, я также сохраняю страницы, содержащиеся в ней, благодаря CascadeType.ALL.
Который работает, на самом деле Hibernate пытается сохранить объект Book, понимает с помощью OneToMany, что есть также объект страницы для сохранения, и запускает запрос благодаря Cascade ALL.

Я просто делаю:

 bookRepository.save(book);
  

JSON, представляющий книгу, которую я хочу сохранить, выглядит примерно так:

 {
  "id": 0,
  "page": [
    {
      "id": 0,
      "idBook": 0
    }
  ]
}
  

Но, если я добавлю 0 к идентификатору книги, она будет создана. Если я добавлю 0 в idBook на страницу, это выдаст мне ошибку, подобную этой: ключ (id_book) = (0) отсутствует в таблице «книга».

Но, чего я действительно не понимаю: почему, если я помещаю id_book в json, который существует в БД, затем завершаю сохранение объекта book, он понимает, что это за идентификатор, и правильно вставляет его в БД?

Итак, если я дам этот JSON.

 {
  "id": 0,
  "page": [
    {
      "id": 0,
      "idBook": 1
    }
  ]
}
  

И давайте рассмотрим, что в БД есть только одна книга с идентификатором=1.
После сохранения он генерирует мне строку с идентификатором = 2 для книги и страницу, связанную с книгой с идентификатором = 2?

Эта штука сводит меня с ума. Если дать idBook = 0, чтобы сказать ему, что он должен получить / сгенерировать его, он говорит мне, что он не существует. Если, с другой стороны, я даю ему idBook, который действительно существует (но это неверно), он даже не рассматривает это и правильно использует idBook книги, которую он только что создал?

Я бы просто хотел, чтобы мне не приходилось использовать поддельный идентификатор, скажите ему, что он должен использовать идентификатор страницы после создания книги, точно так же, как он это делает на самом деле.

Комментарии:

1. Итак, вы передаете Page и сохраняете Book ?

2. @Andronicus нет, я передаю книгу со страницей (или, может быть, несколькими страницами) для сохранения книги и страницы. Две разные сущности с одним сохранением благодаря CascadeType в OneToMany.

Ответ №1:

Из наличия id_book столбца в Page сущности я делаю вывод, что вы хотели бы использовать двунаправленное отображение. Попробуйте сделать это так, с дочерним элементом в качестве владельца отношения:

 @Entity
@Table(name = "pages")
public class Page implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    @Column(name = "id")
    private Integer id;

    @ManyToOne(fetch = FetchType.LAZY) // EAGER by default
    @JoinColumn(name = "id_book")
    private Book book;
    ...
    ...
}
  
 @Entity
@Table(name = "books")
public class Book implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @OneToMany(mappedBy = "book", cascade = CascadeType.ALL) // Lazy fetch type by default
    private List<Page> pages = new ArrayList<>();
    ...
    ...
    
    // obligatory synchronization methods
    public void addPage(Page page) {
        pages.add(page);
        page.setBook(this);
    }

    public void removePage(Page page) {
        pages.remove(this);
        page.setBook(null);
    }
}
  

Ответ №2:

Учитывая, что «я передаю книгу со страницей (или, может быть, несколькими страницами)» Я также хотел бы поделиться своим решением.

Немного похоже на другие ответы, я также использую двунаправленный. Я также включил интеграционный тест.

Объект Book

 @Entity
public class Book {

    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "uuid2")
    private String id;

    @OneToMany(mappedBy = "book", cascade = { CascadeType.PERSIST, CascadeType.MERGE })
    private List<Page> pages;

    public Book(String id, List<Page> pages) {
        this.id = id;
        this.pages = pages;
        this.pages.stream().forEach(page -> page.attachedTo(this));
    }

    public String getId() {
        return id;
    }

    public List<Page> getPages() {
        return pages;
    }

    // Required by JPA
    protected Book() {
    }

}
  

Объект страницы

 @Entity
public class Page {

    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "uuid2")
    private String id;

    private String content;

    @ManyToOne
    private Book book;

    public Page(String content) {
        this.content = content;
    }

    void attachedTo(Book book) {
        this.book = book;
    }

    public String getId() {
        return id;
    }

    public Book getBook() {
        return book;
    }

    // Required by JPA
    protected Page() {}
}
  

Интеграционный тест

 @Test
@Transactional
public void saveBookWithPages() {
    List<Page> firstBookPages = new ArrayList<>();
    firstBookPages.add(new Page("content-0001"));
    firstBookPages.add(new Page("content-0002"));
    firstBookPages.add(new Page("content-0003"));

    Book firstBook = new Book("first-book", firstBookPages);
    firstBook = bookRepository.save(firstBook);

    List<Page> secondBookPages = new ArrayList<>();
    secondBookPages.add(new Page("content-0004"));
    secondBookPages.add(new Page("content-0005"));

    Book secondBook = new Book("second-book", secondBookPages);
    secondBook = bookRepository.save(secondBook);

    // query the first and second book
    firstBook = bookRepository.findById(firstBook.getId()).orElseThrow(() -> new IllegalArgumentException("book id not found."));
    secondBook = bookRepository.findById(secondBook.getId()).orElseThrow(() -> new IllegalArgumentException("book id not found"));

    Assert.assertEquals(3, firstBook.getPages().size());  // check if first book has 3 pages
    Assert.assertEquals(2, secondBook.getPages().size()); // check if second book has 2 pages

    // query the first page of second book
    Page secondBookFirstPage = pageRepository.findById(secondBook.getPages().get(0).getId()).orElseThrow(() -> new IllegalArgumentException(""));

    Assert.assertEquals(secondBook.getId(), secondBookFirstPage.getBook().getId());  // check if the page is correctly associated to the book
}