POSTing a @OneToMany sub-resource association in Spring Data REST
Asked Answered
E

5

111

Currently I have a Spring Boot application using Spring Data REST. I have a domain entity Post which has the @OneToMany relationship to another domain entity, Comment. These classes are structured as follows:

Post.java:

@Entity
public class Post {

    @Id
    @GeneratedValue
    private long id;
    private String author;
    private String content;
    private String title;

    @OneToMany
    private List<Comment> comments;

    // Standard getters and setters...
}

Comment.java:

@Entity
public class Comment {

    @Id
    @GeneratedValue
    private long id;
    private String author;
    private String content;

    @ManyToOne
    private Post post;

    // Standard getters and setters...
}

Their Spring Data REST JPA repositories are basic implementations of CrudRepository:

PostRepository.java:

public interface PostRepository extends CrudRepository<Post, Long> { }

CommentRepository.java:

public interface CommentRepository extends CrudRepository<Comment, Long> { }

The application entry point is a standard, simple Spring Boot application. Everything is configured stock.

Application.java

@Configuration
@EnableJpaRepositories
@Import(RepositoryRestMvcConfiguration.class)
@EnableAutoConfiguration
public class Application {

    public static void main(final String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Everything appears to work correctly. When I run the application, everything appears to work correctly. I can POST a new Post object to http://localhost:8080/posts like so:

Body: {"author":"testAuthor", "title":"test", "content":"hello world"}

Result at http://localhost:8080/posts/1:

{
    "author": "testAuthor",
    "content": "hello world",
    "title": "test",
    "_links": {
        "self": {
            "href": "http://localhost:8080/posts/1"
        },
        "comments": {
            "href": "http://localhost:8080/posts/1/comments"
        }
    }
}

However, when I perform a GET at http://localhost:8080/posts/1/comments I get an empty object {} returned, and if I try to POST a comment to the same URI, I get an HTTP 405 Method Not Allowed.

What is the correct way to create a Comment resource and associate it with this Post? I'd like to avoid POSTing directly to http://localhost:8080/comments if possible.

Essy answered 14/8, 2014 at 15:32 Comment(3)
7 days later and still not luck. If anybody knows a way to get this behavior working, please let me know. Thanks!Essy
are you using @RepositoryRestResource or a controller? It would be helpful to see that code as well.Cherise
I am using Spring boot data rest ,It worked for me https://mcmap.net/q/196278/-add-item-to-the-collection-with-foreign-key-via-rest-callFriseur
M
48

You have to post the comment first and while posting the comment you can create an association posts entity.

It should look something like below :

http://{server:port}/comment METHOD:POST

{"author":"abc","content":"PQROHSFHFSHOFSHOSF", "post":"http://{server:port}/post/1"}

and it will work perfectly fine.

Mohave answered 17/10, 2014 at 14:9 Comment(9)
This worked for me. Just make sure the author.post is writable (for example by having a setter or @JsonValue annotation)Archducal
Should this also work with a patch request as in moving the comment from one post to another?Byebye
This worked perfectly for me. Much preferred to the multi-step method mentioned above.Efferent
This would be my (vastly) preferred approach, but it does not seem to be working for me. :( It creates the Comment, but does not create the row in the resolution table (POST_COMMENTS). Any suggestions on how to resolve?Nisan
This works for non-nullable relationships, too, which was exactly what I needed. Thanks.Slr
What would the approach be for a scenario, eg with Venue and Address entities, where a venue must have an Address and an address MUST be associated with a Venue? I mean...to avoid creating an orphaned address which may never be assigned to anything? Maybe Im wrong, but the client app SHOULD NEVER be responsible maintaining consistency within the database. I cannot rely on client app creating an Address and then definitely assigning to a Venue. Is there a way to POST the sub-resource (in this case the Address entity) with the creation of the actual resource so that I can avoid inconsistency??Creel
I try to do this (see here) but for some reason only the resource, not the association, is created.Security
This didn't work for me. There has to be more to it, to make this work (which would be the desired way). Because having to create the "comment", then PUT the association (the "post" URL) is pretty dumb (but it works).Apartheid
is it really? I gonna remove data rest from my project...Overlord
P
56

Assuming you already have discovered the post URI and thus the URI of the association resource (considered to be $association_uri in the following), it generally takes these steps:

  1. Discover the collection resource managing comments:

     curl -X GET http://localhost:8080
    
     200 OK
     { _links : {
         comments : { href : "…" },
         posts :  { href : "…" }
       }
     }
    
  2. Follow the comments link and POST your data to the resource:

     curl -X POST -H "Content-Type: application/json" $url 
     { … // your payload // … }
    
     201 Created
     Location: $comment_url
    
  3. Assign the comment to the post by issuing a PUT to the association URI.

     curl -X PUT -H "Content-Type: text/uri-list" $association_url
     $comment_url
    
     204 No Content
    

Note, that in the last step, according to the specification of text/uri-list, you can submit multiple URIs identifying comments separated by a line break to assign multiple comments at once.

A few more notes on the general design decisions. A post/comments example is usually a great example for an aggregate, which means I'd avoid the back-reference from the Comment to the Post and also avoid the CommentRepository completely. If the comments don't have a lifecycle on their own (which they usually don't in an composition-style relationship) you rather get the comments rendered inline directly and the entire process of adding and removing comments can rather be dealt with by using JSON Patch. Spring Data REST has added support for that in the latest release candidate for the upcoming version 2.2.

Prader answered 22/8, 2014 at 16:31 Comment(23)
Interested to here from the down voters, what the reason for the votes were ;).Prader
I'm not sure about the down voters... I don't even have the reputation to do it! The reason I don't necessarily like putting comments inline with posts is because consider the (unlikely) scenario when I have thousands of comments for a single post. I would like to be able to paginate the collection of comments instead of getting the entire bunch of them every time I want to access the contents of the post.Essy
Additionally, the API users may not necessarily care about the comments, but instead only care about the content of the post (or even vice-versa). I'd like the be able to give the option to retrieve the comments separately from the post itself, and the post separately from the comments.Essy
If that's the case, create dedicated resources for them.Prader
Most likely you'll end up caring if a comment has been made after any of the current user's comments or something like that :) thus the need for a dedicated resource. But seriously if one of the downvoters could come up with what's wrong please?Byebye
Why can you not POST the comment to the returning comments link? What was the design decision around this? The HAL examples show this as supported. See this example provided in the spec on users posts. haltalk.herokuapp.com/explorer/browser.html#/users/mikeKnopp
Most intuitive way for me to post a comment is to make a POST to localhost:8080/posts/1/comments. Is it not the simplest and most meaningful way to do it? And at the same time, you should still be able to have a dedicated comment repository. Is it spring or HAL standard that does not allow this?Byebye
What would you return as Location header then?Prader
localhost:8080/posts/1/comments or localhost:8080/posts/1/comments/xxxByebye
Returning /posts/1/comments as the created location i think is not accurate. If there is a comments repository, you could return the location header as /posts/1/comments/xxx or as /comments/xxx. They refer to the same resource, so it's the same thing and it doesn't matter which one is returned, right? although /posts/1/comments/xxx may be more appropriate since it reflects the url for the POST.Intensifier
if you return /posts/1/comments/xxx then /comments/xxx can also be found from the self link at that location. if you return /comments/xxx all you have is /comments/xxx unless of course the comment ids in both locations match. Well if two addresses are completely interchangeable then it shouldn't really matter which one you return or which one you post to. It makes sense to return whichever the client used in the request or whichever has a link to the other as in a self link.Byebye
@Byebye lets say /comment/36 belongs to post 2, what /posts/1/comments/36 should return than? Or /posts/1/comments/1 should return the first comment of post 1 or the comment that id is 1?Cychosz
I think both is fine. SDR does the second one, in which case /posts/1/comments/36 returns no data if there is no comment with id 36 that belongs to post 1. Like it should.Byebye
Here is a problem (IMO) with this approach: If the Post already has Comments, i.e., the POST_COMMENTS table already has rows with the Post ID for this Post, doing a PUT removes all rows in the table and adds the new row. It seems like I shouldn't have to query the resolution table first to get all existing rows and then add them back again. Unless I'm doing something wrong...Nisan
@OliverGierke I like the idea, can you point me to some sample code on how to remove the back reference and the repository? I get errors when I tries it :)Jobi
I'm struggling with a similar scenario, not posts/comments but very similar.Philbrick
(sorry, missed the editing window for my previous comment) In my case, POST /posts/1/comments/xxx would be the seemingly intuitive way to add a resource (with xxx being the posting user's name, in my case). I'm not quite happy with either of the suggested solutions: 1) @ChetanKokil's solution allows adding a comment in a single step, but the entities do not accurately reflect the aggregate nature of the association 2) OliverGierke's solution models the aggregate correctly but relies on sending JSON Patch bodies, which is cumbersome if all you want is add new comments and use plain JSON bodiesPhilbrick
@olivier-gierke : I tried to do a composition-style aggregate on a similar relationship using JSONPatch as you recommend here and couldn't get it working. See #35757253Plymouth
@OliverGierke Is this still the recommended/only way to do this? What if the child is not-nullable (@JoinColumn(nullable=false))? It would not be possible to first POST the child, then PUT/PATCH the parent association.Slr
Is there any guide for using api created with spring data rest? I've googled it for 2 hours and found nothing. Thank you!Thighbone
@Nisan you should use PATCH, not PUTGonzales
@OliverGierke : in case of step 2 , can this work if your comment has a non-null constraint on a post_id ? How would you avoid ConstraintViolationExceptions like this : ERROR: null value in column post_id violates not-null constraint: could not execute statement"}Stedmann
@OliverGierke can you please answer these 2 questions? #51247846 #51323765Mood
M
48

You have to post the comment first and while posting the comment you can create an association posts entity.

It should look something like below :

http://{server:port}/comment METHOD:POST

{"author":"abc","content":"PQROHSFHFSHOFSHOSF", "post":"http://{server:port}/post/1"}

and it will work perfectly fine.

Mohave answered 17/10, 2014 at 14:9 Comment(9)
This worked for me. Just make sure the author.post is writable (for example by having a setter or @JsonValue annotation)Archducal
Should this also work with a patch request as in moving the comment from one post to another?Byebye
This worked perfectly for me. Much preferred to the multi-step method mentioned above.Efferent
This would be my (vastly) preferred approach, but it does not seem to be working for me. :( It creates the Comment, but does not create the row in the resolution table (POST_COMMENTS). Any suggestions on how to resolve?Nisan
This works for non-nullable relationships, too, which was exactly what I needed. Thanks.Slr
What would the approach be for a scenario, eg with Venue and Address entities, where a venue must have an Address and an address MUST be associated with a Venue? I mean...to avoid creating an orphaned address which may never be assigned to anything? Maybe Im wrong, but the client app SHOULD NEVER be responsible maintaining consistency within the database. I cannot rely on client app creating an Address and then definitely assigning to a Venue. Is there a way to POST the sub-resource (in this case the Address entity) with the creation of the actual resource so that I can avoid inconsistency??Creel
I try to do this (see here) but for some reason only the resource, not the association, is created.Security
This didn't work for me. There has to be more to it, to make this work (which would be the desired way). Because having to create the "comment", then PUT the association (the "post" URL) is pretty dumb (but it works).Apartheid
is it really? I gonna remove data rest from my project...Overlord
B
2

There are 2 types of mapping Association and Composition. In case of association we used join table concept like

Employee--1 to n-> Department

So 3 tables will be created in case of Association Employee, Department, Employee_Department

You only need to create the EmployeeRepository in you code. Apart from that mapping should be like that:

class EmployeeEntity{

@OnetoMany(CascadeType.ALL)
   private List<Department> depts {

   }

}

Depatment Entity will not contain any mappping for forign key...so now when you will try the POST request for adding Employee with Department in single json request then it will be added....

Banda answered 27/6, 2017 at 19:3 Comment(0)
O
1

I faced the same scenario and I had to remove the repository class for the sub entity as I have used one to many mapping and pull data thru the main entity itself. Now I am getting the entire response with data.

Orchestrion answered 14/6, 2016 at 20:54 Comment(1)
This thing which you talk about may be easily done with projectionsHeterogenesis
S
0

For oneToMany mapping, just make a POJO for that class you want to Map, and @OneToMany annotation to it, and internally it will map it to that Table id.

Also, you need to implement the Serializable interface to the class you are retrieving the Data.

Swen answered 29/10, 2018 at 9:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.