spring-projects / spring-projects/spring-data-rest

PUT Request Issue with Composite Keys and OneToMany Relationship (Single Repo for Main Entity)

Open
#2,398 1 comment 1 reaction 1 assignee View on GitHub

@odrotbohm is already working on this.

Since Jul 22, 2024.

status: waiting-for-triage
Dominant language
Java
Stars
958
Forks
568
PR merge metrics
No merged PRs in 30d

Description

Reproducer:

I've created a reproducer to demonstrate the issue. The repository is available here.

Summary

The main entity Movie has a OneToMany relation to Ratings. Ratings has a composite primary key with movieID and ratingPlatformId. Only the Movie entity has a REST repository, and IDs are exposed. A single movie API exists where GET works fine, but updating via PUT doesn't. Updating the ratings list gives unexpected results. For example:

Before:

{
   "id": 2,
   "name": "The Dark Knight",
   "ratings": [
      {
         "ratingPlatformId": 1,
         "score": 10
      }
   ]
}

PUT Request:

{
   "id": 2,
   "name": "The Dark Knight",
   "ratings": [
      {
         "ratingPlatformId": 2,
         "score": 2
      }
   ]
}

Expectation:
The rating for ratingPlatformId 1 should be deleted, and a new one with ratingPlatformId 2 should be created.

Actual Result:

{
   "id": 2,
   "name": "The Dark Knight",
   "ratings": [
      {
         "ratingPlatformId": 1,
         "score": 2
      }
   ]
}

I debugged a bit and ratingPlatformId is not considered at all during the PUT operation and gets skipped here as it is an ID:

https://github.com/spring-projects/spring-data-rest/blob/aaadc344ab1bef4ed98cb0dbf6ca8ebd7c9262ff/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/DomainObjectReader.java#L688-L690

The only working setup is inspired by AresEkb from #2324 and involves:

  • Using a HashMap for the OneToMany ratings and writing getRatings and setRatings methods manually.
    @OneToMany(mappedBy = "movie", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    @JsonManagedReference("movie-ratings")
    @MapKey(name = "ratingPlatformId")
    @ToString.Exclude
    private Map<Long, Rating> ratings = new HashMap<>();

    public Collection<Rating> getRatings() {
        return ratings.values();
    }

    public void setRatings(Collection<Rating> ratings) {
        this.ratings.clear();
        ratings.forEach(rating -> this.ratings.put(rating.getRatingPlatformId(), rating));
    }
  • Adding some AOP workaround for RepositoryEntityController.putItemResource to set the movie in the ratings.
    @Around("putItemResource()")
    public Object aroundPutItemResource(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();

        if (args[1] instanceof PersistentEntityResource payload) {
            Object content = payload.getContent();
            if (content instanceof Movie updatedMovie) {
                for (Rating rating : updatedMovie.getRatings()) {
                    rating.setMovieId(updatedMovie.getId());
                    rating.setMovie(updatedMovie);
                }
            }
        }

Branches

  • main: Working "solution" with HashMap and AOP workarounds.
  • experimental/hash-set: Setup with HashSet and AOP, but adding list elements in the middle fails.
  • experimental/plain: Setup with HashSet and without any AOP or workarounds; many tests fail.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.