9

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!

Maciej Kowalski
  • 25,605
  • 12
  • 54
  • 63
Manisha
  • 775
  • 6
  • 13
  • 30
  • whats inside customReportService.createCustomReport? – Maciej Kowalski May 30 '17 at 13:16
  • @MaciejKowalski I've edited my post. Please check – Manisha May 30 '17 at 13:21
  • Please refer related post : https://stackoverflow.com/questions/3927091/save-child-objects-automatically-using-jpa-hibernate – RoshanKumar Mutha May 30 '17 at 13:36
  • @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! – Manisha May 30 '17 at 14:05

2 Answers2

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.

Blip
  • 3,061
  • 5
  • 22
  • 50
Maciej Kowalski
  • 25,605
  • 12
  • 54
  • 63
  • 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 operation – Manisha May 31 '17 at 11:22
  • yes there are problems with that implicit merging. Personally i had to implement by own merge operations. – Maciej Kowalski May 31 '17 at 12:25
  • Oh okay. I was expecting JPA to handle merge of related entities.Thanks for the clarification! – Manisha May 31 '17 at 12:43
  • 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 feature – Maciej Kowalski May 31 '17 at 12:46
  • @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. – jumping_monkey Jun 16 '20 at 01:35
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;
}
Ilya Lysenko
  • 1,772
  • 15
  • 24
  • 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. – jumping_monkey Jun 16 '20 at 01:39