How to correctly describe entities with many-to-many relationship, Spring Boot JPA - spring

I have those entities:
#Entity
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
public class Tender {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(nullable = false, updatable = false)
private Long id;
private String source;
private String sourceRefNumber;
private String link;
private String title;
#Column(columnDefinition="TEXT")
private String description;
private String field;
private String client;
private LocalDate date;
private LocalDate deadline;
#ManyToMany
private List<Cpv> cpv;
}
And CPV:
#Entity
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
public class Cpv {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String code;
private String description;
}
Each Tender can have list of Cpv-s.
In my DB I have already list of all CPV codes with description, so when I add new Tender to DB, it should add record to tender_cpv table with tender_id and cpv_id.
But when I'm using this method in my TenderServiceImpl to set Cpv id-s from DB I got error after that when try to save Tender:
#Override
public Tender addNewTender(Tender tender) {
if(tender.getCpv() != null) {
for(Cpv cpv : tender.getCpv()) {
cpv = cpvRepository.findCpvByCode(cpv.getCode());
}
}
tenderRepository.save(tender);
return tender;
}
org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.supportportal.domain.Cpv;
I understand that somewhere in the description of the entities a mistake was made, because earlier I did not have a database with all the CPV codes and before saving the tender I saved all the CPVs, but now I need to redo the logic to use the existing CPV database.
Please advise how can I change the entity description.

addNewTender method changes solved my problem:
#Override
public Tender addNewTender(Tender tender) {
if(tender.getCpv() != null) {
List<Cpv> dbCpvs = new ArrayList<>();
for(Cpv cpv : tender.getCpv()) {
dbCpvs.add(cpvRepository.findCpvByCode(cpv.getCode()));
}
tender.setCpv(dbCpvs);
}
tenderRepository.save(tender);
return tender;
}
In order for the existing entities from the database to bind to the new object, we had to first get each of them from the database and bind to the new entity.

Related

Spring Boot JPA Using Many-to-Many relationship with additional attributes in the join table

I have two simple classes Student and Course. I am trying to set up many to many relationship between these classes. I want to use additional table whose PRIMARY KEY is the combination of the primary keys of student and course tables (student_id and course_id).
The student class:
#Entity
#Table(name = "student")
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
public class Student {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(name = "name")
private String name;
#Column(name = "surname")
private String surname;
#OneToMany(mappedBy = "student")
private Set<CourseStudent> courses;
}
The course class:
#Entity
#Table(name = "course")
public class Course {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String courseName;
#OneToMany(mappedBy = "course")
Set<CourseStudent> students;
}
The entity that stores the relationship between course and the student:
#Entity
#NoArgsConstructor
#Data
public class CourseStudent {
#EmbeddedId
CourseStudentKey id;
#ManyToOne
#MapsId("studentId")
#JoinColumn(name = "student_id")
Student student;
#ManyToOne
#MapsId("courseId")
#JoinColumn(name = "course_id")
Course course;
public CourseStudent(Student student, Course course) {
this.student = student;
this.course = course;
this.rating = 0;
}
int rating;
}
Attention: Since I want to have additional features in this entity (for example, storing the rating of the students for courses), I don't want to use #JoinTable idea that we implement in the Student class.
Since I have multiple attributes in the primary key of CourseStudent entity, I used the following class
#Embeddable
#Data
public class CourseStudentKey implements Serializable {
#Column(name = "student_id")
Long studentId;
#Column(name = "course_id")
Long courseId;
}
I have the following POST request to insert the student into a course:
#PostMapping("/insert/students/{studentId}/courses/{courseId}")
public CourseStudent insertStudentIntoCourse(#PathVariable(value = "studentId") Long studentId,
#PathVariable(value = "courseId") Long courseId) {
if (!studentRepository.existsById(studentId)) {
throw new ResourceNotFoundException("Student id " + studentId + " not found");
}
if (!courseRepository.existsById(courseId)) {
throw new ResourceNotFoundException("Course id " + courseId + " not found");
}
CourseStudent courseStudent = new CourseStudent(
studentRepository.findById(studentId).get(),
courseRepository.findById(courseId).get()
);
return courseStudentRepository.save(courseStudent);
}
I have manually added Student and the Course into my local database and send this request by using Postman.
http://localhost:8080/insert/students/1/courses/1
However, I get the following error:
{
"timestamp": "2022-08-04T12:33:18.547+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/insert/students/1/courses/1"
}
In the console, I get NullPointerException. What is the thing I am doing wrong here?

