Many to Many relation with embedded key is inserting null in primary key - spring-boot

Facing problem inserting records in intermediate table.
Post and Tag tables are sharing many to many relationship using intermediate table post_tag;
Post table: id (pk), post, created_by, created_on, modified_by, modified_on
Tag table: id (pk), tag, created_by, created_on, modified_by, modified_on
Post_Tag table: post_id, tag_id (pk post_id and tag_id), created_by, created_on, modified_by, modified_on
Entities:
// ID class
#Embeddable #Data #NoArgsConstructor #AllArgsConstructor
public class PostTagId
implements Serializable {
#Column(name = "post_id")
private Long postId;
#Column(name = "tag_id")
private Long tagId;
}
#Entity
#Table(name = "post_tag")
#EntityListeners(AuditingEntityListener.class)
#Data
#EqualsAndHashCode(callSuper = false)
public class PostTag extends Auditable<String> {
#EmbeddedId
private PostTagId id;
#ManyToOne
#MapsId("postId")
#JoinColumn(name="post_id")
private Post post;
#ManyToOne
#MapsId("tagId")
#JoinColumn(name="tag_id")
private Tag tag;
public PostTag(Post post, Tag tag) {
this.post = post;
this.tag = tag;
this.id = new PostTagId(post.getId(), tag.getId());
}
}
Using springboot(AuditingEntityListener) to add audit columns. Post and Tag entities are somewhat like -
///////////Tag
#Data
#EqualsAndHashCode(callSuper = false, exclude = {"postTags"})
#ToString( exclude = {"postTags"})
#Entity
#Table(name = "tag")
#EntityListeners(AuditingEntityListener.class)
public class Tag extends Auditable<String> {
#Id
private Long id;
#Column(name = "tag")
private String tag;
#OneToMany(mappedBy = "tag", cascade={CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
private Set<PostTag> postTags;
}
//////////////POST
#Data
#EqualsAndHashCode(callSuper = false, exclude = {"postTags"})
#ToString( exclude = {"postTags"})
#Entity
#Table(name = "post")
#EntityListeners(AuditingEntityListener.class)
public class Post extends Auditable<String> {
#Id
private Long id;
#Column(name = "post")
private String post;
#OneToMany(mappedBy = "post", cascade={CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
private Set<PostTag> postTags;
public void addTag(Tag tag) {
PostTag postTag = new PostTag(this, tag);
if (postTags == null) {
postTags = new HashSet<>();
}
postTags.add(postTag);
tag.getPostTags().add(postTag);
}
}
Code snippet for insertion of tags using addTag() method -
public Post create(Post post) {
// DefaultTag is some default tag getting from repository
post.addTag(DefaultTag)
return postRepo.save(post);
}
Insert in post_tag is trying to insert null post_id.
Hibernate:
insert
into
post_tag
(created_by, created_on, modified_by, modified_on, post_id, tag_id)
values
(?, ?, ?, ?, ?, ?) [0;39m [36mo.h.engine.jdbc.spi.SqlExceptionHelper SQL Error: 0, SQLState:
23502 [0;39m [36mo.h.engine.jdbc.spi.SqlExceptionHelper ERROR: null
value in column "post_id" violates not-null constraint Detail:
Failing row contains (null, 100, anonymousUser, anonymousUser,
2020-07-06 15:22:19.227-06, 2020-07-06 15:22:19.227-06).

Removed #EmbededId and used #Id in PostTag class. This way do not need to create embededId in constructor, and hibernate handles id creation. Changes look like this -
#Entity
#Table(name = "post_tag")
#EntityListeners(AuditingEntityListener.class)
#Data
#EqualsAndHashCode(callSuper = false)
public class PostTag extends Auditable<String> implements Serializable {
#ManyToOne
#Id
#JoinColumn(name="post_id")
private Post post;
#ManyToOne
#Id
#JoinColumn(name="tag_id")
private Tag tag;
}
Since PostTag(Post, Tag) constructor is removed, addTag(Tag tag) method needs to be refactored in Post entity.
public void addTag(Tag tag) {
PostTag postTag = new PostTag();
postTag.setPost(this);
postTag.setTag(tag);
if (postTags == null) {
postTags = new HashSet<>();
}
postTags.add(postTag);
tag.getPostTags().add(postTag);
}

Related

JPA JoinTable with additional columns

Spring Boot
Spring Data
JPA Hibernate
Came across a requirement where JPA ManyToMany relationship table with an extra column. Have looked at StackOverflow and found several questions related to same requirement. Most of the answers on the forums ask for EmbeddedId field with a composite primary key with two columns. I tried the solutions from the forums, here is the code snippet.
#Data
#Entity
#Table (name = "TABLE_A")
public class TableA {
#Id
#Column (name = "table_a_id")
private Integer id;
...
#OneToMany (mappedBy = "pk.tableA")
private List<TableABMapping> mappingTable;
}
#Data
#Entity
#Table (name = "TABLE_B")
public class TableB {
#Id
#Column (name = "table_b_id")
private Integer id;
...
#OneToMany (mappedBy = "pk.tableB")
private List<TableABMapping> mappingTable;
}
#Data
#Entity
#Table (name = "TABLE_A_TABLE_B_MAPPING")
public class TableABMapping implements Serializable {
#EmbeddedId
private MappingKey pk = new MappingKey();
#Column(name = "addon_field")
private Double additionalField;
#Transient
public TableA getTableA() {
return getPk().getTableA();
}
public void setTableA(TableA tableA) {
getPk().setTableA(tableA);
}
#Transient
public TableB getTableB() {
return getPk().getTableB();
}
public void setTableB(TableB tableB) {
getPk().setTableB(tableB);
}
// equals() & hashCode() method override
}
#Data
#Embeddable
public class MappingKey implements Serializable {
#ManyToOne
#JoinColumn(name = "table_a_id", referencedColumnName = "table_a_id")
private TableA tableA;
#ManyToOne
#JoinColumn(name = "table_b_id", referencedColumnName = "table_b_id")
private TableB tableB;
// No argument constructor, two arguments constructor.
// equals() & hashCode() method override
}
Trying save operation from service class like this:
for (TableB tabB : tableA.getTableB()) {
TableABMapping abMapping = new TableABMapping();
abMapping.setTableA(tableA);
abMapping.setProduct(tabB);
abMapping.setAdditionalField(tabB.getAddonField());
if (tableA.getMappingTable() == null) {
tableA.setMappingTable(new ArrayList<TableABMapping>());
}
tableA.getMappingTable().add(abMapping);
}
TableA ta = tableARepository.save(tableA);
System.out.println("TableA.save(): " + ta);
Getting this error on save operation.
Unable to find TableABMapping with id MappingKey(tableA = TableA( ... ), tableB = TableB ( ... ))
Both the entities have proper ids at the time of saving the entity. But still it throws this error. Where I am making mistake?

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?

Spring Data JPA ManyToOne query null

I've started to learn Spring Boot, I've created two entities Invoice and YearDate
When I try to search by year(id), in my log query I got null (see this part of log query)
http://localhost:8080/appapi/invoices/search/findByYearId?year=1
from invoice invoice0_ left outer join year_date yeardate1_ on invoice0_.year_id=yeardate1_.id where yeardate1_.id is null limit?
I'm using Lombok also for getters and setters
Here are all my class entities, SQL tables, and JpaRepository interface :
SQL foreign key :
KEY `fk_year` (`year_id`),
CONSTRAINT `fk_year` FOREIGN KEY (`year_id`) REFERENCES `YearDate` (`id`)
YearDate class :
#Entity
#Table(name="YearDate")
#Data
public class YearDate {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "year_value")
private String yearValue;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "year")
private Set<Invoice> invoices;
}
Invoice class:
#Entity
#Table(name="invoice")
#Data
public class Invoice {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "name")
private String name;
#ManyToOne
#JoinColumn(name = "year_id", nullable = false)
private YearDate year;
#Column(name = "description")
private String description;
}
**And The Invoice Interface:**
#CrossOrigin("http://localhost:4200")
public interface InvoiceRepository extends JpaRepository<Invoice, Long> {
Page<Invoice> findByYearId(#RequestParam("year") Long id, Pageable page);
}
Work for me.
try to extract call in rest controller and log params
#GetMapping(value = "/invoices")
public Page<Invoice> getInvoices(#RequestParam("yearId") Long yearId) {
Page<Invoice> byYearId = invoiceRepository.findByYearId(yearId, PageRequest.of(0, 10));
return byYearId;
}
public interface InvoiceRepository extends JpaRepository<Invoice, Long> {
Page<Invoice> findByYearId(Long id, Pageable page);
}
#ManyToOne
#JoinColumn(name = "year_id", nullable = false)
#JsonIgnoreProperties("invoices")
private YearDate year;
#OneToMany(mappedBy = "year")
#JsonIgnoreProperties("year")
private Set<Invoice> invoices;
Generated SQL :
Hibernate:
select
invoice0_.id as id1_0_,
invoice0_.description as descript2_0_,
invoice0_.name as name3_0_,
invoice0_.year_id as year_id4_0_
from
invoice invoice0_
left outer join
year_date yeardate1_
on invoice0_.year_id=yeardate1_.id
where
yeardate1_.id=? limit ?
Hibernate:
select
yeardate0_.id as id1_5_0_,
yeardate0_.year_value as year_val2_5_0_
from
year_date yeardate0_
where
yeardate0_.id=?
-> ADD #JsonIgnoreProperties("invoices") and #JsonIgnoreProperties("year") to entities to avoid infinite json recusrsion.
I've disabled Lombok and it works
try pass him a Year object not id is better
#CrossOrigin("http://localhost:4200")
public interface InvoiceRepository extends JpaRepository<Invoice, Long> {
Page<Invoice> findByYear(YearDate year, Pageable page);
}
and in your service get this year
for exemple I create a getYears method in service:
#Service
public class YearService{
#Autowired
private YearRepository yearRepository;
#Autowired
private InvoiceRepository invoiceRepository;
getYears(idYear:Long){
YearDate yearParam=yearRepository.findById(id).get();
Page<Invoice> invoices=invoiceRepository.findByYear(yearParam,YourPagination)
}
}

Spring Data JPA OneToOne Mapping returning Null

I have two Entity classes, each for my Table. They both are OneToOne-Mapped. When I read the Data, am always getting the other table's value as null.
These are my SQLs
CREATE TABLE driver_master (
unique_driver_id VARCHAR(60) PRIMARY KEY,
driver_name TEXT,
mobile_number VARCHAR (20),
vehicle_number TEXT,
vehicle_make TEXT,
seating_capacity INTEGER (10),
creation_time DATETIME
)
CREATE TABLE user_master (
user_id MEDIUMINT AUTO_INCREMENT PRIMARY KEY,
user_name TEXT,
password VARCHAR (20),
unique_driver_id VARCHAR(60),
FOREIGN KEY (unique_driver_id) REFERENCES driver_master(unique_driver_id)
)
These are my Entity classes
DriverMaster.java
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
#Entity
#Table(name = "driver_master")
public class DriverMaster {
#Id
#GeneratedValue(generator = "uuid2")
#GenericGenerator(name = "uuid2", strategy = "org.hibernate.id.UUIDGenerator")
#Column(name = "unique_driver_id")
UUID id;
#Column(name = "driver_name")
String driverName;
#Column(name = "mobile_number")
String mobileNumber;
#Column(name = "vehicle_number")
String vehicleNumber;
#Column(name = "vehicle_make")
String vehicleMake;
#Column(name = "seating_capacity")
Integer seatingCapacity;
#Column(name = "creation_time")
OffsetDateTime creationTime;
#OneToOne(cascade = CascadeType.ALL, mappedBy = "driverMaster")
UserMaster userMaster;
}
UserMaster.java
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
#Entity
#Table(name = "user_master")
public class UserMaster {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "user_id")
Long id;
#Column(name = "user_name")
String userName;
#Column(name = "password")
String password;
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "unique_driver_id", nullable = false)
DriverMaster driverMaster;
}
This is my DriverMasterRepository
public interface DriverMasterRepository extends CrudRepository<DriverMaster, Long> {
DriverMaster findById(UUID id);
}
This is my UserMasterRepository
public interface UserMasterRepository extends CrudRepository<UserMaster, Long> {
UserMaster findById(Long id);
}
I am creating DriverMaster and UserMaster at the same time. Code Snippet below
public DriverMaster create() {
UserMaster userMaster = UserMaster.builder()
.userName("xxxxx")
.password("xxxx").build();
DriverMaster driverMaster = DriverMaster.builder().driverName("TestDriver")
.creationTime(ZonedDateTime.now().toOffsetDateTime())
.seatingCapacity(8)
.mobileNumber("xxxxxxx")
.vehicleNumber("xxxx")
.vehicleMake("xxxx")
.userMaster(userMaster)
.build();
return driverUserService.create(driverMaster);
}
When i access each repository and get the data, the data for that particular table is getting populated while the referenced Object is always coming as Null.
After creation of DriverMaster, I couldn't get UserMaster within DriverMaster, it is always coming as null .
The REST Response below shows that UserMaster within DriverMaster is coming as Null
{
"id": "0d97073b-6ae2-47a9-b751-0313fd9e8ba2",
"driverName": "TestDriver",
"mobileNumber": "11111",
"vehicleNumber": "111",
"vehicleMake": "figo",
"seatingCapacity": 8,
"creationTime": "2018-02-16T15:56:50.331Z",
"userMaster": null
}
it's not a 100% problem's reason, but I believe you have to add an implementation of Serializableinterface into your entity classes.

