web-dev-qa-db-ja.com

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を取得し、本のauthorsリンクをたどって、関連付ける著者のURIでPUTを実行できます。逆に行くことはできません。

著者に対してGETを実行し、そのbooksリンクに対してPUTを実行すると、応答は200を返しますが、関係は永続化されません。

これは予想される動作ですか?

27
keaplogik

tl; dr

その鍵は、Spring Data REST-シナリオで簡単に機能させることができるため、それほど重要ではありませんが、モデルが関連付けの両端を同期させるようにしてください。

問題

ここに表示される問題は、Spring Data RESTが基本的にbooksAuthorEntityプロパティを変更するという事実から発生します。それ自体は、この更新を反映していませんauthorsBookEntityプロパティ。これは手動で回避する必要があります。これは、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は基本的に必須です。それ以外の場合、2つのメソッドは常に自分自身を呼び出します。

Spring Data RESTはデフォルトでフィールドアクセスを使用するため、このロジックを挿入できるメソッドは実際にはありません。 1つのオプションは、プロパティアクセスに切り替えて、ロジックをセッターに配置することです。別のオプションは、_@PreUpdate_/_@PrePersist_で注釈が付けられたメソッドを使用して、エンティティを反復処理し、変更が両側に反映されるようにすることです。

問題の根本原因を取り除く

ご覧のように、これによりドメインモデルがかなり複雑になります。昨日ツイッターで冗談を言ったように:

#1双方向関連付けのルール:使用しないでください…:)

通常、可能な場合は双方向の関係を使用せずに、リポジトリにフォールバックして関連付けの裏側を構成するすべてのエンティティを取得しようとすると、問題が簡単になります。

どちらの側をカットするかを決定するための優れたヒューリスティックは、アソシエーションのどちらの側が本当にコアであり、モデリングしているドメインにとって重要であるかを考えることです。あなたの場合、著者が書いた本がない状態で著者が存在することはまったく問題ないと主張します。反対に、著者のいない本はあまり意味がありません。したがって、authorsプロパティをBookEntityに保持しますが、BookRepositoryに次のメソッドを導入します。

_interface BookRepository extends Repository<Book, Long> {

  List<Book> findByAuthor(Author author);
}
_

はい、そのためには、以前はauthor.getBooks()を呼び出すことができたすべてのクライアントが、リポジトリを操作する必要があります。しかし、良い面は、ドメインオブジェクトからすべての残骸を取り除き、途中で本から著者への明確な依存関係の方向性を作成したことです。本は著者に依存しますが、その逆はありません。

39
Oliver Drotbohm

同様の問題に直面し、POJO(双方向マッピング@OneToManyおよび@ManyToOneを含む)をREST apiを介してJSONとして送信すると、データは親エンティティと子エンティティの両方に保持されましたが、外部キー関係が確立されていません。これは、双方向の関連付けを手動で維持する必要があるために発生します。

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を付けないようにするために、無限再帰エラーが発生する可能性があります。この解決策は私にとってうまくいきました。うまくいけば、あなたにとってもうまくいくでしょう。

7
Vidhi Gupta