Home > Blockchain >  Spring boot Restful API: DTO with relationships convert to entity using ModelMapper?
Spring boot Restful API: DTO with relationships convert to entity using ModelMapper?

Time:11-20

I'm now confused about how to do a CRUD in a Rest API with Spring.

Let me explain, I have two routes to POST and PUT an entity. I created two DTOs createPostRequest and updatePostRequest for this. Because when adding, the properties cannot be null, while when updating they can (nulled properties are ignored).

Problem 1:

On my frontend, the user is asked to choose a list of tags from the database (multi select html). This is why createPostRequest has a tags property typed TagDTO. But, how can I use modelMapper to map, for example, the createPostRequest to the Post entity making sure that the tags exist in the database?

if for example a user try to insert a tag that does not exist, I was thinking of doing something like this:

postEntity.setTags(tagService.findAllByIds(postEntity.getTagsId()));

This makes a lot of repetition in the code, because between create and update method of my entity in service, there is a lot of identical code.

Problem 2:

Based on my problem 1, how can I easily map my two DTOs to the same entity without repeating the code 2x?

Code example - PostService (see comment)

This is an example for the update, but there will be almost identical code for the create, so how do I proceed?

@Transactional
public Post update(Integer postId, UpdatePostRequest request) {
    return Optional.ofNullable(this.getById(postId)).map(post -> {
      // here how to map non-null properties of my request 
            // into my post taking in consideration my comment above?   

      postDAO.save(post);

      return post;
    }).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}

================================

UPDATE:

As requested, found the code bellow.

The controller:

@RestController
@RequestMapping("/v1/posts")
public class PostController {
    RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json; charset=UTF-8")
    public ResponseEntity<Object> update(@Valid @RequestBody CreatePostRequest createPostRequest) {
        Post post = postService.create(createPostRequest);
        return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
    }

    RequestMapping(value = "/{postId}", method = RequestMethod.PUT, consumes = "application/json", produces = "application/json; charset=UTF-8")
    public ResponseEntity<Object> update(@Valid @RequestBody UpdatePostRequest updatePostRequest, @PathVariable Integer postId) {
        Post post = postService.update(postId, updatePostRequest);
        return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
    }
}

CreatePostRequest :

@Data
public class CreatePostRequest {
    @NotNull
    @Size(min = 10, max = 30)
    private Sting title;

    @NotNull
    @Size(min = 50, max = 600)
    private String description

    @NotNull
    @ValidDateString
    private String expirationDate;

    @NotNull
    private List<TagDTO> tags;

    public List<Integer> getTagIds() {
        return this.getTags().stream().map(TagDTO::getId).collect(Collectors.toList());
    }
}

UpdatePostRequest :

@Data
public class UpdatePostRequest {
    @Size(min = 10, max = 30)
    private Sting title;

    @Size(min = 50, max = 600)
    private String description

    @ValidDateString
    private String expirationDate;

    private List<TagDTO> tags;

    public List<Integer> getTagIds() {
        return this.getTags().stream().map(TagDTO::getId).collect(Collectors.toList());
    }
}

The service :

@Service
@Transactional
public class PostService {
    @Transactional
    public Post create(CreatePostRequest request) {
        ModelMapper modelMapper = new ModelMapper();
        Post post = modelMapper.map(request, Post.class);

        // map will not work for tags : how to check that tags exists in database ?

        return postDAO.save(post);
    }

    @Transactional
    public Post update(Integer postId, UpdatePostRequest request) {
        return Optional.ofNullable(this.getById(postId)).map(post -> {
            ModelMapper modelMapper = new ModelMapper();
            modelMapper.getConfiguration().setSkipNullEnabled(true);
            modelMapper.map(request, post);
            
            // map will not work for tags : how to check that tags exists in database ?
            
            postDAO.save(post);

            return post;
        }).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    }
}

CodePudding user response:

To avoid duplication of two similar DTOs you could use @Validated group validations. This allows you to actively set which validations are to be done on each property. You can read more about this in the following online resource https://www.baeldung.com/spring-valid-vs-validated. You would begin with the creation of two market interfaces:

interface OnCreate {}

interface OnUpdate {}

You can then use these marker interfaces with any constraint annotation in your common DTO:

@Data
public class CreateOrUpdatePostRequest {
    @NotNull(groups = OnCreate.class)
    @Size(min = 10, max = 30, groups = {OnCreate.class, OnUpdate.class})
    private Sting title;

    @NotNull(groups = OnCreate.class)
    @Size(min = 50, max = 600, groups = {OnCreate.class, OnUpdate.class})
    private String description

    @NotNull(groups = OnCreate.class)
    @ValidDateString(groups = {OnCreate.class, OnUpdate.class})
    private String expirationDate;

    @NotNull(groups = OnCreate.class)
    private List<TagDTO> tags;

    public List<Integer> getTagIds() {
        return this.getTags().stream().map(TagDTO::getId).collect(Collectors.toList());
    }
}

Finally, you just need to annotate your methods in the Controller accordingly:

@RestController
@RequestMapping("/v1/posts")
@Validated
public class PostController {

    @RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json; charset=UTF-8")
    public ResponseEntity<Object> update(@Validated(OnCreate.class) @RequestBody CreateOrUpdatePostRequest createPostRequest) {
        Post post = postService.create(createPostRequest);
        return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
    }

    @RequestMapping(value = "/{postId}", method = RequestMethod.PUT, consumes = "application/json", produces = "application/json; charset=UTF-8")
    public ResponseEntity<Object> update(@Validated(OnUpdate.class) @RequestBody CreateOrUpdatePostRequest updatePostRequest, @PathVariable Integer postId) {
        Post post = postService.update(postId, updatePostRequest);
        return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
    }
}

With this, you can have a single mapping function.

Still, keep in mind that using validation groups can easily become an anti-pattern given that we are mixing different concerns. With validation groups, the validated entity has to know the validation rules for all the use cases it is used in. Having said that, I usually avoid using validation groups unless it is really necessary.


Regarding tags I guess your only option is to query the database. The ones that do not exist you should create them (I guess), so something along the following lines:

List<Integer> tagsId = createOrUpdatePostRequest.getTagsId();
List<Tag> tags = tagService.findAllByIds(tagsId);
List<Integer> nonExistentTagsId = tagsId.stream().filter(id -> tags.stream().noneMatch(tag -> tag.getId().equals(id)));
if (!nonExistentTagsId.isEmpty()) {
    // create Tags and add them to tags List
}
  • Related