Spring Data Rest - POST with relations does not persist dependent entity - spring-data-jpa

I'm trying to persist 2 dependent entities with SDR 3.1.6 and RestTemplate which are defined like this
#Entity
public class Child {
#Id
#GeneratedValue
private long id;
[getters/setters omitted]
}
#Entity
public class Parent {
#Id
#GeneratedValue
private long id;
#OneToOne(cascade = CascadeType.ALL)
private Child child;
[getters/setters omitted]
}
Since Parent owns the Child my order of persisting is the following
RestTemplate restTemplate = new RestTemplate();
Child child = restTemplate.postForObject("server-url/children", new Child(), Child.class);
Parent parent = new Parent();
parent.setChild(child);
Parent created = restTemplate.postForObject("server-url/parents", parent, Parent.class);
The result is not what I would expect, becausecreatedParent.getChild() results in null and querying server-url/parents shows a parent with link to child, which leads to a 404 when followed.
_embedded: {
parents: [
{
_links: {
self: {
href: "server-url/parents/2"
},
parent: {
href: "server-url/parents/2"
},
child: {
href: "server-url/parents/2/child"
}
}
}
]
}
However there is a Child object persisted, which can be queried by server-url/children
_embedded: {
children: [
{
_links: {
self: {
href: "server-url/exported-children/1"
},
child: {
href: "server-url/exported-children/1"
}
}
}
]
}
I think I followed the Spring Data Rest docs, but something here is awful amiss. Can anyone enlighten me please.

Related

how to save parent object containing child object using spring boot in #ManyToOne unidirectional mapping?