Mapping DTO to existing entity , it is creating new entity

I have Activity class and ActivityDTO as you see below. While saving new entity there is no problem, but if I want to update existing entity, then it is creating new entity although my entity id is from my database entity.
#Getter
#Setter
#Entity
public class Activity implements Serializable {
#Id
#Column(name = "ID")
#GeneratedValue
#SequenceGenerator
private Long id;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval=true)
#JoinTable(name = "ACTIVITY_ATTACHMENT", joinColumns = {
#JoinColumn(name = "ACTIVITY_ID") }, inverseJoinColumns = { #JoinColumn(name = "ATTACHMENT_ID") })
private Set<Attachment> attachments = new HashSet<>();
#Temporal(TemporalType.DATE)
#JsonSerialize(using = CustomJsonDateSerializer.class)
#JsonDeserialize(using = CustomJsonDateDeserializer.class)
#Column(name = "BEGIN_DATE")
private Date beginDate;
#Temporal(TemporalType.DATE)
#JsonSerialize(using = CustomJsonDateSerializer.class)
#JsonDeserialize(using = CustomJsonDateDeserializer.class)
#Column(name = "END_DATE")
private Date endDate;}
And my ActivityDTO
#Getter
#Setter
public class ActivityDTO {
private Long id;
private Set<Attachment> attachments = new HashSet<>();
#Temporal(TemporalType.DATE)
#JsonSerialize(using = CustomJsonDateSerializer.class)
#JsonDeserialize(using = CustomJsonDateDeserializer.class)
private Date beginDate;
#Temporal(TemporalType.DATE)
#JsonSerialize(using = CustomJsonDateSerializer.class)
#JsonDeserialize(using = CustomJsonDateDeserializer.class)
private Date endDate;
And here is my ActivityController class;
public Activity save(ActivityDTO activityDTO, List<MultipartFile> fileList) throws Exception {
Activity activity = convertActivityDTOtoEntity(activityDTO);
activity.getAttachments().addAll(ObjectFactory.createAttachment(fileList, Activity.class));
return activityRepository.save(activity);
}
public Activity update(ActivityDTO activityDTO, List<MultipartFile> fileList) throws Exception {
Activity activity = convertActivityDTOtoEntity(activityDTO);
activity.getAttachments().addAll(ObjectFactory.createAttachment(fileList, Activity.class));
return activityRepository.save(activity);
}
private Activity convertActivityDTOtoEntity(ActivityDTO activityDTO) {
return modelMapper.map(activityDTO, Activity.class);
}
Also I have one more problem, I have just transformed my entity usage to DTO objects, until now service was reaching entity directly and while updating if I delete any attachment or add, there was no problem. After I transformed to DTO objects and used like above, there is a problem while updating;
detached entity passed to persist: com.thy.agencycrm.entity.Attachment
And here is my Attachment entity if you would like to see;
#Getter
#Setter
#Entity
public class Attachment implements Serializable {
#Id
#Column(name = "ID")
#GeneratedValue
#SequenceGenerator
private Long id;
#Column(name = "MIME_TYPE")
private String mimeType;
Please help me about this problem, I am searhing and trying to solve it for long times.
Thanks you in advance.
I think you just copy the fields into a new object in your converter right?
Default JPA only update the entity if it is in the persistance context and the two object are identical. If you have a detached object, create a new one with in the converter, it will be saved as new record. It does not matter if you set the ID, because the id is generated by the sequence, as you annotated on the entity class.
You can resolve this many ways. The easiest is to load the entity by id, and set the fields from the another object into this managed object.
Updated your Class ActivityController
public Activity save(ActivityDTO activityDTO, List<MultipartFile> fileList) throws Exception {
Activity activity = convertActivityDTOtoEntity(activityDTO);
activity.getAttachments().addAll(ObjectFactory.createAttachment(fileList, Activity.class));
return activityRepository.save(activity);
}
public Activity update(ActivityDTO activityDTO, List<MultipartFile> fileList) throws Exception {
Activity activity = activitiyRepository.findOne(activityDTO.getID());
// This will update the existing activity with activityDTO
modelMapper.map(activityDTO, activity);
activity.getAttachments().addAll(ObjectFactory.createAttachment(fileList, Activity.class));
return activityRepository.save(activity);
}
private Activity convertActivityDTOtoEntity(ActivityDTO activityDTO) {
return modelMapper.map(activityDTO, Activity.class);
}

SpringBoot CascadeType ALL vs MERGE and detached entities

I have the following entities:
#Entity
#Getter #Setter #NoArgsConstructor #RequiredArgsConstructor
public class Link extends Auditable {
#Id
#GeneratedValue
private Long id;
#NonNull
private String title;
#NonNull
private String url;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "link")
private List<Comment> comments = new ArrayList<>();
#Transient
#Setter(AccessLevel.NONE)
private String userAlias ;
public String getUserAlias() {
if(user == null)
return "";
return user.getAlias();
}
#ManyToOne
private User user;
public Long getUser() {
if(user == null)
return -1L;
return user.getId();
}
public void addComment(Comment c) {
comments.add(c);
c.setLink(this);
}
}
#Entity
#Getter #Setter #NoArgsConstructor #RequiredArgsConstructor
public class Comment extends Auditable{
#Id
#GeneratedValue
private Long id;
#NonNull
private String comment;
#ManyToOne(fetch = FetchType.LAZY)
private Link link;
public Long getLink() {
return link.getId();
}
}
If I create a comment and a link, associate the link to the comment and then save that works.
Eg:
Link link = new Link("Getting started", "url");
Comment c = new Comment("Hello!");
link.addComment(c);
linkRepository.save(link);
However, if I save the comment first:
Link link = new Link("Getting started", "url");
Comment c = new Comment("Hello!");
commentRepository.save(c);
link.addComment(c);
linkRepository.save(link);
I get
Caused by: org.hibernate.AnnotationException: #OneToOne or #ManyToOne on uk.me.dariosdesk.dariodemo.domain.Comment.link references an unknown entity: uk.me.dariosdesk.dariodemo.domain.Link
at org.hibernate.cfg.ToOneFkSecondPass.doSecondPass(ToOneFkSecondPass.java:97) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processEndOfQueue(InFlightMetadataCollectorImpl.java:1815) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processFkSecondPassesInOrder(InFlightMetadataCollectorImpl.java:1759) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processSecondPasses(InFlightMetadataCollectorImpl.java:1646) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:287) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:903) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:934) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
Changing the cascade type from ALL to MERGE seems to fix the problem and accept both implementations. (Ie: Adding a pre-existing comment or creating both and then saving via the link)
1) Why is this?
2) Is there anything I should be aware of in using MERGE rather than ALL?
Repository save method checks if entity exist. For new entity persist is called, for persisted entity merge is called.
#Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
In 2nd use-case Link is new entity, therefore persist() is called. With CascadeType.ALL persist() is cascaded to Comment entity. Comment is already persisted and needs to be merged, persist() fails.
If you use CascadeType.MERGE persist() is not cascaded down to Comment. It does not fail.

