How to create a new parent entity that references an already existing child entity in spring data rest / HATEOAS
Asked Answered
D

2

8

In my project I have two domain models. A parent and a child entity. The parent references a list of child entitires. (e.g. Post and Comments) Both entities have their spring data JPA CrudRepository<Long, ModelClass> interfaces which are exposed as @RepositoryRestResource

HTTP GET and PUT operations work fine and return nice HATEOS representation of these models.

Now I need a special REST endpoint "create a new Parent that references one ore more already existing child entities". I'd like to POST the references to the children as a text/uri-list that I pass in the body of the request like this:

POST http://localhost:8080/api/v1/createNewParent
HEADER
  Content-Type: text/uri-list
HTTP REQUEST BODY:
   http://localhost:8080/api/v1/Child/4711
   http://localhost:8080/api/v1/Child/4712
   http://localhost:8080/api/v1/Child/4713

How do I implement this rest endpoint? This is what I tried so far:

 @Autowired
 ParentRepo parentRepo  // Spring Data JPA repository for "parent" entity


 @RequestMapping(value = "/createNewParent", method = RequestMethod.POST)
 public @ResponseBody String createNewParentWithChildren(
    @RequestBody Resources<ChildModel> childList,                         
    PersistentEntityResourceAssembler resourceAssembler
) 
{
   Collection<ChildModel> childrenObjects = childList.getContent()

   // Ok, this gives me the URIs I've posted
   List<Link> links = proposalResource.getLinks();

   // But now how to convert these URIs to domain objects???
   List<ChildModel> listOfChildren = ... ???? ...

   ParentModel newParnet = new ParentModel(listOfChildren)
   parentRepo.save(newParent)

}

Reference / Related https://github.com/spring-projects/spring-hateoas/issues/292

Demagnetize answered 17/11, 2017 at 18:43 Comment(5)
Remark: I know how I can add elements to the list of childs via the spring-hateoas rest endpoint exposed by the RepositoryRestResource. There I can create the parnet child relationship via POSTing text/uri-list as described here: #26259974 But I want to know how I do that in my own custom rest endpoint.Demagnetize
There are quite some very similar questions like this. But my special case is: I want to create a NEW parent entity, that will be linked to the already EXISTING child entitry.Demagnetize
I'm a bit confused but how can a child exist before its parent? It's like you create a comment to a post that dosn't yet exist. Usually you also try to avoid the verb in the resource endpoint as it gives some kind of RPC smell to the endpoint, though to REST it doesn't really matter if it's there or not.Stauder
@RomanVottner There are two kinds of OneToMany relationships: Composition and Aggregation. (see #886437) In my domain model, both entities can exist on their own and have their own life cycle. They can be linked.Demagnetize
Maybe "Forum Posts" and "Comments" is a bad example. A better examaple is "Schools" and "Student". One School has several students. (School -> OneToMany ---> Students) But one Student may move to another school, e.g. when the school is closed or when he moves where he lives. The Student exists on his own. And the problem that I have: I want to found a new school. And I want it to be related to some already existing students.Demagnetize
D
2

There is one related problem on a side note, that one also needs to take into account: When I want to save the parent entity, then I do not want to touch, save or alter the already existing child entity in any way. This is not that easy in JPA. Because JPA will also (try to) persist the dependant child entity. This fails with the exception:

javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist:

To circumvent that, you have to merge the child entity into the transactin of the JPA save() call. The only way I found to have both entities in one transaction was to create a seperate @Services which is marked as @Transactional. Seems like complete overkill and overengeneering.

Here is my code:

PollController.java // the custom REST endpoint for the PARENT entiy

@BasePathAwareController
public class PollController {

@RequestMapping(value = "/createNewPoll", method = RequestMethod.POST)
public @ResponseBody Resource createNewPoll(
    @RequestBody Resource<PollModel> pollResource, 
    PersistentEntityResourceAssembler resourceAssembler
) throws LiquidoRestException
{
  PollModel pollFromRequest = pollResource.getContent();
  LawModel proposalFromRequest = pollFromRequest.getProposals().iterator().next();             // This propsal is a "detached entity". Cannot simply be saved.
  //jpaContext.getEntityManagerByManagedType(PollModel.class).merge(proposal);      // DOES NOT WORK IN SPRING.  Must handle transaction via a seperate PollService class and @Transactional annotation there.

  PollModel createdPoll;
  try {
    createdPoll = pollService.createPoll(proposalFromRequest, resourceAssembler);
  } catch (LiquidoException e) {
    log.warn("Cannot /createNewPoll: "+e.getMessage());
    throw new LiquidoRestException(e.getMessage(), e);
  }

  PersistentEntityResource persistentEntityResource = resourceAssembler.toFullResource(createdPoll);

  log.trace("<= POST /createNewPoll: created Poll "+persistentEntityResource.getLink("self").getHref());

  return persistentEntityResource;   // This nicely returns the HAL representation of the created poll
}

PollService.java // for transaction handling

@Service
public class PollService {

    @Transactional    // This should run inside a transaction (all or nothing)
    public PollModel createPoll(@NotNull LawModel proposal, PersistentEntityResourceAssembler resourceAssembler) throws LiquidoException {
    //===== some functional/business checks on the passed enties (must not be null etc)
    //[...]

    //===== create new Poll with one initial proposal
    log.debug("Will create new poll. InitialProposal (id={}): {}", proposal.getId(), proposal.getTitle());
    PollModel poll = new PollModel();
    LawModel proposalInDB = lawRepo.findByTitle(proposal.getTitle());  // I have to lookup the proposal that I already have

    Set<LawModel> linkedProposals = new HashSet<>();
    linkedProposals.add(proposalInDB);
    poll.setProposals(linkedProposals);

    PollModel savedPoll = pollRepo.save(poll);

    return savedPoll;
}
Demagnetize answered 21/11, 2017 at 11:13 Comment(0)
I
0

I had the same problem. The question is a bit old, but I found another solution :

In fact you have to force merge on parent but persis is called on creation. You could by-pass by saving parent with empty child list, add childs to list and saving again :

List<ChildModel> listOfChildren = ... ???? ...

ParentModel newParnet = new ParentModel()
parent = parentRepo.save(newParent)
parent.getChilds().addAll(listOfChildren)
parentRepo.save(parent)

To have access to merge, you have to code a Custom repo :

public interface PollModelRepositoryCustom {
    public PollModel merge(PollModel poll);
}

and its implementation

@Repository
public class PollModelRepositoryCustomImpl implements PollModelRepositoryCustom {
    @PersistenceContext
    private EntityManager entityManager;

    public PollModel merge(PollModel poll) {
        return entityManager.merge(poll);
    }
}

then you can call : parentRepo.(newParent) instead of parentRepo.save(newParent)

Incult answered 15/11, 2018 at 17:25 Comment(1)
saving twice works around it, yes. But that's then also two calls to the DB. Might be performance relevant in some extreme cases.Demagnetize

© 2022 - 2024 — McMap. All rights reserved.