如何在Spring Data REST中创建具有唯一多对多关系的资源?

时间:2015-03-01 18:34:56

标签: rest jpa spring-data spring-data-rest

我使用Spring Boot并尝试使用Spring Data REST实现实体之间的多对多关系,其中不会创建依赖Author实体的重复项。

鉴于以下代码,当我发布一本初始书籍时,与作者一起创建作者,创建书籍,并创建关系(查找表条目)。当我使用同一作者发布第二本书时,会创建第二本书,并尝试创建第二位作者,由于对作者姓名的唯一约束而导致该作者失败。

我是否期望/想要发生,是否创建了第二本书,并且将第二本书与原作者记录相关联,而不是尝试创建第二个/重复的作者。

@Entity(name = 'book')
class Book {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    long id
    ...
    @ManyToMany(fetch = FetchType.EAGER, cascade = [CascadeType.ALL])
    @JoinTable(name = 'books_authors', joinColumns = @JoinColumn(name = 'book_id'),
        inverseJoinColumns = @JoinColumn(name = 'author_id'))
    @RestResource(exported = false)
    Set<Author> authors
}

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

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

    @Column(nullable = false, unique = true)
    String fullName

    @ManyToMany(mappedBy = "authors")
    Set<Book> books
}

@RepositoryRestResource(collectionResourceRel = "authors", path = "authors")
interface AuthorRepository extends PagingAndSortingRepository<Author, Long> { }

@RepositoryRestResource(collectionResourceRel = "books", path = "books")
interface BookRepository extends PagingAndSortingRepository<Book, Long> { }

$ curl -i -X POST -H "Content-Type:application/json" -d '{ "title" : "Book One",  "authors" { "fullName": "AuthorName"} }' ....
HTTP/1.1 201 Created

$ curl -i -X POST -H "Content-Type:application/json" -d '{ "title" : "Book Two",  "authors" { "fullName": "AuthorName"} }' ....

第二篇文章因尝试创建包含重复fullName的新作者而导致失败。

我使用Spring Data实现了相同的关系,没有Spring Data REST。在其中,我有控制器和服务,并且能够在createBook(Book book)方法中覆盖它。但是使用Spring Data REST,我没有服务可以实现。我尝试在AbstractRepositoryEventListener<Book>#onBeforeCreate()中实现相同的逻辑,但最终会出现分离的实体问题。

这似乎是我应该能够处理的JPA关系定义,但我似乎无法实现。

1 个答案:

答案 0 :(得分:9)

您对REST和Spring Data REST所基于的一些概念略有误解或误用。首先让我解释一下您提供的代码。

基础

由于AuthorBook都由存储库管理,因此它们基本上从普通实体升级为聚合根。 DDD中的聚合根应该在自身内部管理不变量,不能作为操纵另一个聚合的副作用进行调整。这就是为什么Spring Data REST默认为它们公开专用资源并在两者之间创建链接(字面意思)。

我想简要阐述的另一个方面是REST中的身份主题。它实际上非常简单,因为它非常明确。资源具有身份 - 它们的URI(顾名思义)。因此,服务器必须识别身份的唯一方法是通过提供相同的URI。

您的样本

因此,在您的特定示例中,服务器无法判断您发布的第二个Book是指同一作者。基本上因为你没有引用,你进行了内联,这使得Author聚合的Book部分与你的方法相矛盾,从而首先为两者建立了存储库。

如果你仔细想想,你的例子提出了两个问题,甚至使问题复杂化:

  1. 您为什么要第二次转移Author的所有属性?你基本上试图表达的是:&#34;这本书属于我已经创作的作者&#34;您要通过提交第一作者的URI来创建第一作者的URI,作为要创建的第二个author的{​​{1}}属性的值。

  2. 如果第二个内联Book文档也会更改某些属性,应该会发生什么?这基本上与我首先概述的聚合的假设相矛盾,并回到问题1:你想要引用已经存在的东西,而不是重新提交东西。

  3. 建议的步骤

    所以我基本上建议如下:

    1. Author的{​​{1}}媒体资源中移除@RestResource(exported = false)
    2. 提交您已有的第一个请求。
    3. 按照服务器返回的Book标题中提供的链接进行操作。
    4. 按照链接目标中提供的authors链接。
    5. 使用该资源返回的链接,并在后续的创建请求中使用它们来填充Location属性。
    6. 如果您可以在单独的步骤中创建作者,我甚至建议这样做,因为他们立即返回创建作者的URI,以便随后可以在后续使用创作书籍。