Spring JPA 发布 ManyToOne 父实体

发布于 2025-01-10 19:40:45 字数 1993 浏览 0 评论 0原文

情况如下: Work 对象,用户可以执行的一些工作。用户还可以签署作品。所以一个非常基本的工作对象:

class Work (
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var workId: Int,
    var userId: Int,
    ...
    var flags: Int,
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "signTargetUserId")
    var signTargetUser: User?,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "signedUserId")
    var signedUser: User?
)

User 有一些东西,这个和那个......:

class User(
    @Id
    val userId: Int,
    val username: String,
    val name: String,
    val email: String,
    @JsonIgnore
    val password: String,
    val flags: Int,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "roleId")
    var role: Role
)

告诉 JPA 用户角色关系非常方便,我希望我们同意这一点。当我获得用户时,我也获得了角色。工作和用户的关系也很好,我喜欢。我得到一个工作对象,我知道谁应该在它上签字(signTargetUser),而且我也知道这个用户角色。现在,有一个控制器来检索一个或多个工作对象,它们的返回类型自然是 WorkList。必须有一个函数来处理发布新的 Work 实体。我在某处读过,在 REST API 中,如果往返的实体相同,那就太好了。那么发布作品需要什么样的结构呢?与查询时得到的结果相同。那:

    fun createWork(@RequestBody work: Work, authentication: Authentication): ResponseEntity<Any> {

这非常方便,我真的很喜欢,只是它根本不起作用。现在,请求正文必须是一个有效的 Work 对象,这听起来很不错,需要像毫秒一样进行验证,但事实并非如此。它需要 2 个 User 对象,一个位于signTargetUser,一个位于signedUser。更糟糕的是,这些用户必须挂有 Role 对象。更糟糕的是,User 对象必须具有非空密码属性。显然我什至不知道,现在事情已经失控了。我想要做的就是插入一个 Work 对象,并知道signTargetUserId(如果有)。 我看到了一个分两步的解决方案,但我根本不喜欢它:

  1. 首先,我需要创建另一个类 (WorkIncoming),仅用于描述帖子中传入的对象的结构,但是这次没有多对一关系。但当你读回你创作的作品时,你会得到不同的结构。 WorkIncoming 输入和Work 输出。更不用说半无用的新类,它大多是第一个类的糟糕重复。如果我添加或更改一个字段,是否需要更改 2 个文件?严重地?
  2. 显然,原始存储库正在处理 Work 类,因此它将无法处理 WorkIncoming 类型:新存储库。再说一遍,代码完全重复,维护等等。我现在甚至不确定 JPA 对引用同一张表的 2 个实体有何感受。

那么这里真正的解决方案是什么?真实的人是如何做到这一点的?我在这里没有主意了。我刚才描述的这个解决方案技术含量非常低,不可能!

here's the situation: Work object, some work a user can do. Users can also sign off on a work. So a very basic work object:

class Work (
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var workId: Int,
    var userId: Int,
    ...
    var flags: Int,
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "signTargetUserId")
    var signTargetUser: User?,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "signedUserId")
    var signedUser: User?
)

The User has a couple of things, this and that...:

class User(
    @Id
    val userId: Int,
    val username: String,
    val name: String,
    val email: String,
    @JsonIgnore
    val password: String,
    val flags: Int,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "roleId")
    var role: Role
)

Telling JPA about the user-role relationship is very handy, I hope we agree in that. When I get the user, I get the role too. The work-user relationship is also very nice, I like it. I get a work object, I know who's supposed to sign off on it (signTargetUser), and I also know this users role. Now, there's a controller to retrieve one or many work objects, and naturally the return type of these are Work or List<Work>. There has to be a function dealing with posting new Work entities. I have read it somewhere, that in a REST API it's nice if the entities travelling to and from are the same. So what structure does one have send to post a work? The same as one gets when querying one. Na then:

    fun createWork(@RequestBody work: Work, authentication: Authentication): ResponseEntity<Any> {

This is very handy, I really like that, except that it doesn't work at all. Now the request body must be a valid Work object that sounds sweet with the validation for like a ms, but then it doesn't. It requires 2 User objects, one at signTargetUser and one at signedUser. To make it worse, these users must have Role objects hanging on them. To make it even worse, the User objects must have a non-null password property. Obviously I don't even know that, this is now way out of hand. All I want to do is insert a Work object, knowing the signTargetUserId if any.
I see a solution in 2 steps, but i don't like it at all:

  1. First I need to create another class (WorkIncoming) only to describe the structure of the object coming in in the post, but this time without the ManyToOne relationships. But then when you read back the work you created, you'd get a different structure. WorkIncoming in and Work out. Not to mention the semi-useless new class that would mostly be a bad repetition of the first one. If I add or change a field, do i need to change 2 files? Seriously?
  2. Obviously the original repository is dealing with Work classes, so it won't be able to handle WorkIncoming type: new repository then. Again, total code duplication, maintenance blah blah. I'm not even sure now how the JPA would feel about 2 entities referencing the same table.

So what's the real solution here? How do real people do that? I'm out of ideas here. This solution that I have just described is terribly low-tech, that can't be it!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

韵柒 2025-01-17 19:40:45

最常见的方法是使用 DTO 对象来进行请求和响应。
应使用包含所有工作字段的 DTO 名称 - workId、类型、标志等以及 signTargetUserId。 DTO 字段名称应与实体的字段名称相同,以便于映射。

class WorkDTO (
    var userId: Int,
    ...
    var flags: Int,
    var signTargetUserId: Int
)

之后,您可以使用signTargetUserId从userRepository获取用户对象。

User user = this.usersRepository.findById(signTargetUserId);

这样,您就不需要 Role 对象,因为它已经存在于用户对象中。

然后在您的服务中,您可以使用许多映射器库(如 ModelMapper、MapStruct、JMapper、Orika、Dozer 等)将 DTO 映射到工作实体。
请记住将上面创建的用户对象也传递给映射器。

只是一个示例(此示例使用模型映射器,但您可以使用您选择的映射器):

public Work convertDtoToEntity( WorkDTO workDto, User user) {

        this.modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
        Work work = modelMapper.map(workDto, Work.class);
        work.setSignTargetUser(user);
        return work;
}

然后只需使用 workRepository 的 save 方法。

另外,回答你的第二个问题,永远不应该为 DTO 类创建存储库。

PS:这只是编写服务的方法之一。您可能会发现其他一些方法更适合您的编码风格。

The most common way is to use a DTO object for requests and responses.
One should use a DTO name that would contain all the work fields - workId, type, flags, etc along with the signTargetUserId. DTO field names should be same as that of the entity for easy mapping.

class WorkDTO (
    var userId: Int,
    ...
    var flags: Int,
    var signTargetUserId: Int
)

After that, you get the user Object from userRepository using signTargetUserId.

User user = this.usersRepository.findById(signTargetUserId);

This way, you won't need a Role object as it would already be present in the user object.

Then in your service, you can use many mapper libraries like ModelMapper, MapStruct, JMapper, Orika, Dozer, etc to map your DTO to Work Entity.
Remember to pass the user object created above to the mapper as well

Just an example (This example is with Model Mapper, but you can use mapper of your choice):

public Work convertDtoToEntity( WorkDTO workDto, User user) {

        this.modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
        Work work = modelMapper.map(workDto, Work.class);
        work.setSignTargetUser(user);
        return work;
}

Then simply use the save method of workRepository.

Also, answering your second question, one should never create a repository for DTO classes.

PS: This is just one of the ways to write a service. There might be some other ways that you might find more aligned with your style of coding.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文