Links to Embedded entities in Spring Data Rest - spring

I have the following entities defined in my project:
Country
#Entity
#Data
public class Country {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
Long id;
#Column(nullable = false)
String name;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
List<City> cities = new ArrayList<City>();
}
City
#Entity
#Data
public class City {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
Long id;
#Column(nullable = false)
String name;
#ManyToOne
Country country;
}
Person
#Entity
#Data
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
Long id;
#Column
String name;
#Embedded
Address address = new Address();
}
Address
#Data
public class Address {
#Column
String line;
#ManyToOne
Country country;
#ManyToOne
City city;
}
I have also repositories defined for Person, Country and City.
When I make a GET request to /persons/1 I get the following result:
{
"name":null,
"address":{
"line":"Address1"
},
"_links":{
"self":{
"href":"http://localhost:8080/persons/1"
},
"city":{
"href":"http://localhost:8080/persons/1/city"
},
"country":{
"href":"http://localhost:8080/persons/1/country"
}
}
}
I suspect that since address is an embedded object, the generated links to country and city are wrong. They don't return anything although city and country values are present. What should the correct links be?
Are embedded objects not supported by Spring Data Rest?

Possible solutions:
move associations to the parent entity
promote the embeddable into a separate entity resource
add ResourceProcessor to remove those links
add a custom controller to handle those links
UPDATE: This seems to be already fixed in Spring-DATA-REST v2.1. See DATAREST-262.

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?

Shared Primary Key between two Entities Not Working

I have created two Entities namely Teacher and Detail, the code snippet is shown below
Teacher.java
#Entity
#Table(name = "teacher")
public class Teacher implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private long id;
#Column(name = "name")
private String name;
#Column(name = "age")
private int age;
#OneToOne(mappedBy = "teacher", cascade = CascadeType.ALL)
private Detail detail;
public Teacher() {
}
public Teacher(String name, int age) {
this.name = name;
this.age = age;
}
//getter and setter
}
Detail.java
#Entity
#Table(name = "detail")
public class Detail implements Serializable {
#Id
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "id")
private Teacher teacher;
#Column(name = "subjects")
private String subjects;
public Detail() {
}
public Detail(String subjects) {
this.subjects = subjects;
}
//getter and setter
}
I am trying to achieve one to one mapping with the shared primary key concept
but when i execute the controller, only Teacher table is updating with the value
try {
Teacher teacher=new Teacher("xyz",23);
Detail detail=new Detail("Java,c,c++");
teacher.setDetail(detail);
session.beginTransaction();
session.save(teacher);
session.getTransaction().commit();
model.addAttribute("added", "data inserted");
session.close();
}
After executing only Teacher table is updated with the specified values.Detail table is still showing empty
It does not work exactly like that. You still need the id field in your Detail, so add:
#Id
private long id;
to your Deatail class.
And - as comment suggests - replace the #Id annotation in field Teacher to #MapsId. This way the id of Teacher is mapped to the id of Detail BUT ONLY if you also set the teacher to the detail - you always need to set both sides of relationship - like:
teacher.setDetail(detail);
detail.setTeacher(teacher);

Jpa OneToOne shared primary key half works

I have SpringBoot 2.1.3 and Java 8 application. Building DB with JPA I have 3 table in one to one relationship. Suppose the tables is the follows:
#Entity
#Data //lombok
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
private Address address;
}
And then:
#Entity
#Data
#Table(name = "address")
public class Address {
#Id
#Column(name = "id")
private Long id;
#OneToOne
#MapsId
private User user;
}
That's works.. and it is the best way to do (this exactly example is taken from documentation).
If I start the application the DB is created and if I tried to add entities all works well. The model created follows:
Now I want to add a Country object to my address Entities (for example) and I modified the Entities as follows:
#Entity
#Data
#Table(name = "address")
public class Address {
#Id
#Column(name = "id")
private Long id;
#OneToOne
#MapsId
private User user;
#OneToOne
#MapsId
private Country country;
}
And Country Entities:
#Entity
#Data
#Table(name = "country")
public class Country {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
#OneToOne(mappedBy = "country", cascade = CascadeType.ALL)
private Address address;
}
The application still starts, the DB is created and the model follows:
But if I try to save a User as follows:
User user = new User();
Address address = new Address();
Country country = new Country();
user.setAddress(address);
address.setUser(user);
address.setCountry(country);
country.setAddress(address);
userRepository.save(user);
I obtain the error:
java.sql.SQLException: Field 'country_id' doesn't have a default value
Anyway I solve the issue removing #MapsId and added #JoinColumn but I would like to understand what's wrong.
P.S.: I'm using MySQL 5.7 with InnoDB dialect (setting on application.properties)
Thanks all
It works only with one #MapsId annotation. Using two is causing that country id is not inserted:
insert into Country (id) values (?)
insert into Users (id) values (?)
insert into Address (user_id) values (?)

