-2

I have a parent which stores a list of children. When i update the children(add/edit/remove), is there a way to automatically decide which child to remove or edit based on the foreign key? Or do i have to manually check through all the child to see which are new or modified?

Parent Class

@Entity
@EntityListeners(PermitEntityListener.class)
public class Permit extends Identifiable {

    @OneToMany(fetch = FetchType.LAZY, cascade=CascadeType.ALL, mappedBy = "permit")
    private List<Coordinate> coordinates;
}

Child Class

@Entity
public class Coordinate extends Identifiable {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "permit_id", referencedColumnName = "id")
    private Permit permit;

    private double lat;

    private double lon;
}

Parent's Controller

@PutMapping("")
public @ResponseBody ResponseEntity<?> update(@RequestBody Permit permit) {

    logger.debug("update() with body {} of id {}", permit, permit.getId());
    if (!repository.findById(permit.getId()).isPresent()) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
    }

    Permit returnedEntity = repository.save(permit);
    repository.flush();
    return ResponseEntity.ok(returnedEntity);
}

=EDIT=

Controller Create

@Override
    @PostMapping("")
    public @ResponseBody ResponseEntity<?> create(@RequestBody Permit permit) {

        logger.debug("create() with body {}", permit);
        if (permit == null || permit.getId() != null) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
        }

        List<Coordinate> coordinates = permit.getCoordinates();
        if (coordinates != null) {
            for (int x = 0; x < coordinates.size(); ++x) {
                Coordinate coordinate = coordinates.get(x);
                coordinate.setPermit(permit);
            }
        }

        Permit returnedEntity = repository.save(permit);
        repository.flush();
        return ResponseEntity.ok(returnedEntity);
    }

Controller Update

@PutMapping("")
public @ResponseBody ResponseEntity<?> update(@RequestBody Permit permit) {

    logger.debug("update() with body {} of id {}", permit, permit.getId());
    if (!repository.findById(permit.getId()).isPresent()) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
    }

    List<Coordinate> repoCoordinate = coordinateRepository.findByPermitId(permit.getId());
    List<Long> coordinateIds = new ArrayList<Long>();
    for (Coordinate coordinate : permit.getCoordinates()) {
        coordinate.setPermit(permit);
        //if existing coordinate, save the ID in coordinateIds
        if (coordinate.getId() != null) {
            coordinateIds.add(coordinate.getId());
        }
    }
    //loop through coordinate in repository to find which coordinate to remove
    for (Coordinate coordinate : repoCoordinate) {
        if (!(coordinateIds.contains(coordinate.getId()))) {
            coordinateRepository.deleteById(coordinate.getId());
        }
    }

    Permit returnedEntity = repository.save(permit);
    repository.flush();
    return ResponseEntity.ok(returnedEntity);
}

I have tested this and it is working, is there no simplified way of doing this?

Cherple
  • 725
  • 3
  • 10
  • 24

2 Answers2

2

You were close to the solution. The only thing you're missing is orphanRemoval=true on your one to many mapping:

@Entity
@EntityListeners(PermitEntityListener.class)
public class Permit extends Identifiable {

    @OneToMany(mappedBy = "permit", cascade=CascadeType.ALL, orphanRemoval=true)
    private List<Coordinate> coordinates;

}

Flagging the mapping for orphan removal will tell the underlying ORM to delete any entities that no longer belong to any parent entity. Since you removed a child element from the list, it will be deleted when you save the parent element. Creating new elements and updating old is based on the CascadeType. Since you have CascadeType.ALL all elements in the list without an ID will be saved to the database and assigned a new ID when you save the parent entity, and all elements that are already in the list and have an ID will be updated.

On a side note, you might need to update the setter method for List coordinates to look something like:

public void setCoordinates(List<Coordinates> coordinates) {
    this.coordinates = coordinates;
    this.coordinates.forEach(coordinate -> coordinates.setPermit(this));
}

Or simply use @JsonManagedReference and @JsonBackReference if you're working with JSON.

Nick
  • 218
  • 1
  • 2
  • 12
0

I have a parent which stores a list of children.

Lets write the DDL for it.

TABLE parent (
  id integer pk
)
TABLE child(
  id integer pk
  parent_id integer FOREIGN KEY (parent.id)
)

When i update the children(add/edit/remove), is there a way to automatically decide which child to remove or edit based on the foreign key?

Assuming you have a new child #5 bound to the parent #2 and:

  1. The FK in the DDL is correctly
  2. The entitys knows the FK
  3. You are using the same jpa-context
  4. The transaction is executed correctly

Then every call to parent.getChilds() must(!) return all the entitys that are existing before your transaction has been executed and the same instance of the entity that you have just committed to the database.

Then, if you remove child #5 of parent #2 and the transaction executed successfully parent.getChilds() must return all entitys without child #5.

Special case:

If you remove parent #2 and you have cascade-delete in the DDL as well as in the Java-Code all childrens must be removed from the Database as well as the parent #2 in the Database you just removed. In this case the parent #2 is not bound anymore to the jpa-context and all the childrens of parent #2 are not bound anymore to the jpa-context.

=Edit=

You could use merge. This will work for constructs like this:

POST {
  "coordinates": [{
    "lat":"51.33",
    "lon":"22.44"
  },{
    "lat":"50.22",
    "lon":"22.33"
  }]
}

It will create one row in table "permit" and two rows in table "coordinate", both coordinates are bound to the permit-row. The result will include the ids set.

But: You will have to do the validation work (check that id is null, check that coordinates not refering different permit, ...)!

The removal of coordinates must be done using the DELETE method:

DELETE /permit/972/coordinate/3826648305
Grim
  • 1,938
  • 10
  • 56
  • 123
  • What i am trying to do is something more complex.. Because when user modify the record, they are able to add a new child, remove an existing child, and edit an existing child, all at the same time.. Is there a way in Spring where it will automatically compare this new edit with all existing records in the repository/DB and make the changes automatically? Or will i have to manually loop through all the child records in the DB and compare it with what the user just edited? – Cherple Feb 19 '19 at 01:14
  • @Cherple Spring will automatically do this for you. – Grim Feb 19 '19 at 05:46
  • I just did a test, i removed child#1, modified child#2, added new child#3. I should be expecting only 2 child row in my child table, but instead child#1 is still there, and child#2 and child#3's foreign key (parent_id) is now null. – Cherple Feb 20 '19 at 04:42
  • Infact, when i first added the parent, i had to loop through each child and do a setParent(parent) in order for the parent_id to be populated. – Cherple Feb 20 '19 at 05:17
  • @Cherple In any relation you have a "owning-side", the table "child" is the owning side for the relation because the table of child holds a field that "owns" the relation. Only the owning side is the "source of truth" for relation issues. A change of the child-collection of the parent is not recommendet. Not sure if this explains your issue. See this for more details: https://stackoverflow.com/questions/2749689/ – Grim Feb 20 '19 at 13:10
  • I've edited my question with the controller code for create and update which i have tested and it's working as intended. But is there a simplier way of doing this? Imagine having a parent with multiple child fields, this will create chaos in the code for tonnes of loops. – Cherple Feb 21 '19 at 01:28