109

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.

Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
ccampo
  • 1,483
  • 3
  • 12
  • 14
  • 9
    7 days later and still not luck. If anybody knows a way to get this behavior working, please let me know. Thanks! – ccampo Aug 22 '14 at 14:48
  • are you using @RepositoryRestResource or a controller? It would be helpful to see that code as well. – Magnus Lassi Sep 09 '14 at 02:33
  • I am using Spring boot data rest ,It worked for me [http://stackoverflow.com/questions/37902946/add-item-to-the-collection-with-foreign-key-via-rest-call](http://stackoverflow.com/questions/37902946/add-item-to-the-collection-with-foreign-key-via-rest-call) – Taimur Mar 28 '17 at 14:46

5 Answers5

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.

Community
  • 1
  • 1
Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
  • 5
    Interested to here from the down voters, what the reason for the votes were ;). – Oliver Drotbohm Sep 02 '14 at 14:41
  • 3
    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. – ccampo Sep 09 '14 at 18:49
  • 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. – ccampo Sep 09 '14 at 18:50
  • If that's the case, create dedicated resources for them. – Oliver Drotbohm Oct 04 '14 at 09:22
  • 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? – aycanadal Feb 23 '15 at 22:40
  • 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. http://haltalk.herokuapp.com/explorer/browser.html#/users/mike – keaplogik Mar 09 '15 at 15:57
  • 28
    Most intuitive way for me to post a comment is to make a POST to http://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? – aycanadal May 21 '15 at 13:37
  • 1
    What would you return as `Location` header then? – Oliver Drotbohm May 22 '15 at 14:16
  • localhost:8080/posts/1/comments or localhost:8080/posts/1/comments/xxx – aycanadal May 25 '15 at 22:25
  • 1
    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. – Jason Jun 01 '15 at 12:01
  • 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. – aycanadal Jun 25 '15 at 13:57
  • @aycanadal 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? – Onur Jun 26 '15 at 06:07
  • 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. – aycanadal Jun 26 '15 at 08:27
  • 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... – banncee Oct 25 '15 at 14:43
  • @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 :) – Miguel Pereira Nov 18 '15 at 13:52
  • I'm struggling with a similar scenario, not posts/comments but very similar. – raner Dec 03 '15 at 03:01
  • 1
    (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 bodies – raner Dec 03 '15 at 03:16
  • @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 http://stackoverflow.com/questions/35756753/how-to-properly-add-an-element-to-a-collection-using-jsonpatch-with-spring-data – Gabriel Bauman Mar 03 '16 at 19:39
  • 6
    @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. – JW Lim Jun 09 '16 at 00:14
  • 2
    Is there any guide for using api created with spring data rest? I've googled it for 2 hours and found nothing. Thank you! – Skeeve Nov 21 '16 at 00:39
  • @banncee you should use PATCH, not PUT – Adam Jan 10 '17 at 12:46
  • 2
    @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"} – ddewaele Mar 12 '17 at 10:50
  • @OliverGierke can you please answer these 2 questions? https://stackoverflow.com/questions/51247346/enable-repository-only-for-sub-resource-level-in-spring-data-rest https://stackoverflow.com/questions/51323265/actual-payload-instead-of-resource-uri-for-sub-resource-post-in-spring-data-rest – Supun Wijerathne Jul 13 '18 at 10:42
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.

Uniruddh
  • 4,427
  • 3
  • 52
  • 86
Chetan Kokil
  • 504
  • 5
  • 2
  • 2
    This worked for me. Just make sure the `author.post` is writable (for example by having a setter or `@JsonValue` annotation) – scheffield May 28 '15 at 03:14
  • 1
    Should this also work with a patch request as in moving the comment from one post to another? – aycanadal Jun 25 '15 at 14:16
  • This worked perfectly for me. Much preferred to the multi-step method mentioned above. – Laran Evans Sep 07 '15 at 05:34
  • 2
    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? – banncee Oct 25 '15 at 14:45
  • 1
    This works for non-nullable relationships, too, which was exactly what I needed. Thanks. – JW Lim Jun 09 '16 at 18:35
  • 4
    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?? – apostrophedottilde Apr 17 '17 at 11:40
  • 2
    I try to do this ([see here](https://stackoverflow.com/questions/47376620/creating-new-entity-plus-association-not-working)) but for some reason only the resource, not the association, is created. – Stefan Falk Nov 19 '17 at 12:49
  • 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). – Jared Jun 08 '20 at 00:14
  • is it really? I gonna remove data rest from my project... – withoutOne Jan 25 '22 at 06:02
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....

Peter Reid
  • 5,139
  • 2
  • 37
  • 33
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.

Selva
  • 11
  • 1
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.

Armali
  • 18,255
  • 14
  • 57
  • 171