Save an entity and all its related entities in a single save in spring boot
Asked Answered
O

2

10

I'm using Spring Boot,REST and JPA to build my application. In app, there are 2 entities with one to many relationship.

Entity 1 :

@Entity
@Table( name = "report")
@JsonIgnoreProperties(ignoreUnknown = true)
public class CustomReport {

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "REPORT_SEQ")
@SequenceGenerator(sequenceName = "REPORT_SEQ", allocationSize = 1, name = "REPORT_SEQ")
private Long id;

private String name;
private Long createdBy;
private Timestamp lastModifiedTimestamp;


@OneToMany(mappedBy = "customReport", cascade = CascadeType.ALL)
private Set<CustomReportActivity> customReportActivitySet;



public Set<CustomReportActivity> getCustomReportActivitySet() {
    return customReportActivitySet;
}

public void setCustomReportActivitySet(Set<CustomReportActivity> customReportActivitySet) {
    this.customReportActivitySet = customReportActivitySet;
}



public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public Long getCreatedBy() {
    return createdBy;
}

public void setCreatedBy(Long createdBy) {
    this.createdBy = createdBy;
}

public Timestamp getLastModifiedTimestamp() {
    return lastModifiedTimestamp;
}

public void setLastModifiedTimestamp(Timestamp lastModifiedTimestamp) {
    this.lastModifiedTimestamp = lastModifiedTimestamp;
}

}

Entity 2:

@Entity
@Table( name = "report_activity")
public class CustomReportActivity {

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "REPORT_ACTIVITY_SEQ")
@SequenceGenerator(sequenceName = "REPORT_ACTIVITY_SEQ", allocationSize = 1, name = "REPORT_ACTIVITY_SEQ")
private Long id;

String activityName;

@ManyToOne
@JoinColumn( name="report_id" )
@JsonBackReference
private CustomReport customReport;

public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

public String getActivityName() {
    return activityName;
}

public void setActivityName(String activityName) {
    this.activityName = activityName;
}

public CustomReport getCustomReport() {
    return customReport;
}

public void setCustomReport(CustomReport customReport) {
    this.customReport = customReport;
}

}

And my request JSON is as follows :

{
   "name": "test report",
   "createdBy" : 129,
   "customReportActivitySet": [
        {"activityName":"a"},
        {"activityName":"b"},
        {"activityName":"c"},
        {"activityName":"d"},
        {"activityName":"e"}
    ]  
}

I want to save both entities in one shot. I've implemented the save functionality in following way:

@RequestMapping(value="/save", method = RequestMethod.POST)
public ResponseEntity<?> addReport(@RequestBody CustomReport customReport) {
    return new ResponseEntity<>(customReportService.createCustomReport(customReport), HttpStatus.CREATED);

}

CustomReportService method:

 public CustomReport createCustomReport(CustomReport customReport) {
    return customReportRepository.save(customReport);
}

CustomRepository:

public interface CustomReportRepository extends CrudRepository<CustomReport, Long> {

}

But I'm getting the constraint violation exception with this:

java.sql.SQLIntegrityConstraintViolationException: ORA-01400: cannot insert NULL into ("REPORT_ACTIVITY"."REPORT_ID")

Is it possible to save both entities in one save operation?

Please help!

Opportunity answered 30/5, 2017 at 13:7 Comment(4)
whats inside customReportService.createCustomReport?Ethmoid
@MaciejKowalski I've edited my post. Please checkOpportunity
Please refer related post : #3927591Detraction
@MaciejKowalski I was creating another method in service to populate all properties of child entity. Didn't know that we could do it by just adding custom report relationship in child entity before save. Your solution worked. Thank you so much!Opportunity
E
7

You would have to add a small piece of code which would populate each CustomReportActivity within the CustomReport instance. Only then the persistence provide can successfully perform the cascade save operation:

public CustomReport createCustomReport(CustomReport customReport) {
   customReport.getCustomReportActivitySet.forEach((activity) -> {
      activity.setCustomReport(customReport);
   });

   return customReportRepository.save(customReport);
}

The bottom line is that the dependencies have to be set on both sides of the relationship.

Ethmoid answered 30/5, 2017 at 13:27 Comment(5)
As per documentation, CrudRepository.save(entity) saves as well as update the entity.But when i use save() to update the custom report entity, my primary key constraint on report_activity table is getting violated .The primary key for report_activity table is (report_id,activity_name). It seems JPA is trying to insert a new row in report_activity table instead of updating the existing row. Please tell me why is it not doing the merge operationOpportunity
yes there are problems with that implicit merging. Personally i had to implement by own merge operations.Ethmoid
Oh okay. I was expecting JPA to handle merge of related entities.Thanks for the clarification!Opportunity
JPA never takes care of that implicitly (you have to call the merge yourself).. spring-data-jpa should.. but like i said there are some problems with that featureEthmoid
@MaciejKowalski is right, since this is a bidirectional many-to-one association, you need to update both sides of the association. That's the only reason it was not working.Mesolithic
H
3

Try this sample, in my case it worked as expected, child entities are saved automatically in a single save operation with creating relations to the parent entity:

@Entity
public class Parent {
    @Id
    private Long id;

    @JoinColumn(name = "parentId")
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Child> children;
}

@Entity
public class Child {
    @Id
    private Long id;
    private Long parentId;
}
Higgler answered 6/4, 2020 at 20:4 Comment(1)
It might be ok for your use case, but it's not the same use case as the OP as you are using a unidirectional one-to-many association as opposed to a bidirectional association. So, setting only one-side of the association will work for you.Mesolithic

© 2022 - 2024 — McMap. All rights reserved.