如何在 REST 端点中发布具有关系的实体 - Kotlin Spring Data

Posted

技术标签:

【中文标题】如何在 REST 端点中发布具有关系的实体 - Kotlin Spring Data【英文标题】:How to POST entity with relationships in REST endpoint - Kotlin Spring Data 【发布时间】:2021-08-06 04:49:15 【问题描述】:

我跟随 this tutorial 使用 Spring Boot 在 Kotlin 中创建了一个基本的 Web 应用程序。但是,我无法发布与现有资源具有多对一关系的新实体。

我的代码:

@Entity
class Song(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,
    var title: String,
    @ManyToOne(fetch = FetchType.EAGER)
    var addedBy: User)

@Entity
class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,
    var email: String,
    var displayName: String)
@RestController
@RequestMapping("/api/songs")
class SongController(private val repository: SongRepository) 

    @PostMapping("/")
    fun add(@RequestBody song: Song) =
        repository.save(song)

This answer 和其他人指出您可以使用其 URI 引用另一个资源,但发送以下请求:


  "title": "Some title",
  "addedBy": "http://localhost:8080/api/users/1"

给我一​​个堆栈跟踪错误org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of 'com.example.springboot.User' (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/api/users/1'); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of 'com.example.springboot.User' (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/api/users/1')\n at [Source: (PushbackInputStream); line: 6, column: 13] (through reference chain: com.example.springboot.Song[\"addedBy\"])

我发现在 Jackson/Hibernate/Spring Data 之间的某个地方,它无法将 User 资源 URI 转换为 User 实体,但我不知道应该在哪里发生这种魔法。

这似乎是 Kotlin 特有的问题。这里关于 SO 的所有建议都不能解决这个特定的错误,并且教程本身就没有处理关系。如果以这种方式处理关系根本不是正确的方法,我很想知道首选的做法是什么。

【问题讨论】:

您究竟将什么作为正文发布到端点?异常已经告诉你出了什么问题。 Song 对象(例如 addedBy 属性)不能仅基于提供的字符串反序列化。您至少需要用户的子对象 json 表示此外,您不应直接将实体发布到控制器。使用值对象,然后将它们映射到实体。所以你需要创建一个 Song 的实例。根据add by,你需要获取对应的用户实体,并将该实体添加到歌曲中。您也可以只提供用户 ID,然后提供映射。 @Daniel 我正在发送如上所述的请求正文(第三个代码块)还是您的意思是别的?直接发送实体而不必使用值对象正是 Spring 教程所做的,并且通过使用资源 URI 引用其他实体应该可以与 Spring 一起使用 OOB,对吗?另请参阅他们在这里做了什么,但使用 PUT 代替 baeldung.com/spring-data-rest-relationships 嘿 Steven,本教程使用 HATEOAS。使用"books" : "href" : "http://localhost:8080/authors/1/books" 查看他们引用相应子实体的请求正文,这意味着您还应该将此模式应用于您的请求。否则这将不起作用。 HATEOAS 允许您通过相应的资源路径直接引用相关的子实体,但您需要保留您发布的请求正文缺少的必要结构。 谢谢@DanielWosch,我似乎一直在挑选我在问题中提到的那些实际上使用Spring Data REST(我不是)的例子,我想这就是我的混乱来自。 Spring Data REST 似乎确实遵循 HATEOAS,我将尝试在我的项目中使用它。再次感谢! 我已经为您的问题发布了相应的答案,因此您可以接受它;) 【参考方案1】:

本教程使用HATEOAS。使用

查看他们引用相应子实体的请求正文
"books" :        "href" : "http://localhost:8080/authors/1/books"     

这意味着您还应该将此模式应用于您的请求。否则这将不起作用。 HATEOAS 允许您通过相应的资源路径直接引用相关的子实体,但您需要保留您发布的请求正文缺少的必要结构。此外,您必须在您的 WebService / WebApi / Spring Boot 项目中支持 HATEOAS。

你能做什么:


  "title": "Some title",
  "addedByUserId": "1"

然后

@PostMapping("/")
    fun add(@RequestBody song: Song) =
        val userEntity = userRepository.findById(song.getAddedByUserId())
        Song newSong = new SongEntity();
        // map props
        newSong.setUser(userEntity)
        repository.save(song)

该代码不起作用,但我希望您能理解。

进一步

在您的代码中,您将请求正文视为实体。请考虑将您的传入类和实体类分开。这将使一些事情变得更容易。

【讨论】:

使用 Spring Data REST 支持 HATEOAS 是使其工作的关键。对于发布具有多对一关系的实体,我现在可以使用 "title": "Some title", " addedBy": "localhost:8080/api/users/1" 。谢谢! 关于您最后一句话的一个问题:Spring Data REST 不是通过让您在更喜欢使用中间对象时直接发布和检索实体来促进不良做法吗? 这取决于:使用 HATEOAS 框的 auto 似乎除了直接使用与真实实体的关系之外别无他法。但说实话:在实践中我对 HATEOAS 并不熟悉。【参考方案2】:

我认为您缺少 jackson 的 kotlin 模块,这正是它的创建目的。

https://github.com/FasterXML/jackson-module-kotlin

只需在您的项目中添加此依赖项将导致 spring 使用此新模块自动配置您的对象映射器。如果你有一个带有你自己创建的 objectMapper 的 Bean,那么你需要手动配置它,在模块的 README.md 中有一个关于这个的部分

【讨论】:

感谢 Yayotrón,我已经加载了这个依赖项,但它似乎没有任何区别。但是,开箱即用的 Spring Boot 似乎不支持这种发布关系格式,我在网上找到的示例似乎专门用于 Spring Data REST。我将了解如何在我的项目中实现这一点。

以上是关于如何在 REST 端点中发布具有关系的实体 - Kotlin Spring Data的主要内容,如果未能解决你的问题,请参考以下文章

来自非实体对象的 REST 端点

如何在 Spring Data Rest 中的两个实体之间发布和 PUT 关系@OneToMany / @ManyToOne?

具有授权的 Keycloak 自定义 SPI REST 端点

Spring Rest API 验证应该在 DTO 中还是在实体中?

如何在Django Rest Framework中序列化三个具有多对多关系的模型

具有多个标识符的 REST 端点