Home > OS >  Spring JPA posting ManyToOne parent entity
Spring JPA posting ManyToOne parent entity

Time:02-28

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!

CodePudding user response:

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.

  • Related