Reputation: 7980
In spring boot with Java I have an Entity class that contains a list of elements. I want to create an endpoint (POST? or PATCH?) from which I can add a new element to those present:
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import jakarta.persistence.*;
import lombok.*;
import org.apache.commons.lang3.StringUtils;
import org.cnr.plantvocdb.enums.LeafHabitus;
import org.cnr.plantvocdb.enums.PlantsRanks;
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
@Table(name = "plants_voc")
public class PlantEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name="id", length = 50, nullable = false, updatable = false)
private UUID id;
@Column(name="ipni", length = 50)
private String ipni;
@Setter(AccessLevel.NONE)
@Column(name="full_name_plain", length = 50)
private String fullNamePlain;
@Setter(AccessLevel.NONE)
@Column(name="full_name_no_authors_plain", length = 50)
private String fullNameNoAuthorsPlain;
@Setter(AccessLevel.NONE)
@Column(name="plant_name", length = 30, nullable = false)
private String name;
@Setter(AccessLevel.NONE)
@Column(name="family", length = 30, nullable = false)
private String family;
@Setter(AccessLevel.NONE)
@Column(name="genus", length = 30, nullable = false)
private String genus;
@Setter(AccessLevel.NONE)
@Column(name="species", length = 30, nullable = false)
private String species;
@Column(name="valid_nomenclature")
private boolean validNomenclature;
@Column(name="rank", length = 20)
@Enumerated(EnumType.STRING)
private PlantsRanks rank;
@Column(name="leaf_habitus", length = 20)
@Enumerated(EnumType.STRING)
private LeafHabitus leafHabitus;
@OneToMany(
fetch = FetchType.EAGER,
cascade = CascadeType.ALL,
orphanRemoval = true, // Ensures childs are deleted when plant is deleted
mappedBy = "plant"
)
private List<PlantEmitterEntity> emitter;
@ElementCollection
@CollectionTable(
name = "synonyms", // Nome della tabella intermedia
joinColumns = @JoinColumn(name = "fk_synonyms_plant_id"),
foreignKey = @ForeignKey(name = "FK_synonyms_plant")
)
@Column(name="synonyms")
private Set<String> synonyms;
@Column(name="created_datetime_utc", updatable = false) // creation_datetime_utc
private OffsetDateTime createdDatetimeUTC;
@Column(name="updated_datetime_utc") // last_modified_datetime_utc
private OffsetDateTime updatedDatetimeUTC;
public void setName(String name) {
this.name = StringUtils
.normalizeSpace(name.toLowerCase());
}
public void setFamily(String family) {
this.family = StringUtils
.normalizeSpace(StringUtils
.capitalize(family.toLowerCase()));
}
public void setGenus(String genus) {
this.genus = StringUtils
.normalizeSpace(StringUtils
.capitalize(genus.toLowerCase()));
}
public void setSpecies(String species) {
this.species = StringUtils
.normalizeSpace(species.toLowerCase());
}
public void setFullNamePlain(String fullNamePlain) {
this.fullNamePlain = StringUtils
.normalizeSpace(fullNamePlain);
}
public void setFullNameNoAuthorsPlain(String fullNameNoAuthorsPlain) {
this.fullNameNoAuthorsPlain = StringUtils
.normalizeSpace(fullNameNoAuthorsPlain);
}
public void addEmitter(PlantEmitterEntity newEmitter) {
emitter.add(newEmitter);
}
}
The Entity class method addEmitter
was specifically created to do this, that is, to add a new PlantEmitterEntity
object to the existing ones
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name="emitters")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PlantEmitterEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name="emits", length = 50)
private boolean emits;
@Column(name="doi")
private String doi;
@ManyToOne(
fetch = FetchType.LAZY,
cascade = CascadeType.ALL
)
@JoinColumn(
name = "fk_emitters_plant_id",
nullable = false,
updatable = true,
insertable = true,
foreignKey = @ForeignKey(name = "FK_emitters_plant"))
private PlantEntity plant;
}
For this purpose, in the controller I created a special endpoint
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.apache.commons.lang3.StringUtils;
import org.cnr.plantvocdb.dto.PlantInfoDTO;
import org.cnr.plantvocdb.dto.RequestPlantDTO;
import org.cnr.plantvocdb.dto.ResponsePlantDTO;
import org.cnr.plantvocdb.enums.LeafHabitus;
import org.cnr.plantvocdb.enums.PlantsEmitterType;
import org.cnr.plantvocdb.enums.PlantsRanks;
import org.cnr.plantvocdb.exceptions.ErrorResponseDTO;
import org.cnr.plantvocdb.exceptions.PlantNotFoundException;
import org.cnr.plantvocdb.service.PlantsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.text.MessageFormat;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@RestController
@RequestMapping("/api/v1/isoprene")
//@Tag(name = "Plant isoprene APIs")
public class PlantsController {
private final PlantsService service;
@Autowired
public PlantsController(PlantsService service) {
this.service = service;
}
/*
*
* Endpoints POST
*
* */
@Tag(name = "Collection of methods used for creating data")
@PostMapping("/plants/emitter")
public ResponseEntity<ResponsePlantDTO> addEmitterById(
@RequestParam(value = "id") UUID id,
@RequestParam(value = "emits") boolean emits,
@RequestParam(value = "doi") String doi
){
Optional<ResponsePlantDTO> optionalResponsePlantDTO = service.findPlantById(id);
if(optionalResponsePlantDTO.isPresent()){
ResponsePlantDTO plantDTO = optionalResponsePlantDTO.get();
service.addEmitter(plantDTO, emits, doi);
}
//TODO: end with updated resource or Exeception
return null;
}
}
In service I do two operations. The first I delete the record using its ID and the second I save the record again (with the added element) always using its ID
import org.apache.commons.lang3.StringUtils;
import org.cnr.plantvocdb.dto.PlantEmitterDTO;
import org.cnr.plantvocdb.dto.PlantInfoDTO;
import org.cnr.plantvocdb.dto.RequestPlantDTO;
import org.cnr.plantvocdb.dto.ResponsePlantDTO;
import org.cnr.plantvocdb.entity.PlantEmitterEntity;
import org.cnr.plantvocdb.entity.PlantEntity;
import org.cnr.plantvocdb.enums.LeafHabitus;
import org.cnr.plantvocdb.enums.PlantsEmitterType;
import org.cnr.plantvocdb.enums.PlantsRanks;
import org.cnr.plantvocdb.repository.PlantsRepository;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Service
public class PlantsService {
private final PlantsRepository repository;
private final ModelMapper mapper;
@Autowired
public PlantsService(PlantsRepository repository,
ModelMapper mapper) {
this.repository = repository;
this.mapper = mapper;
}
// POST
public ResponsePlantDTO addEmitter(
ResponsePlantDTO plantDTO,
boolean emits,
String doi
){
PlantEntity plantEntity = mapper.map(plantDTO, PlantEntity.class);
plantEntity.setUpdatedDatetimeUTC(OffsetDateTime.now(ZoneOffset.UTC));
plantEntity.addEmitter(PlantEmitterEntity
.builder()
.emits(emits)
.doi(doi)
.build()
);
// iterate over the set to add plantEntity
plantEntity.getEmitter().forEach(it -> it.setPlant(plantEntity));
// delete plant by ID in order to save the updated plant
repository.deleteById(plantEntity.getId());
PlantEntity updatedPlantEntity = repository.save(plantEntity);
// TODO: return new object
return null;
}
}
When I run the service I get this error when I try to create the object with the updated list
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [org.cnr.plantvocdb.entity.PlantEntity#9333c963-bba1-4780-a16c-01a1d6ce7920]
Upvotes: 1
Views: 45
Reputation: 111
Instead of deleting and re-saving, simply update the object and add a new element to the list.
Updated addEmitter()
Method in PlantsService
.
public ResponsePlantDTO addEmitter(UUID plantId, boolean emits, String doi) {
// Find the existing record in the database
Optional<PlantEntity> optionalPlant = repository.findById(plantId);
if (optionalPlant.isEmpty()) {
throw new PlantNotFoundException("Plant with ID " + plantId + " not found.");
}
// Retrieve the entity from the database
PlantEntity plantEntity = optionalPlant.get();
plantEntity.setUpdatedDatetimeUTC(OffsetDateTime.now(ZoneOffset.UTC));
// Create a new PlantEmitterEntity object
PlantEmitterEntity newEmitter = PlantEmitterEntity.builder()
.emits(emits)
.doi(doi)
.plant(plantEntity) // Associate it with the existing plant
.build();
// Add the new emitter to the list
plantEntity.getEmitter().add(newEmitter);
// Save the updated entity (no need to delete it first)
PlantEntity updatedPlant = repository.save(plantEntity);
// Convert back to DTO and return
return mapper.map(updatedPlant, ResponsePlantDTO.class);
}
Controller
Now, the controller should pass plantId
to the service method:
@PostMapping("/plants/emitter")
public ResponseEntity<ResponsePlantDTO> addEmitterById(
@RequestParam(value = "id") UUID id,
@RequestParam(value = "emits") boolean emits,
@RequestParam(value = "doi") String doi
) {
ResponsePlantDTO updatedPlant = service.addEmitter(id, emits, doi);
return ResponseEntity.ok(updatedPlant);
}
Why is POST
used here?
PlantEntity
; instead, you are adding a new PlantEmitterEntity
object to the emitter list.PATCH
is meant for partial updates of fields, not for extending a listPATCH
is ideal for modifying specific attributes, such as name, validNomenclature, or rank.POST
does).Thus, POST
is the logical choice because it aligns with HTTP
semantics and RESTful
standards.
UPDATE:
Maybe the issue is here:
PlantEntity plantEntity = mapper.map(plantDTO, PlantEntity.class);
You’re taking a DTO
, mapping it into a new PlantEntity
object, and then immediately deleting the old one from the database:
repository.deleteById(plantEntity.getId());
PlantEntity updatedPlantEntity = repository.save(plantEntity);
The problem is that Hibernate sees this object as new, and because of orphanRemoval = true
, it’s likely removing all associated PlantEmitterEntity records.
Instead of creating a new PlantEntity
, try retrieving it from the database, adding the new emitter
Upvotes: 2