EntityManager persist multiple relationship

I have spring boot rest api, I have persisted 1 table successfully, but when I tried to persist object which has 2 another relations and I got error:
o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1364, SQLState:
HY000
o.h.engine.jdbc.spi.SqlExceptionHelper : Field 'id' doesn't have a default value
here is my entity and entity manger persistance:
#Entity
#Table(name="booking")
public class Booking {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name="id")
private int id;
#Column(name="description")
private String description;
#OneToMany(mappedBy="booking",cascade = CascadeType.ALL)
private List<CategoriesBooking> bookingInfos = new ArrayList<>();
#Entity
#Table(name="category_booking")
public class CategoriesBooking {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name="id")
private int id;
#Column(name = "name")
private String name;
#ManyToOne(fetch=FetchType.LAZY, cascade=CascadeType.ALL)
#JoinColumn(name="booking_id")
private Booking booking;
#OneToMany(mappedBy="categoriesBooking",cascade = CascadeType.ALL)
private List<OptionsBooking> options = new ArrayList<>();
#Entity
#Table(name="options_booking")
public class OptionsBooking {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name="id")
private int id;
#Column(name="name")
private String name;
#ManyToOne(fetch=FetchType.LAZY, cascade=CascadeType.ALL)
#JoinColumn(name = "catebooking_id")
private CategoriesBooking categoriesBooking;
#Transactional
#Repository
public class Services{
#PersistenceContext
protected EntityManager entityManager;
public Services() {
}
public boolean add(Booking booking){
try {
entityManager.persist(booking);
return true;
} catch (Exception e) {
entityManager.getTransaction().rollback();
}
return false;
}
}
data:
{description: 'test for persist',
bookingInfos:[{
name:'test1',
options:[{
name: 'test1-test1'
}]
}]
}
I update for use MySQL
GenerationType.AUTO chooses an ID generation strategy suitable for your database. What it actually picks depends on the database you are using. But judging from the error message it assumes the id column to be some kind of IDENTITY field which creates a unique value upon insertion.
And it seems your schema doesn't declare the id column in this way.
The obvious fix is to change that.
Sometimes changes made to the model or to the ORM may not reflect accurately on the database even after an execution of SchemaUpdate.
If the error actually seems to lack a sensible explanation, try recreating the database (or at least creating a new one) and scaffolding it with SchemaExport.