I am new in spring boot. I've two model classes Party(parent) and PartyCategory(child). PartyCategory stores data id, labelAr and labelEn successfully.
Now I am passing child id in json request and getting null values for labelAr and labelEn in json response as pasted below. Can someone please help what and doing wrong here.
I've pasted my code as well.
Json Request:
{
"name": "Party A",
"description": "Description of Party A",
"category": {
"id": 1
}
}
Json response;
{
"id": 6,
"name": "Party A",
"description": "Description of Party A",
"category": {
"id": 1,
"labelAr": null,
"labelEn": null
}
}
Party.java:
public class Party {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String name;
private String description;
#ManyToOne(cascade = CascadeType.MERGE, fetch = FetchType.EAGER)
#JoinColumn(name = "category_id")
private PartyCategory category;
....setters and getters
}
PartyCategory.java:-
public class PartyCategory {
#Id
#GeneratedValue
private Integer id;
private String labelAr;
private String labelEn;
...setters and getters..
Repositories:
public interface PartyCategoryRepository extends JpaRepository<PartyCategory, Integer> {
}
public interface PartyRepository extends JpaRepository<Party, Integer> {
}
Services:
public class PartyServiceImpl {
#Autowired
PartyRepository partyRepository;
public Party saveParty(Party party) {
return partyRepository.save(party);
}
Controller:
#RestController
public class PartyController {
#Autowired
PartyServiceImpl partyServiceIml;
#PostMapping(value = "/party/save")
public Party saveParty(#RequestBody Party party ) {
Party returnedParty = partyServiceIml.saveParty(party);
return returnedParty;
}
}
The problem is that the category you are posting is not recognised as being an existing category.
You can then do something like the below. Firstly, Create a Jackson converter class to customise the Json deserialisation. I was not sure if these were Spring managed but they are so you can then inject the necessary repository.
import com.fasterxml.jackson.databind.util.StdConverter;
#Component
public class CategoryConverter extends StdConverter<Integer, PartyCategory> {
#Autowired
private PartyCategoryRepository repo;
#Override
public PartyCategory convert(Integer value) {
return repo.findById(value).get();
}
}
Then update your entity as follows so that the category Json property will be handled via the converter created above. Note that in reality I would use a Jackson mix-in to apply this custom deserializer as this would avoid 'polluting' the entity class with Json processing instructions. You can look up how to do that.
#Entity
public class Party {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String name;
private String description;
#ManyToOne
#JoinColumn(name = "category_id")
#JsonDeserialize(converter = CategoryConverter.class) //in real life use a 'mix-in'
private PartyCategory category;
}
Then you can post JSON as below where we simply specify the id of an existing category:
{
"name": "Party A",
"description": "Description of Party A",
"category": 1
}
By enhancing this solution to use mix-ins as suggested then it is possible then to cleanly separate the view model from the entity model without having to create a DTO layer which will typically largely duplicate the entity model and which will required tedious mapping code to handle the conversions.
First of all, it's not a good practice to use the same entity for the database and also for the rest services. They should be separate entities, normally the entities for the rest services are called DTO (Data Access Objects).
Now, regarding your problem, it's normal what it's happening in your code, because you overwrite the PartyCategory labelAr and labelEn associated to the ID 1 with null values when you save your new Party, because you didn't provide any value for those two labels.
return partyRepository.save(party);
If you want to avoid this problem you have to retrieve the PartyCategory data first from the database, set to the Party entity and after that to save it to the database. Something like this:
public class PartyServiceImpl {
#Autowired
PartyRepository partyRepository;
#Autowired
PartyCategoryRepository partyCategoryRepository;
public Party saveParty(Party party) {
PartyCategory partyCategory = partyCategoryRepository.findById(party.getPartyCategory().getId());
party.setPartyCategory(partyCategory);
return partyRepository.save(party);
}

Entity with CascadeType.ALL in OneToMany not persisting children

I'm working with a 3rd party library provided to our team where one of the entities has a OneToMany relationship to entities of the same type of itself. I've changed the entity name to keep it anonymous.
Probably there's a better way of annotating entities with this type of relationship but as it's provided by a 3rd party I'm avoiding making to many changes so that it's compatible with future patches and updates.
It's using OpenJPA 2.4.0-ep2.0
#Entity
#Table(name = Person.TABLE_NAME)
public class Person {
private Long parentUid;
private List<Person> children = new ArrayList<>();
#OneToMany(targetEntity = Person.class, cascade = { CascadeType.ALL }, fetch = FetchType.LAZY)
#ElementJoinColumn(name = "PARENT_UID")
#ElementForeignKey
#ElementDependent
public List<Person> getChildren() {
return this.children;
}
}
When I try to persist a person with children, only the main entity gets persisted and children ignored.
However, if I change the fetch attribute to FetchType.EAGER it works (it persists both the parent and children). My understanding was that the fetch type only affects the loading, not the inserting. Any ideas why is it happening?
Also, is there a way of making it work while keeping the fetch type to FetchType.LAZY?
I've tried the following (modify the setter):
protected void setChildren(final List<Person> children) {
if (Objects.nonNull(children)) {
for (Person child : children) {
child.setParentUid(parentUid);
}
this.childItems = children;
} else {
this.childItems = new ArrayList<>();
}
}
Problem is in the child entity ,you should use #ManyToOne annotation in child entity.
add following code to Person :
public class person {
.
.
#MantToOne(fetch=FetchType.LAZY)
#JoinClolumn(name="PARENT_UID")
private Person parent;
public void setParent(Person parent){
}
.
.
}
then revise setChildren Code like this:
protected void setChildren(final List<Person> children) {
if (Objects.nonNull(children)) {
for (Person child : children) {
child.setParent(this);
}
this.childItems = children;
} else {
this.childItems = new ArrayList<>();
}
}
one important point is ،always fetch type must be sync in parent and child.

Update reference in existing ChildEntity on ParentEntity save

I have some Child entities that are already stored in the database. At some point, the user selects some entries adds them to a Parent and saves that Parent. The reference to the Parent in the Child does not get updated when calling the JpaReposity.save function of the Parent. Do I need to manually update/save every child entity in the database?
Parent
#Entity
public class Parent extends Base {
#OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH}, mappedBy = "parent")
private List<Child> children;
public List<Child> getChildren() {
return Collections.unmodifiableList(children);
}
public void addChild(Child child) {
if(this.children== null) { this.children= new ArrayList<>(); }
this.children.add(child);
child.setParent(this);
}
}
Child
#Entity
public class Child extends Base{
#ManyToOne
private Parent parent;
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent= parent
if(!parent.getChildren().contains(this)) {
parent.getChildren().add(this);
}
}
}
Repository
public interface ParentRepository extends JpaRepository<Parent, Long> {}
Service
#Transactional
Parent createAndSaveParent() {
List<Child> children = this.childRepo.findAll();
Parent parent = new Parent();
children.forEach(c -> parent.addChild(c));
return this.parentRepo.save(parent);
}
I am not getting any error, if I look at the returned Parent object after calling save, the children have the corrent parent set, but in the database the reference doesn't get updated.
Yes because of
mappedBy = "parent"
this means that the relationship is maintained by parent in the Child class and is responsible for setting the foreign key.

Spring Data Rest testing with JPA Relationship Mapping : Odd behaviour with update

I followed this tutorial (https://spring.io/guides/tutorials/react-and-spring-data-rest/#react-and-spring-data-rest-part-5) to experiment Spring Data REST and I wanted to test the CRUD with TestRestTemplate.
Add (postForEntity) is ok.
Delete (delete) is ok.
Read (getForEntity) is ok.
Update (template.exchange(URL, HttpMethod.PUT, entity, String.class, ID)) only works when I don't have any relation with other entities... and I don't understand why.
Here's an example :
#Data
#Entity
public class Dojo {
private #Id #GeneratedValue Long id;
private String name;
private String location;
private Date created;
#OneToMany(mappedBy = "dojo")
#JsonIgnore
private List<Workshop> workshops;
private Dojo() {}
public Dojo(String name, String location) {
this.name = name;
this.location = location;
this.created = new Date();
this.workshops = new ArrayList<>();
}
//getters and setters ...
}
#Data
#Entity
public class Workshop {
private #Id #GeneratedValue Long id;
private String name;
#ManyToOne
private Dojo dojo;
private Workshop() {}
public Workshop(String name, Dojo dojo) {
this.name = name;
this.dojo = dojo;
}
}
So, I have a bidirectionnal 1:n relation between Dojo & Workshop. The #JsonIgnore annotation is here to avoid an infinite loop with the JSON Marshaller.
The repositories are standard
public interface WorkshopRepository extends CrudRepository<Workshop, Long> {}
Now my test : I want to update a workshop. Sounds good, doesn't work.
#Test
public void testUpdateWorkshop() throws Exception {
final String DOJO_NAME="My Dojo";
final String DOJO_LOCATION="Liege";
final String WORKSHOP_NAME="Stuff";
final String HOST_PORT="http://localhost:8080";
//creation of a dojo
final Dojo DOJO = dojoRep.save(new Dojo(DOJO_NAME,DOJO_LOCATION));
//creation of a workshop
Workshop workshop = workshopRep.save(new Workshop(WORKSHOP_NAME,DOJO));
String newValue = "After Test";
System.out.println("before update");
System.out.println(workshop.getName()+" == "+WORKSHOP_NAME);
Long oldID = workshop.getId();
//As you can see I didn't modify the workshop object
HttpEntity<Workshop> entity = new HttpEntity<Workshop>(workshop);
ResponseEntity<String> response = template.exchange(HOST_PORT+"/api/workshops/"+oldID, HttpMethod.PUT, entity, String.class, oldID);
assert response.getStatusCodeValue() == 200;
//re-Get the updated workshop
workshop = workshopRep.findOne(oldID);
System.out.println("after update");
System.out.println(workshop.getName()+" == "+WORKSHOP_NAME);
// as I didn't set the newValue, it must fail and workshop.getName() must stay equal to "Stuff".
Assert.assertEquals("Update does not work",newValue,workshop.getName());
}
I run mvn clean test and
before update
Stuff == Stuff
after update
My Dojo == Stuff
Failed tests:
WorkshopTests.testUpdateWorkshop:218 Update not work expected:<[After Test]> but was:<[My Dojo]>
So basically, I didn't change anything into my object but
Result code is 200.
It changed a property of my object.
The name was modified to take the dojo.name value !
Just ... Why ?
More information :
When I create a new workshop object with a new name (using the newValue ;-) ) and a new Dojo and try to update the existing workshop, the result is still the same. workshop.dojo unchanged and name copied from dojo.name. So basically, my update doesn't work.
I also try with mockMvc instead of TestRestTemplate like this.
mockMvc.perform(put(HOST_PORT+"/api/workshops/"+oldID)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(convertObjectToJsonBytes(workshop))
);
with the function
private byte[] convertObjectToJsonBytes(Object object) throws IOException {
ObjectMapper mapper = new ObjectMapper();
System.out.println("log my face ");
System.out.println(mapper.writeValueAsString(object));
return mapper.writeValueAsBytes(object);
}
And the log seems to rightly parse my object before update...
{"id":1,"name":"Stuff","dojo":{"id":1,"name":"My Dojo","location":"Liege","created":1500799092330}}
but still doesn't work :(
When I run the app (mvn spring-boot:run), a GET on localhost:8080/api/workshops/1 returns
{
"name" : "Stuff",
"_links" : {
"self" : {
"href" : "http://localhost-core:8080/api/workshops/1"
},
"workshop" : {
"href" : "http://localhost-core:8080/api/workshops/1"
},
"dojo" : {
"href" : "http://localhost-core:8080/api/workshops/1/dojo"
}
}
}
If I change the property name of my Dojo class by nameD and I update with a new name and a new Dojo (previously saved into DB), the name is updated but not the dojo.
To summarize my questions are :
Just ... why ?
What is the correct way to update an object like Workshop with a HTTP request ?
What is the correct way to test this update ?
Thanks to all and have a nice day ! :-)
I think it's because you are using bidirectional one-to-many association. In this case you have to provide linking/unlinking of entities by yourself. For example in the collection setter, like this:
#Data
#ToString(exclude = "slaves")
#Entity
public class Master {
#Id
#GeneratedValue
private Long id;
private String name;
#OneToMany(mappedBy = "master", cascade = {PERSIST, MERGE})
private List<Slave> slaves;
public void setSlaves(List<Slave> slaves) {
// link new slaves to this master
slaves.forEach(slave -> slave.setMaster(this));
// unlink prev slaves
if (this.slaves != null) this.slaves.forEach(slave -> slave.setMaster(null));
this.slaves = slaves;
}
}
#Data
#Entity
public class Slave {
#Id
#GeneratedValue
private Long id;
private String name;
#ManyToOne
private Master master;
}
Then you can store Slave:
POST http://localhost:8080/api/slaves
{
"name": "slave1"
}
// the same for salve2, slave3, slave4
Store Master:
POST http://localhost:8080/api/masters
{
"name": "master1",
"slaves": [
"http://localhost:8080/api/slaves/1",
"http://localhost:8080/api/slaves/2"
]
}
Update Master:
PUT http://localhost:8080/api/masters/1
{
"name": "master1u",
"slaves": [
"http://localhost:8080/api/slaves/3",
"http://localhost:8080/api/slaves/4"
]
}
PUT http://localhost:8080/api/masters/2
{
"name": "master2"
}
Or update Slave:
PUT http://localhost:8080/api/slaves/1
{
"name": "slave1u",
"master": "http://localhost:8080/api/masters/2"
}
PUT http://localhost:8080/api/slaves/2
{
"name": "slave2u",
"master": "http://localhost:8080/api/masters/2"
}
See working example.
Additional info

JPA (with Play!) bidirectional Many-to-Many - how to remove from both sides of the relationship?

I have the following mapping in my Play! app using JPA:
#Entity
public class Contact extends Model {
public String name;
#ManyToMany(mappedBy = "contacts", fetch = FetchType.EAGER)
public Set<Category> categories = new HashSet<Category>();
public void addCategory(Category c) {
this.categories.add(c);
if (!c.contacts.contains(this)) {
c.contacts.add(this);
}
}
#PreRemove
public void preRemove() {
for (Category c : this.categories) {
c.contacts.remove(this);
}
this.categories = null;
}
}
#Entity
public class Category extends Model {
public String name;
#ManyToMany
public Set<Contact> contacts = new HashSet<Contact>();
public void addContact(Contact c) {
this.contacts.add(c);
if (!c.categories.contains(this)) {
c.categories.add(this);
}
}
#PreRemove
protected void preRemove() {
for (Contact c : this.contacts) {
c.categories.remove(this);
}
this.contacts = null;
}
}
Deleting Category works fine, the relationship is updated correctly. If I delete a Contact however, I'm getting a constraint violation:
Caused by: org.h2.jdbc.JdbcBatchUpdateException: Referential integrity constraint violation: "FK34B085DF487AF903:
PUBLIC.CATEGORY_CONTACT FOREIGN KEY(CONTACTS_ID) REFERENCES PUBLIC.CONTACT(ID)"; SQL statement:
delete from Contact where id=? [23003-149]
How can I ensure that deleting a Contact will not delete the Category but will only remove the relationship?
EDIT: Duh! The issue was I also had a User object which had references to both Contact and Category. I missed clearing that relationship. The following is the change to preRemove() method in Contact class:
#PreRemove
public void preRemove() {
for (Category c : this.categories) {
c.contacts.remove(this);
}
this.user.contacts.remove(this);
for (Category c : this.user.categories) {
c.contacts.remove(this);
}
//It's important to save the user
this.user.save();
}
An other solution is to manually remove elements from the list and save it, before deleting the main entity :
while( !contact.categories.isEmpty() ) {
contact.categories.remove(0);
}
contact.save();
contact.delete();