Veruca Stucker
Veruca Stucker

Reputation: 11

Spring boot & JSON: How to add foreign key when using POST request

I can't seem to figure out how to add an entity that has a foreign key, though JSON.

I have a user model, and a post model. A user can make different posts on a website.

This is a many-to-one relationship. A user can have several posts, while a post can only have one user (the poster). The post as a foreign key representing the id of the user that made the post.

This is the User model:

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Table(name = "user")
public class User {

//ID
@Id
@SequenceGenerator(
        name = "user_sequence",
        sequenceName = "user_sequence",
        allocationSize = 1
)
@GeneratedValue(
        strategy = GenerationType.IDENTITY,
        generator = "user_generator"
)
@Column(name = "id",nullable = false)
private int id;

private String username;
private String password;
private String email;

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd")
@Column(name = "creation_date")
private Date creationDate;


//RELATIONSHIP
@OneToMany(cascade = CascadeType.ALL, mappedBy = "user")
private List<Post> posts = new ArrayList<>();




/* =========== GETTERS AND SETTERS ===========*/

public int getId() {
    return id;
}

public void setId(int id) {
    this.id = id;
}

public String getUsername() {
    return username;
}

public void setUsername(String username) {
    this.username = username;
}

public String getPassword() {
    return password;
}

public void setPassword(String password) {
    this.password = password;
}

public String getEmail() {
    return email;
}

public void setEmail(String email) {
    this.email = email;
}

public Date getCreationDate() {
    return creationDate;
}

public void setCreationDate(Date creationDate) {
    this.creationDate = creationDate;
}

public List<Post> getPosts() {
    return posts;
}

public void setPosts(List<Post> posts) {
    this.posts = posts;
}
}

This is the Post model:

    @Entity
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    @Table(name = "post")
    public class Post {
    
    //ID
    @Id
    @SequenceGenerator(
            name = "post_sequence",
            sequenceName = "post_sequence",
            allocationSize = 1
    )
    @GeneratedValue(
            strategy = GenerationType.IDENTITY,
            generator = "post_generator"
    )
    @Column(name = "id", nullable = false)
    private int id;
    
    @Column(name = "post_content")
    private String postContent;
    private String title;
    
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd")
    @Column(name = "creation_date")
    private Date creationDate;
    
    
    //RELATIONSHIP
    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "user_id", referencedColumnName = "id")
    private User user;
    
    
    
    /* ======== GETTERS AND SETTERS ======== */
    public int getId() {
        return id;
    }
    
    public void setId(int id) {
        this.id = id;
    }
    
    public String getPostContent() {
        return postContent;
    }
    
    public void setPostContent(String postContent) {
        this.postContent = postContent;
    }
    
    public String getTitle() {
        return title;
    }
    
    public void setTitle(String title) {
        this.title = title;
    }
    
    public Date getCreationDate() {
        return creationDate;
    }
    
    public void setCreationDate(Date creationDate) {
        this.creationDate = creationDate;
    }
    
    public User getUser() {
        return user;
    }
    
    public void setUser(User user) {
        this.user = user;
    }
    }

This is the postController:

@RestController
public class PostController {

@Autowired
private PostService postService;

@PostMapping("/savePost")
public Post getPost(@Validated @RequestBody Post post) {
    return postService.savePost(post);
}

@GetMapping("getPost/{id}")
public Post getPost(@PathVariable int id) {
    return postService.getPost(id);
}

@PutMapping("/deletePost/{id}")
public void deletePost(int id) {
    postService.deletePost(id);
}

}

This is the JSON I send in to add a post. Request to: http://localhost:8080/savePost JSON body:

{
"postContent": "some content",
"creationDate": "2022-07-31",
"title": "my title",
"user": 1
}

But in postMan i get this error:

{
"timestamp": "2022-08-02T10:40:11.794+00:00",
"status": 400,
"error": "Bad Request",
"path": "/savePost"
}