Spring Data Rest - PUT is not working for associated reference types?

I have the following domain class implemented for a Spring Data Rest project.
#Entity
#Data
public class Address {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private long addressID;
private String houseName;
private String apartmentNumber;
#ManyToOne
private City city;
#ManyToOne
private Country country;
}
Now I am creating an Address resource by sending a POST with following JSON.
{
"houseName":"Some House",
"apartmentNumber":"13 B",
"city": "http://localhost:8080/city/1"
"country":"http://localhost:8080/countries/1"
}
When I send a PUT request to the endpoint http://localhost:8080/addresses/1 with the following JSON, the values for houseName is updated. However the city remains unchanged even though I am sending a different URI for the city.
{
"houseName":"Another House",
"apartmentNumber":"13 B",
"city": "http://localhost:8080/city/2"
"country":"http://localhost:8080/countries/1"
}
If I send a PATCH instead of PUT the city value is also updated. So how do I fix this?
UPDATE 1
Country class
#Data
#Entity
public class Country {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long countryID;
private String countryName;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "country", orphanRemoval = true)
private List<City> cities;
}
City class
#Data
#Entity
public class City {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private long cityID;
private String cityName;
#ManyToOne
#JoinColumn(name = "country_id")
private Country country;
}
I had the same problem and manage to find some information on it.
It is a change in version 2.5.7 of Spring Data Rest and is "by purpose".
The answer of Oliver Drotbohm is:
I looked into this and I'd argue you're expecting things to work in a
way they don't work. PUT requests don't consider associations to
linkable resources, i.e. related resources that are pointed to by
links. The reason for that is two-fold:
If we consider URIs for association fields in the payload to update those associations, the question comes up about what's supposed to
happen if no URI is specified. With the current behavior, linked
associations are simply not a part of the payload as they only reside
in the _links block. We have two options in this scenario: wiping the
associations that are not handed, which breaks the "PUT what you GET"
approach. Only wiping the ones that are supplied using null would sort
of blur the "you PUT the entire state of the resource".
For all the reasons mentioned in 1. there are dedicated assoctiation resources exposed that can be manipulated directly.
So it looks like that if you want to change both state of the resource
plus associations at the same time, I guess exposing a dedicated
resource to do that is the way to go.
Full answer you can find on Jira Spring site: Unable to update associated resource using PUT request on the item resource
(the question I wrote on stack overflow is here: Spring Data Rest - PUT on repository silently fails on child references)
If you're using Hibernate as your JPA provider, then you must let know how the entities are mapped in both the sides and indicate the how it is mapped in the child entity which will take care how the relationships are managed during a transaction.
EDITED and UPDATED:
// City Class
#Entity
public class City {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "city_id")
private Long cityID;
#Column(name = "city_name")
private String cityName;
#ManyToOne
private Country country;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "city", orphanRemoval = true)
private List<Address> addresses;
}
// Country Class
#Entity
public class Country {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "country_id")
private Long countryID;
#Column(name = "country_name")
private String countryName;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "country", orphanRemoval = true)
private List<City> cities = new ArrayList<>();;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "country", orphanRemoval = true)
private List<Address> addresses;
}
USE PATCH: If you're updating part of the resource, subset of the resource and relationships
USE PUT: If you're replacing the resource with an entirely new representation

Need help understanding foreign keys

I have a message class with a foreign key that should be the id of the author who wrote the message. As I understand it, the foreign key in the message class should look like the class below.
Setting the foreign key as the author object as below, seems absurd because instead of a short and human readable id like "5", I get a very long string in the database that isn't human readable.
I'm missing something, right?
message class:
public class Message {
...
private Author author; // this is the foreign key
...
#ManyToOne
#JoinColumn(name = "USERNAME")
public User getAuthor() {
return author;
}
public void setAuthor(Author author) {
this.author = author;
}
...
Creating the message object to be saved:
Author author = ...
message.setAuthor(author);
Assuming you are simply looking for a Many-to-One unidirectional relationship
#Entity
public class Message {
...
#ManyToOne
#JoinColumn(name="USERNAME")
private Author author;
#Entity
public class Author {
#Id
#GeneratedValue
#Column(name="USERNAME")
private Long USERNAME;
You do not post the annotations from the Author class. And it is highly probable that You are missing annotations on the Author class site. Nevertheless look below:
#Entity
#Table(name = "bill")
public class BillModel {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = "bill_id")
private Integer billId;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "fk_shop_id")
private Shop shop;
// getters and setters
}
and class Shop
#Entity
#Table(name = "shop")
public class Shop {
#Id
#GeneratedValue
#Column(name = "shop_id")
private Integer shopId;
#Column(name = "shop_name")
private String shopName;
#OneToMany(mappedBy = "shop", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private Set<BillModel> billModels = new HashSet<BillModel>();
// getters and setters
}

Resources