Creating One to Many relation in JPA Spring

I would like to link a comment table and a film table with a user table. I wish to allow a user to have many comments, and a film have many comments. I then want to display a list of comments in a details page for each film, giving the option for the user who created the comment to delete or update it.
I altered my code in an attempt to create a one to many relation between comment and film, but I get the error:
Caused by: org.h2.jdbc.JdbcSQLException: NULL not allowed for column
"FILM_ID"; SQL statement: alter table film add column film_id bigint
not null [23502-196]
It makes me think two things:
1) Set to allow null or figure out why there is a null field. I attempted allow null by adding #Column(name = "film_id", nullable = true) but it said parameter is redundant.
2) Film table has auto incrementing ID already, so by adding #Column(name = "film_id") am I duplicating an ID? As with the error message saying "add column" it made me think so?
My attempt currently stands at:
Film.java
package com.demo.spring.domain;
import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.*;
import java.util.Date;
import java.util.List;
#Entity
public class Film {
#Id #GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "film_id", nullable = true)
Long id;
String title;
String director;
String description;
#DateTimeFormat(pattern="yyyy-MM-dd")
Date date;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "film_id", referencedColumnName = "film_id")
List<Comment> comments;
public List<Comment> getComments() {
return comments;
}
public void setComments(List<Comment> comments) {
this.comments = comments;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
//rest of getter and setters below//
Comment.java
package com.demo.spring.domain;
import javax.persistence.*;
#Entity
public class Comment {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "comment_id")
Long id;
String body;
#Column(name = "film_id")
Long filmId;
public Long getFilmId() {
return filmId;
}
public void setFilmId(Long filmId) {
this.filmId = filmId;
}
public Comment(){
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
}
UPDATE
I have changed Film.java..
From:
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "film_id", referencedColumnName = "film_id")
List<Comment> comments;
To:
#OneToMany(cascade = CascadeType.ALL)
List<Comment> comments;
And if I add in Comment.java:
#OneToMany(cascade=CascadeType.ALL)
#JoinTable(name="film", joinColumns=#JoinColumn(name = "film_id_fk", referencedColumnName = "film_id"))
private Set<Comment> comment = new HashSet<Comment>();
Film film;
I get:
MappingException: Foreign key
(FK5vk85sy54a8be115ye9ra1lyu:film_comments [film_film_id])) must have
same number of columns as the referenced primary key (film
[film_id_fk,comment_comment_id])
If I change private Set<Comment> comment = new HashSet<Comment>(); to List<Comment> comments = new ArrayList<Comment>(); I get:
NULL not allowed for column "FILM_ID"; SQL statement: alter table film
add column film_id bigint not null
And if instead I add:
#OneToMany(cascade=CascadeType.ALL)
#JoinColumn(name = "film_id_fk", referencedColumnName = "film_id")
private Set<Comment> comment = new HashSet<Comment>();
Film film;
I get:
MappingException: Could not determine type for:
com.demo.spring.domain.Film, at table: comment, for columns:
[org.hibernate.mapping.Column(film)]
If I change private Set<Comment> comment = new HashSet<Comment>(); to List<Comment> comments = new ArrayList<Comment>(); I get:
NULL not allowed for column "FILM_ID"; SQL statement: alter table film
add column film_id bigint not null
A primary key can't be null, so you can't make "film_id" nullable. And your #JoinColumn annotation is wrong, that goes on the #ManyToOne side. The name parameter should be the name of the foreign key column in the Comments table (so it can't be the same name as the primary key) and referencedColumnName should be the name of the column that you're referencing in the other table
#Entity
public class Film {
#Id #GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "film_id")
Long id;
String title;
String director;
String description;
#DateTimeFormat(pattern="yyyy-MM-dd")
Date date;
#OneToMany(cascade = CascadeType.ALL)
List<Comment> comments;
//...
}
#Entity
public class Comment {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "comment_id")
Long id;
String body;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "film_id_fk", referencedColumnName = "film_id")
Film film;
//...
}

Resources