And in spring i get this error: JSON parse error: Cannot construct instance of x.model.User (although at least one Creator exists): no int/Int-argument constructor/factory method to deserialize from Number value (1);

If i send in a JSON where I call the user for "user_id" or "uderId", then Im able to send the request, but then the foreign key turns into null

{
"creationDate": "2022-07-31",
"postContent": "some content",
"title": "my title",
"user_id": 1
}

what gets sent in:

{
"id": 2,
"postContent": "some content",
"title": "my title",
"creationDate": "2022-07-31",
"user": null
}

Does anyone know what im doing wrong?

Upvotes: 0

Views: 3970

Answers (1)

Yuriy Tsarkov
Yuriy Tsarkov

Reputation: 2548

Firstly, your APIs are not correct in terms of REST concept. Here is a nice explanation. You better should rework it to deal with Post entities:

  • add a userId param to the controller's value and remove it from models.
  • Use different classes for transport and business processes. It'll give you at least two prefenecies: first one is ability to pass any extra data through model objects, or wider, control which properties can be passed to be inserted/updated (as well as possibility of a separated validation for post/put operations), and the second one is guarantee that you won't face an Open session in View problem.
@RestController("/user/{userId}/post")
public class PostController {

  @Autowired
  private PostService postService;

  @PostMapping("/save")
  public PostResponseDTO addPost(@Validated @RequestBody PostAddDTO postModel, @PathVariable Long userId) {
    return postService.savePost(userId, postModel);
  }

  @PutMapping("/{id}")
  public PostResponseDTO updatePost(@Validated @RequestBody PostUpdateDTO postModel, @PathVariable Long userId, @PathVariable Long id) {
    return postService.updatePost(userId, postModel);
  }

  @GetMapping("/{id}")
  public PostResponseDTO getPost(@PathVariable Long userId, @PathVariable Long id) {
    return postService.getPost(userId, id);
  }

  @DeleteMapping("/{id}")
  public void deletePost(Long userId, Long id) {
    postService.deletePost(userId, id);
  }

}

You must:

  • add parameter fetch = FetchType.LAZY to both of @OneToMany and @ManyToOne declarations;
  • add a userId property to the PostUpdateDTO (if changing of post's owner is allowed)

At the service layer you can:

  • if POST: find a User by userId, validate weither exists it and probably raise some exception if doesn't or create and persist a new Post entity:
  @Transactional
  public PostResponseDTO addPost(PostAddDTO postModel, Long userId) {
    User user = getValidUser(userId);
    Post post = new Post(postModel);
    UserDTO userDTO = new UserDTO(user); // here copy only simple properties, not the list of user's posts
    post.setUser(user);
    postRepository.save(post);
    PostAddDTO result = new PostAddDTO(post);
    result.setUser(userDTO);
    return result;
  }

  /**
 - will be used in both post and put operations
 - @param userId user id from a controller
 - @return {@link User} entity if found
 - @throws RuntimeException if the entity has not been found
   */
  private User getValidUser(Long userId) {
    Optional<User> userOpt = userRepository.findById(userId);
    if (!userOpt.isPresent()) {
      log.WARN("addPost. user with id={userId} not found!", userId);
      throw RuntimeException("some exception"); //!!!do not place any business info such as "user with id={userId} not found" because of a scam risc reasons
    }
    return userOpt.get();
  }
  • if PUT: find a Post entity by userId and id, validate weither exists it or not and implement preferable logic. I don't know is it allowed to reassign a user? If so, check new user's existance first.

  • If DELETE, you may raise an exceptions in case of entities absence, but many not and just do nothing to success response be sent

One more reason why we do use transport objects. If you let it be as it is, it'll lead to an infinite loop while serialization: post.user -> (post: user.posts) {post.user -> ...}.

Of course all of this stuff is not the only way of solving of this problem and it doesn't answer all the questions about Java Persistance API, but this is the way that I went for some time ago within a concrete project. Here is a REST guide made by a Spring team

Upvotes: 1

Related Questions