Подтвердить что ты не робот

Как поддерживать двунаправленные отношения с Spring Data REST и JPA?

При работе с Spring Data REST, если у вас есть отношение OneToMany или ManyToOne, операция PUT возвращает 200 для "не владеющего" объекта, но фактически не сохраняет присоединенный ресурс.

Пример сущностей:

@Entity(name = 'author')
@ToString
class AuthorEntity implements Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id

    String fullName

    @ManyToMany(mappedBy = 'authors')
    Set<BookEntity> books
}


@Entity(name = 'book')
@EqualsAndHashCode
class BookEntity implements Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id

    @Column(nullable = false)
    String title

    @Column(nullable = false)
    String isbn

    @Column(nullable = false)
    String publisher

    @ManyToMany(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
    Set<AuthorEntity> authors
}

Если вы поддерживаете их с помощью PagingAndSortingRepository, вы можете ПОЛУЧИТЬ Book, PagingAndSortingRepository по ссылке authors в книге и сделать PUT с URI автора, с которым хотите связаться. Вы не можете пойти другим путем.

Если вы выполняете GET для автора и делаете PUT для ссылки на books, ответ возвращает 200, но связь никогда не сохраняется.

Это ожидаемое поведение?

4b9b3361

Ответ 1

TL;DR

Ключ к этому не так много в Spring Data REST - поскольку вы можете легко заставить его работать в вашем сценарии, но убедитесь, что ваша модель поддерживает оба конца ассоциации в синхронизации.

Проблема

Проблема, которую вы видите здесь, связана с тем, что Spring Data REST в основном модифицирует свойство books вашего AuthorEntity. Это само по себе не отражает это обновление в свойстве authors BookEntity. Это нужно обрабатывать вручную, что не является ограничением, которое Spring Data REST составляет, но то, как JPA работает в целом. Вы сможете воспроизвести ошибочное поведение, просто запустив сеттеры вручную и пытаясь сохранить результат.

Как это решить?

Если удаление двунаправленной связи не является вариантом (см. ниже, почему я рекомендую это), единственный способ сделать эту работу - убедиться, что изменения в ассоциации отражаются с обеих сторон. Обычно люди заботятся об этом, вручную добавляя автора к BookEntity при добавлении книги:

class AuthorEntity {

  void add(BookEntity book) {

    this.books.add(book);

    if (!book.getAuthors().contains(this)) {
       book.add(this);
    }
  }
}

Дополнительное предложение if должно быть добавлено также на стороне BookEntity, если вы также хотите, чтобы изменения с другой стороны распространялись. if в основном требуется, так как в противном случае два метода будут постоянно называть себя.

Spring Data REST, по умолчанию использует доступ к полю, так что на самом деле нет метода, в который вы можете включить эту логику. Один из вариантов заключается в том, чтобы переключиться на доступ к свойствам и поместить логику в сеттеры. Другой вариант - использовать метод, аннотированный с помощью @PreUpdate/@PrePersist, который выполняет итерации над объектами и гарантирует, что изменения будут отражены с обеих сторон.

Удаление основной причины проблемы

Как вы можете видеть, это добавляет довольно много сложности в модель домена. Как я шутил на Twitter вчера:

# 1 правило двунаправленных ассоциаций: не используйте их...:)

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

Хорошая эвристика, чтобы определить, с какой стороны вырезать, подумать о том, какая сторона ассоциации действительно является основной и решающей для области, которую вы моделируете. В вашем случае я бы сказал, что для писателя это совершенно нормально, поскольку нет книг, написанных ею. С другой стороны, книга без автора не имеет особого смысла. Поэтому я бы сохранил свойство authors в BookEntity, но в BookRepository ввел следующий метод:

interface BookRepository extends Repository<Book, Long> {

  List<Book> findByAuthor(Author author);
}

Да, для этого требуется, чтобы все клиенты, которые ранее могли просто вызвать author.getBooks(), теперь работать с репозиторием. Но с положительной стороны вы удалили весь треск из ваших объектов домена и создали путь от явной зависимости от книги к автору. Книги зависят от авторов, но не наоборот.

Ответ 2

Я столкнулся с аналогичной проблемой, отправив POJO (содержащий двунаправленное отображение @OneToMany и @ManyToOne) как JSON через REST api, данные сохранялись как в родительском, так и в дочернем объектах, но отношение внешних ключей не было установлено, Это происходит потому, что двунаправленные ассоциации необходимо поддерживать вручную.

JPA предоставляет аннотацию @PrePersist, которая может использоваться, чтобы удостовериться, что метод, аннотированный с ним, выполняется до того, как объект сохраняется. Поскольку JPA сначала вставляет родительский объект в базу данных, за которой следует дочерний объект, я включил метод, аннотированный с помощью @PrePersist, который будет перебирать список дочерних объектов и вручную установить для него родительский объект.

В вашем случае это будет примерно так:

class AuthorEntitiy {
    @PrePersist
    public void populateBooks {
        for(BookEntity book : books)
            book.addToAuthorList(this);   
    }
}

class BookEntity {
    @PrePersist
    public void populateAuthors {
        for(AuthorEntity author : authors)
            author.addToBookList(this);   
    }
}

После этого вы можете получить бесконечную ошибку рекурсии, чтобы избежать аннотации родительского класса @JsonManagedReference и вашего дочернего класса с помощью @JsonBackReference. Это решение сработало для меня, надеюсь, оно тоже сработает для вас.