I have many tables that belong to the same Project by ID. When I reload a Project with an existing ID, I need to clear all entities from the database.
Controller:
#CrossOrigin
#RequestMapping(value = "projects", method = RequestMethod.POST)
public ResponseEntity<?> uploadProject(MultipartFile file) {
JsonDataDto projectDto = converterService.convertToDto(file, JsonDataDto.class);
if(projectRepository.exists(projectDto.getId())) {
// Delete all project entities from DB
projectService.delete(projectDto.getId());
}
// Save project to DB
importService.import(projectDto);
}
Project Service (delete):
#Service
#Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
public class GenericProjectService implements ProjectService {
// Fields
#Override
public void delete(UUID projectId) {
entity1Repository.deleteByProjectId(projectId)
...
// Most entities are associated with a project by a foreign key.
// Some entities are not linked by a foreign key and are removed manually (entity1Repository for example)
projectRepository.delete(projectId);
}
}
Import Service (save):
#Service
public class GenericImportService implements ImportService {
// Fields
#Override
#Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
public void import(JsonDataDto projectDto) {
Collection<Entity1> entity1 = projectDto.getEntity1()
.stream().map(e -> e1Repository.save(e1Mapper.to(e))).collect(...);
Map<UUID, Type> types = new HashMap<>();
Map<UUID, TypeDto> typeDtosById = projectDto.getTypes().stream()
.collect(Collectors.toMap(TypeDto::getId, Function.identity()));
for (UUID typeId : typeDtosById.keySet()) {
saveType(typeId, typeDtosById, types, ...);
}
}
private void saveType(...) {
Type type = new Type();
// Set fields and relations
// Get exception here
type = typeRepository.save(type);
types.put(typeId, type);
}
}
Type Class:
#Entity
#Data
#Table(name = "...", schema = "...")
public class Type {
#Id
private TypePK typeId;
/*
#Data
#NoArgsConstructor
#AllArgsConstructor
#Embeddable
public class TypePK implements Serializable {
#Type(type = "pg-uuid")
#Column(name = "id")
private UUID id;
#ManyToOne(cascade = CascadeType.ALL,fetch = FetchType.EAGER)
#JoinColumn(name = "project_id", insertable = false, updatable = false)
private Project project;
}
*/
// Fields
#org.hibernate.annotations.Type(type = "pg-uuid")
#Column(name = "parent_id")
private UUID parentId;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumns({
#JoinColumn(name = "parent_id", referencedColumnName = "id", updatable = false, insertable = false),
#JoinColumn(name = "project_id", referencedColumnName = "project_id", updatable = false, insertable = false)})
private Type parent;
}
When the project does not exist in database, the save is successful. If I delete project from controller, it will also be successfully deleted from the database.
If project exists in database and I try to save it again, I get an error: "Unable to find package.Type with id TypePK(id=7e8281fe-77b8-475d-8ecd-c70522f5a403, project=Project(id=8d109d33-e15e-ca81-5f75-09e00a81a194))"
The entities are removed from the database, but the save transaction is rolled back.
I tried to force close the transaction after delete but it did not help:
public void delete(UUID projectId) {
TransactionStatus ts = TransactionAspectSupport.currentTransactionStatus();
entity1Repository.deleteByProjectId(projectId)
...
ts.flush();
}
The only way I found is, in fact, a crutch. I just wait a couple of seconds before starting save:
if(projectRepository.exists(projectDto.getId())) {
// Delete all project entities from DB
projectService.delete(projectDto.getId());
}
// Any timer
DateTime waitFor = DateTime.now().plusSeconds(2);
while(DateTime.now().isBefore(waitFor)) { }
// Save project to DB
importService.import(projectDto);
I managed to solve the problem using the suggestion in this comment: https://stackoverflow.com/a/14369708/10871976
I added an adapter to the delete transaction. On successful deletion in the "afterCompletion" method, I call project saving.
Related
I'm trying to create a Rest API for a school project.Therefor I'm trying to save/edit a nested Object.
I have two bidirectional entities which look like this:
EntityA
#Entity
public class EntityA {
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Id
#Column(name = "id", nullable = false)
#JsonProperty("id")
private int id;
#Column(name = "field1", nullable = false, length = -1)
#JsonProperty("field1")
private String field1;
#Column(name = "field2", nullable = false, length = -1)
#JsonProperty("field2")
private String field2;
#OneToMany(mappedBy = "entityA", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JsonProperty("entityB")
private List<EntityB> entityB;
public EntityA() {
}
//Getter+Setter
}
EntityB
#Entity
public class EntityB {
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Id
#Column(name = "id", nullable = false)
#JsonProperty("id")
private int id;
#Column(name = "field1", nullable = false)
#JsonProperty("field1")
private Date field1;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(...)
#JsonProperty("entityA")
private EntityA entityA;
public EntityB() {
}
//Getter+Setter
}
As RequestBody I will get JSON which should look like this.
{
"field1": "Test",
"field2": "User",
"entityB": [
{
"field1": "30.03.2022"
}
]
}
Right now Spring will automatically map the fields but as soon I try to save it to my DB I will get an error, because the relation in EntityB for EntityA is empty.
I've seen a solution, that I should loop through the EntityB list and add EntityA. I tried it with a for-each but it still sais it null.
What am I doing wrong?
public EntityA createEntityA(EntityA entityA) {
for(EntityB entityB : entityA.getEntityB()){
entityB.setEntityA(entityA);
}
return entityARepository.save(entityA);
}
Edit:
Controller
#PostMapping(value = {"/json/entitya/"})
#ResponseBody
public EntityA createEntityAJson(#RequestBody EntityA entityA) {
return entityAService.createEntityA(entityA);
}
Service
#Service
public class EntityAService {
#Autowired
private EntityARepository entityARepository;
public EntityA createEntityA(EntityA entityA) {
return entityARepository.save(entityA); //in this line the error appears
}
}
Error message
null value in column "entityA" violates not-null constraint
#Service
public class EntityAService {
#Autowired
private EntityARepository entityARepository;
#Autowired
private EntityBRepository entityBRepository;
public EntityA createEntityA(EntityA entityA) {
// create an empty arrayList to stock the entities B retrieveed from the DB
List<EnityB> lst = new ArrayList<>();
// get the entities B from the JSON and sabe it to the DB
for(EntityB entityB : entityA.getEntityB()){
entityB.setEntityA(entityA);
entityBRepository.save(entityB); // you should save entities B to the DataBase before
Optional<EntityB > opt = entityBRepository.findById(entityB.getId());
EntityB b = opt.get();
// add the entities B retrieved from the DB to the arrayList
lst.add(b);
}
// set the EntityB list with the new List from the DB ( include ids ..)
entityA.setEntityB(lst);
// save the entityA to the DB
return entityARepository.save(entityA);
}
}
I'm guessing that what is happening here is that the id fields which are of a non-nullable datatype or some other hidden field from the JPA annotations get set to the wrong value by the json deserialization for JPA to understand that they are new entities. Creating these entities manually in the Java code might solve the issue.
You shouldn't reuse your entity classes as data transfer object for your API. Having classes containing both database-specific annotations and annotations for JSON serialization is a bad idea and it goes against the single-responsibility principle (SRP).
Create separate DTO classes for your API endpoint, then read the entities from the database an copy the values from the DTO object to the entities before saving.
// Receive DTO
// Read entity from DB if update or create new entities if insert
// Copy values from DTO to entitiy
// Save entity
I think your problems will go away if you apply this pattern.
I need based on parameter retrieve or not some associations from an entity. In the bellow example I need to get the records list only if a parameter is passed through my api. Can you recommend a way of achieving this using hibernate/spring data? I'm looking for the most clean and spring data-like approach.
public class Customer {
private UUID id;
#OneToMany(mappedBy = "customer")
private List<Record> records = new ArrayList<>();
}
public class Record {
private UUID id;
#Column(name = "customer_id", length = 36, columnDefinition = "varchar(36)", nullable = false)
private UUID customerId;
#JoinColumn(name = "customer_id", insertable = false, updatable = false)
private Customer customer;
}
My Repository is empty:
public interface CustomerRepository extends JpaRepository<Customer, UUID> {
}
On my service I'm doing something like:
Customer customer = customerRepository.findById(customerId).orElseThrow(() -> new CustomerNotFoundException("customerId", customerId));
But what I would like to do is something like:
if (showRecords) {
Customer customer = customerRepository.findById(customerId).orElseThrow(() -> new CustomerNotFoundException("customerId", customerId));
} else {
Customer customer = customerRepository.findByIdWithoutAssociations(customerId).orElseThrow(() -> new CustomerNotFoundException("customerId", customerId));
}
How about using the base findById to return just the Customer object and have another method findWithRecordsById to return customer+records using #EntityGraph?
public interface CustomerRepository extends JpaRepository<Customer, UUID>{
#EntityGraph(attributePaths = {"records"})
Customer findWithRecordsById(UUID id);
...
}
Note: for simplyfication i have changed some variables names and get rid of unnecessary code to show my issue.
I have two repositories:
#Repository
public interface CFolderRepository extends CrudRepository<CFolder, Long>, QuerydslPredicateExecutor<CFolder> {}
#Repository
public interface CRepository extends JpaRepository<C, Long>, CFinder, QuerydslPredicateExecutor<C> {}
The class C is:
#FilterDef(name = "INS_COMPANY_FILTER", parameters = {#ParamDef(name = "insCompanies", type = "string")})
#Filter(name = "INS_COMPANY_FILTER", condition = " INS_COMPANY in (:insCompanies) ")
#NoArgsConstructor
#AllArgsConstructor
#Audited
#AuditOverrides({#AuditOverride(forClass = EntityLog.class),
#AuditOverride(forClass = MultitenantEntityBase.class)})
#Entity
#Table(name = "INS_C")
#Getter
public class C extends MultitenantEntityBase {
#OneToOne(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "C_FOLDER_ID")
private CFolder cFolder;
public void addFolder(List<String> clsUrl){
this.cFolder = CFolder.createFolder(clsUrl);
}
}
CFolder is:
#Getter
#NoArgsConstructor
#Audited
#AuditOverride(forClass = EntityLog.class)
#Entity
#Table(name = "C_FOLDER")
#AllArgsConstructor
public class CFolder extends EntityBase {
#Column(name = "CREATION_FOLDER_DATE_TIME", nullable = false)
private LocalDateTime creationFolderDateTime;
#Column(name = "UPDATED_FOLDER_DATE_TIME")
private LocalDateTime updatedFolderDateTime;
#Column(name = "FOLDER_CREATED_BY", nullable = false)
private String folderCreatedBy;
#Column(name = "FOLDER_UPDATED_BY")
private String folderUpdatedBy;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "cFolder", fetch = FetchType.EAGER)
#NotAudited
private Set<FolderDocument> folderDocuments = new HashSet<>();
public static CFolder createFolder(List<String> clsUrl){
CFolder cFolder = new CFolder(LocalDateTime.now(), null, SecurityHelper.getUsernameOfAuthenticatedUser(), null, new HashSet<>());
createFolderDocuments(clsUrl, cFolder);
return cFolder;
}
public void updateFolder(List<String> clsUrl){
this.updatedFolderDateTime = LocalDateTime.now();
this.folderUpdatedBy = SecurityHelper.getUsernameOfAuthenticatedUser();
this.folderDocuments.clear();
createFolderDocuments(clsUrl, this);
}
private static void createFolderDocuments(List<String> clsUrl, CFolder cFolder) {
int documentNumber = 0;
for (String url : clsUrl) {
documentNumber++;
cFolder.folderDocuments.add(new FolderDocument(cFolder, documentNumber, url));
}
}
}
FolderDocument is:
#Getter
#NoArgsConstructor
#AllArgsConstructor
#Audited
#AuditOverride(forClass = EntityLog.class)
#Entity
#Table(name = "FOLDER_DOCUMENT")
public class FolderDocument extends EntityBase {
#ManyToOne
#JoinColumn(name = "C_FOLDER_ID", nullable = false)
private CFolder cFolder;
#Column(name = "DOCUMENT_NUMBER", nullable = false)
private int documentNumber;
#Column(name = "URL", nullable = false)
private String url;
}
And finally we have a service in which i use these entities and try to save/load them to/from database:
#Service
#AllArgsConstructor(onConstructor = #__(#Autowired))
public class CFolderService {
private final CRepository cRepository;
private final CommunicationClServiceClient communicationServiceClient;
private final CFolderRepository cFolderRepository;
public List<ClDocumentDto> getClCaseFolder(Long cId) {
C insCase = cRepository.findCById(cId);
List<ClDocumentDto> clDocumentsDto = getClDocuments(insCase.getCNumber()); // here, the object has one cFolder, but many FolderDocument inside of it
return clDocumentsDto;
}
#Transactional
public void updateCFolder(Long cId) {
C insC = cRepository.findCById(cId);
List<ClDocumentDto> clDocumentsDto = getClDocuments(insC.getCNumber());
List<String> clsUrl = clDocumentsDto.stream().filter(c -> "ACTIVE".equals(c.getCommunicationStatus())).map(ClDocumentDto::getUrl).collect(Collectors.toList());
if (Objects.isNull(insC.getCFolder())) {
insC.addFolder(clsUrl);
} else {
insC.getCFolder().updateFolder(clsUrl);
}
cFolderRepository.save(insC.getCFolder()); // here it saves additional FolderDocument instead of updateing it
cRepository.save(insC); // need second save, so can get these collection in getClaimCaseFolder successfully
}
}
I have two issues inside. In the example i was trying to clear the objects that i found from DataBase and create new ones.
1)
First is that i have to make two save operation to successfully restore the object in getClCaseFolder method (outside transactional).
2)
Second is that everytime i am saving - i get additional FolderDocument object pinned to CFolder object inside C object. I want to clear this collection and save new one.
I am not sure why hibernate does not update this object?
EDIT:
I think that i do sth like:
cRepository.save(insC);
instead of this.folderDocuments.clear();
i can do:
for(Iterator<FolderDocument> featureIterator = this.folderDocuments.iterator();
featureIterator.hasNext(); ) {
FolderDocument feature = featureIterator .next();
feature.setCFolder(null);
featureIterator.remove();
}
But i get eager fetching, why lazy wont work? There is an error using it.
Check whether you are setting ID in that Entity or not.
If ID is present/set in entity and that ID is also present in DB table then hibernate will update that record, But if ID is not present/set in Entity object the Hibernate always treat that object as a new record and add new record to the table instead of Updating.
Attachement class:
#Entity
#Table(name="attachments")
#Getter
#Setter
public class AttachmentModel {
//#EmbeddedId
//private AttachmentId attachmentId;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="notice_attachment_id")
private long attachmentId;
#Column(name="notice_id")
private long noticeId;
#Column(name="attachment")
private String attachmentUrl;
#JsonIgnore
#ManyToOne(cascade = {CascadeType.PERSIST , CascadeType.MERGE,
CascadeType.DETACH , CascadeType.REFRESH},optional = false)
#JoinColumn(name="notice_id", insertable=false, updatable=false)
#MapsId("notice_id")
NoticesModel notice;
public void addNotice(NoticesModel notice) {
this.notice = notice;
}
public AttachmentModel() {
}
}
Notices class:
#Entity
#Table(name = "notices")
#Getter #Setter
public class NoticesModel {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "notice_id" ,updatable = false, nullable = false,insertable = true)
private long noticeID;
#OneToMany(fetch = FetchType.EAGER, cascade = { CascadeType.ALL } , mappedBy = "notice")
//#mappedBy(name = "notice_id")
private List<AttachmentModel> attachments;
}
Code to parse JSON and saving it
public HashMap<String,Object> saveNotices(#RequestBody List<NoticesModel> tmpNotices)
{
List<NoticesModel> notices = tmpNotices;
for (NoticesModel notice : notices) {
List<AttachmentModel> attachments = notice.getAttachments();
for (AttachmentModel attachment : attachments) {
attachment.addNotice(notice);
System.out.println(attachment.getAttachmentUrl());
}
for (AttachmentModel attachment : attachments) {
//attachment.addNotice(notice);
System.out.println(attachment.getNotice().getContent());
System.out.println(attachment.getNotice().getNoticeID());
}
}
int result = noticesServices.saveNotice(notices);
HashMap<String,Object> res = new HashMap<>();
res.put("message",result);
return res;
}
This is my JSON I am sending
[
{
"attachments": [
{
"attachmentUrl": "/abc/bcd"
}
],
"content": "string",
}
]
For this case I am trying to save save my notice and attachment.
in this particular case notice_id is getting created while saving to database.
so while trying to save attachement table it is trying to save with notice_id as 0.
so I am getting the exception.
could not execute statement; SQL [n/a]; constraint [attachments_notices_fk]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
How I can be able to solve this issue?
Is this possible to get the notice_id before saving to DB so that I can get notice_id so that I can set it in attachment so that it will not be saved with 0?
What am I doing wrong(Any alternative approach I can take) in this case(I am pretty new to JPA and springboot)?
I think you just should not need to use any notice_id. Remove notice_id and relevant things from your AttachmentModel and usenotice for mapping (NOTE: there will still be column notice_id in db after removal), so:
#ManyToOne
private NoticesModel notice;
and change also the mapping in the NoticesModel to refer to the correct field:
// ALL is just a suggestion
#OneToMany(mappedBy = "noticesModel", cascade = CascadeType.ALL)
private List<AttachmentModel> attachementModels;
Then your for loop might look like:
for (NoticesModel notice : notices) {
for (AttachmentModel am : notice.getAttachments()) {
am.setNotice(notice);
}
noticesServices.save(notice);
}
You could also add something like this in your NoticesModel to handle setting the reference always before persisting:
#PrePersist
private void prePersist() {
for (AttachmentModel am : attachments) {
am.setNotice(this);
}
}
Let's say we have the following three domain model entities: Company, Departament, and Employee.
#Getter #Setter #NoArgsConstrutor
public class Employee {
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "department_id", nullable = false, insertable = false, updatable = false)
private Department department;
#JoinColumn(name = "department_id", nullable = false)
private int department_id;
}
#Getter #Setter #NoArgsConstrutor
public class Department {
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "company_id", nullable = false, insertable = false, updatable = false)
private Company company;
#JoinColumn(name = "company_id", nullable = false)
private int company_id;
#OneToMany(mappedBy = "department")
private List<Employee> employees;
}
#Getter #Setter #NoArgsConstrutor
private class Company {
private String name;
#OneToMany(mappedBy = "company")
private List<Department> departments;
}
For each entity, we have Repositories which extend JpaRepository, Services, and Controllers. In each Service we #Autowire the respective Repository, and in each entity Controller we call methods from the entity Service.
My issue is the following: I cannot save an entire Company, because the Departments require a Company ID, and Employees a Deparment ID. So, firstly, in my CompanyService I save and then clear the departments list, do a saveAndFlush which assigns an ID to my company. I assign the received ID to every company_id in each entity of the previously saved departments list, then attach the list back to the company and do another saveAndFlush, and I do this one more time for the employee list.
#RestController
public class CompanyController {
#Autowire
private CompanyService companyService;
#PostMapping("/companies")
public Company createCompany(#RequestBody Company newCompany) {
return companyService.createCompany(newCompany);
}
}
#Service
public class CompanyService {
#Autowire
private CompanyRepository companyRepository;
public Company createCompany(Company company) {
List<Department> departments = new ArrayList<>(company.getDepartments());
company.getDepartments().clear();
companyRepository.saveAndFlush(company);
int company_id = company.getId();
departments.forEach (department ->
department.setCompany_id(company_id);
);
//here I save a copy of the previously saved departments, because I still need the employees
company.getDepartments().addAll(departments.stream().map(department -> department.clone(department)).collect(Collectors.toList()));
company.getDepartments().forEach(department -> department.getEmployees().clear());
companyRepository.saveAndFlush(company);
//here I assign each employee it's corresponding department ID
for (int i = 0; i < company.getDepartments().size(); i++) {
Department departmentInSavedCompany = company.getDepartments().get(i);
Department departmentWhichStillHasEmployees = departments.get(i);
departmentWhichStillHasEmployees.setId(departmentInSavedCompany.getId());
departmentWhichStillHasEmployees.getEmployees().forEach(employee -> employee.setDepartment_id(departmentInSavedCompany.getId()));
}
company.getDepartments.clear();
company.getDepartments.addAll(departments);
return companyRepository.saveAndFlush(company);
}
}
#Repository
public interface CompanyRepository extends JpaRepository<Company, Integer> {
}
I currenty do not like this implementation neither do I find it good. Which is the correct approach for this situation?
When working with JPA, do not work with IDs, work with object references.
In your case, this means removing the id attributes that duplicate the references.
In order to obtain the proper entities for IDs use JpaRepository.getOne. It will return either the entity if it is already in the 1st level cache or a proxy just wrapping the id, so it won't hit the database.
This allows you to assemble your object graph and persist it in one pass starting with the entity having no references to other entities.
You might also consider configuring cascading, if you consider entities to be part of the same Aggregate, i.e. they should be loaded and persisted together.