Spring Data Rest #EmbeddedId cannot be constructed from Post Request

I have a JPA entity Person and an entity Team. Both are joined by an entity PersonToTeam. This joining entity holds a many-to-one relation to Person and one to Team. It has a multi-column key consisting of the ids of the Person and the Team, which is represented by an #EmbeddedId. To convert the embedded id back and forth to the request id I have a converter. All this follows the suggestion on Spring Data REST #Idclass not recognized
The code looks like this:
#Entity
public class PersonToTeam {
#EmbeddedId
#Getter
#Setter
private PersonToTeamId id = new PersonToTeamId();
#ManyToOne
#Getter
#Setter
#JoinColumn(name = "person_id", insertable=false, updatable=false)
private Person person;
#ManyToOne
#Getter
#Setter
#JoinColumn(name = "team_id", insertable=false, updatable=false)
private Team team;
#Getter
#Setter
#Enumerated(EnumType.STRING)
private RoleInTeam role;
public enum RoleInTeam {
ADMIN, MEMBER
}
}
#EqualsAndHashCode
#Embeddable
public class PersonToTeamId implements Serializable {
private static final long serialVersionUID = -8450195271351341722L;
#Getter
#Setter
#Column(name = "person_id")
private String personId;
#Getter
#Setter
#Column(name = "team_id")
private String teamId;
}
#Component
public class PersonToTeamIdConverter implements BackendIdConverter {
#Override
public boolean supports(Class<?> delimiter) {
return delimiter.equals(PersonToTeam.class);
}
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
if (id != null) {
PersonToTeamId ptid = new PersonToTeamId();
String[] idParts = id.split("-");
ptid.setPersonId(idParts[0]);
ptid.setTeamId(idParts[1]);
return ptid;
}
return BackendIdConverter.DefaultIdConverter.INSTANCE.fromRequestId(id, entityType);
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
if (id instanceof PersonToTeamId) {
PersonToTeamId ptid = (PersonToTeamId) id;
return String.format("%s-%s", ptid.getPersonId(), ptid.getTeamId());
}
return BackendIdConverter.DefaultIdConverter.INSTANCE.toRequestId(id, entityType);
}
}
The problem with this converter is, that the fromRequestId method gets a null as id parameter, when a post request tries to create a new personToTeam association. But there is no other information about the payload of the post. So how should an id with foreign keys to the person and the team be created then? And as a more general question: What is the right approach for dealing many-to-many associations in spring data rest?
After running into the same issue I found a solution. Your code should be fine, except I return new PersonToTeamId() instead of the DefaultIdConverter if id is null in fromRequestId().
Assuming you are using JSON in your post request you have to wrap personId and teamId in an id object:
{
"id": {
"personId": "foo",
"teamId": "bar"
},
...
}
And in cases where a part of the #EmbeddedId is not a simple data type but a foreign key:
{
"id": {
"stringId": "foo",
"foreignKeyId": "http://localhost:8080/path/to/other/resource/1"
},
...
}

Resources