java 如何维护与Spring Data REST和JPA的双向关系?

nfzehxib  于 2023-01-24  发布在  Java
关注(0)|答案(4)|浏览(124)

使用Spring Data REST时,如果您具有OneToManyManyToOne关系,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来关联,你不能走另一条路。
如果对Author执行GET操作,并对其books链接执行PUT操作,响应将返回200,但关系永远不会持久化。
这是预期行为吗?

kg7wmglp

kg7wmglp1#

TL;医生

关键不在于Spring Data REST中的任何东西--因为您可以很容易地让它在您的场景中工作--而在于确保您的模型保持关联的两端同步。
∮问题是
您在这里看到的问题是由于Spring Data REST基本上修改了AuthorEntitybooks属性,而这本身并没有在BookEntityauthors属性中反映出这个更新,因此必须手动解决这个问题。这并不是SpringDataREST构成的约束,而是JPA的一般工作方式。您只需手动调用setter并尝试持久化结果,就可以重现错误行为。

如何解决这个问题?

如果删除双向关联不是一个选项(见下面我为什么建议这样做),唯一的方法是确保对关联的更改反映在双方,通常人们会在添加书籍时手动将作者添加到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默认使用字段访问,因此实际上没有方法可以放入此逻辑。一种选择是切换到属性访问并将逻辑放入setter。另一种选择是使用带有@PreUpdate/@PrePersist注解的方法,该方法迭代实体并确保修改反映在两端。

消除问题的根本原因

正如您所看到的,这给域模型增加了相当多的复杂性,正如我昨天在Twitter上开玩笑说的:
双向关联的第一条规则:不要使用它们...:)
如果您尽可能不使用双向关系,而是退回到存储库来获取构成关联背面的所有实体,那么问题通常会得到简化。
一个很好的启发式方法来决定哪一面被删除,那就是考虑哪一面的关联对你所建模的领域来说是真正的核心和关键。在你的例子中,我认为一个作者没有写过书也是完全可以的。另一方面,一本没有作者的书没有什么意义,所以我会保留BookEntity中的authors属性,但在BookRepository上引入以下方法:

interface BookRepository extends Repository<Book, Long> {

  List<Book> findByAuthor(Author author);
}

是的,这要求所有客户端以前只能调用author.getBooks()来使用存储库。但从积极的方面来说,您已经从域对象中删除了所有的繁琐工作,并创建了从书籍到作者的明确依赖关系。书籍依赖于作者,而不是相反。

uplii1fm

uplii1fm2#

我也遇到过类似的问题,当我通过REST API以JSON格式发送POJO(包含双向Map@OneToMany和@ManyToOne)时,数据在父实体和子实体中都被持久化了,但外键关系没有建立,这是因为需要手动维护双向关联。
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注解你的子类。这个解决方案对我有效,希望对你也有效。

83qze16e

83qze16e3#

这个链接有一个非常好的教程,告诉你如何解决递归问题:Bidirectional Relationships
我能够使用@JsonManagedReference和@JsonBackReference,它的工作非常有魅力

8wtpewkr

8wtpewkr4#

我相信也可以通过添加一个@BeforeLinkSave处理程序来使用@RepositoryEventHandler来交叉链接实体之间的双向关系。

@Component
@RepositoryEventHandler
public class BiDirectionalLinkHandler {

    @HandleBeforeLinkSave
    public void crossLink(Author author, Collection<Books> books) {
        for (Book b : books) {
            b.setAuthor(author);
        }
    }
}
  • 注意 *:@HandlBeforeLinkSave是基于第一个参数调用的,如果在Author类的等效类中有多个关系,则第二个参数应为Object,并且需要在方法中测试不同的关系类型。

相关问题