I'm observing some weird jackson behavior which I need help with. I have the following two one-to-many relationships in my simple library application:
Book contains a list of BookCopy
Library contains a list of BookCopy
The idea is that a single book may have many copies, and each library owns copies of books (and not books directly). That said, here's the relevant snippet of my code:
Book class:
#Entity
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Book {
//.. rest omitted
#OneToMany(mappedBy = "book")
private List<BookCopy> bookCopies;
}
Library class:
#Entity
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Library {
#OneToMany(mappedBy = "library")
private List<BookCopy> bookCopies;
}
BookCopy class:
#Entity
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class BookCopy {
#ManyToOne
#JoinColumn(name = "book_id")
private Book book;
#ManyToOne
#JoinColumn(name = "library_id")
private Library library;
}
Now, when I add a book and a library, without the copies, things work fine. When I add a book-copy object though, I get some seriously strange output from the serialized version of bookRepository.findAll().
This is a sample json I'm POST-ing to add a book copy:
{
"book": {
"id": 1
},
"library": {
"id": 4
}
}
.. and things are persisted perfectly into the DB (postgres). However, when I retrieve all books, here's a sample weird JSON I get (snipped for brevity):
[
{
"id": 1,
"title": "Effective Java",
"bookCopies": [
{
"id": 5,
"book": 1,
"library": {
"id": 3 // ...
}
}
},
2
]
Note the last part - the weird 2 sticks out of nowhere - in a list of book objects. Am I doing something seriously wrong - or is this some jackson bug I'm hitting?
Related
I am building a Single-Page-Webapp for testing-scenario and i am using spring-jpa. I want to use this JSON data-model for my post-request:
{
"id": 1,
"title": "test-title",
"releaseDate": "2021/12/15",
"rating" : {
"stars" : 5,
"comment" : "very exciting"
}
}
If i start my application, i get the following error:
Caused by: org.hibernate.MappingException: Could not determine type for: de.demo.dto.Rating, at table: books, for columns: [org.hibernate.mapping.Column(rating)]
If i am declaring the class "rating" with #Entity and add the field "id", the application is starting without errors (if i am using an #OneToOne annotation). But for the class "Rating" i do not want to use an own data table with an "id". Can everyone help me with my issues? How do i fix this problem?
Class books:
#Getter
#Setter
#Entity
public class Books {
#Id
#GeneratedValue(strategy= GenerationType.AUTO)
#Column(unique = true, nullable = false)
private int id;
private String title;
private String releaseDate;
private Rating rating;
}
class Rating
#Getter
#Setter
public class Rating {
int stars;
String comment;
}
Thanks!
So seems you want one to one relationship but dont want it to be in another table, best thing comes to my mind is saving that rating as a json object String. So you might need to do cruds with some third party library like GSON:
Gson gson = new Gson(); // Or use new GsonBuilder().create();
MyType target2 = gson.fromJson(json, MyType.class); // deserializes json into target2
I am playing around with SpringBoot lately along with Spring data jpa. Here's the thing. I have two classes Teacher and Course where there exists a OneToMany and ManyToOne relationship between them respectively. Course is a owning side of this relationship as seen below:
#Entity
#JsonSerialize(using = CourseSerializer.class)
#JsonIgnoreProperties(value = { "teacher", "students" }, allowGetters = true)
public class Course {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Getter #Setter
private Long id;
#Getter #Setter
private String title;
#ManyToOne(optional = false, fetch = FetchType.LAZY)
#JoinColumn(name = "teacher_id", referencedColumnName = "id")
#Getter
#Setter
private Teacher teacher;
....
}
And Teacher class looks like this:
#Entity
#JsonIgnoreProperties(value = { "courses" }, allowGetters = true)
public class Teacher {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Getter #Setter
private Long id;
#Getter #Setter
#JsonProperty("first_name")
private String firstName;
#Getter #Setter
#JsonProperty("last_name")
private String lastName;
#OneToMany(mappedBy = "teacher", cascade = CascadeType.ALL /*another problem: , fetch = EAGER*/)
#JsonSerialize(converter = ListCourseConverter.class)
#Getter #Setter
private List<Course> courses = new ArrayList<>();
....
}
Now Course cannot exist without a Teacher (optional=false). I am trying to make a POST call for the course creation this way to /api/course/teacherId:
{
"title": "Java Complete"
}
And I am expecting the following result from the call:
{
"id": 1,
"title": "Java Complete",
"teacher": "<firstName> <lastName>"
}
For this reason, I am using a CourseSerializer which serializes the Teacher object to display just that:
...
jgen.writeStartObject();
jgen.writeNumberField("id", value.getId());
jgen.writeStringField("title", value.getTitle());
jgen.writeStringField("teacher", value.getTeacher().getFirstName() + " " + value.getTeacher().getLastName());
jgen.writeEndObject();
...
But interestingly, I am seeing "null null" for the teacher field. My Controller -> Service looks like this for the above operation:
CourseController.java
---------------------
#PostMapping("/{teacherId}")
public Course createCourse(#RequestBody Course course, #PathVariable("teacherId") Long teacherId) {
course.setTeacher(new Teacher(teacherId));
return courseService.createCourse(course);
}
CourseService.java
------------------
public Course createCourse(Course course) {
return courseRepository.save(course);
}
So...
Problem 1) While doing course.getTeacher() for the returned object I see only ID field is populated. I have also tried changing the fetch type for the teacher field in Course class, making a findOne(course.getId()) call after the save(...) operation but doesn't work. But interestingly I see the proper result if I make a GET request after the earlier POST call. So, why am I not able to see the complete data in Course during POST since it is mapped with Teacher already?
Problem 2) Delete operation on Course does not work when the courses are set to fetch in EAGER fashion under Teacher class but works fine for LAZY fetch.
First, you don't need to use all those JSON properties if you use DTOs to receive and response.
course.setTeacher(new Teacher(teacherId)); this is not a good practice at all
use one and point to create the course, other to create the teacher.
after that, you can create a put method to update the course with the teacher
#PutMapping("/{courseId}/{teacherId}")
public Course addTeacherToCourse( #PathVariable("courseId") Long courseId,#PathVariable("teacherId") Long teacherId) {
return courseService.addTeacherToCourse(courseId,teacherId);
}
the course service should validate the courseID and teacherID and if they exist then update and save them.
here you can look at my example based on your code.
Working on this 'twitter' application where a user can have posts #OneToMany and can have followers #ManyToMany.
While retrieving a user all it's posts and followers get retrieved as well.
This is all correct but it's also retrieving every 'poster' for each post (which is the user itself) and for each follower, all it's posts and followers.
I can't figure out how to limit this to the user itself.
User
#Entity
#Table(name = "User")
#NamedQueries({
#NamedQuery(name = "User.findAll", query = "SELECT u FROM User u"),
#NamedQuery(
name = "User.auth",
query = "SELECT u FROM User u WHERE u.username = :username AND u.password = :password"
)
})
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(unique = true, nullable = false)
private String username;
#Column(nullable = false)
private String password;
#ManyToMany
#OneToMany(mappedBy = "poster", cascade = CascadeType.ALL)
private List<Post> posts = new ArrayList<>();
#JoinTable(name = "Followers",
joinColumns = {
#JoinColumn(name = "USER_ID", referencedColumnName = "ID")
},
inverseJoinColumns = {
#JoinColumn(name = "FOLLOWER_ID", referencedColumnName = "ID")
}
)
private List<User> followers = new ArrayList<>();
.... constructor, getters and setters
Post
#Entity
#Table(name = "Post")
public class Post {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
#ManyToOne
private User poster;
.... constructor, getters and setters
Outcome I get vs what I want
{
"id": 1,
"username": "jim",
"posts": [
{
"id": 1,
"content": "Post 1 by jim",
"poster": {
// ^ this is the user itself (I don't need this one)
"id": 1,
"username": "jim",
"posts": [
// keeps recurse
]
}
}
],
"followers": [
{
"id": 2,
"username": "follower1",
"posts": [
{
"id": 4,
"content": "Post 2 by follower 1",
"poster": {
// ^ this is the follower itself (I don't need this one)
"id": 2,
"username": "follower1",
"posts": [
// Same issue
]
}
}
],
"followers": [], // <-- I don't need this one either
}
]
}
Well it's pretty clear that fetching one user fill keeps fetching all it's relations which are recursive.
Is this a designer's fault or can this be ignored/limited?
Note: I am using Gson to serialise objects to JSON format
Update
Tried to use:
#ManyToOne(fetch = FetchType.LAZY)
private User poster;
Which works but still gets the following extra prop in JSONso not sure if this is a neath solution:
"_persistence_poster_vh": {
"sourceAttributeName": "poster",
"isInstantiated": false,
"row": {
"Post.ID": 3,
"Post.CONTENT": "Post 3 by jim",
"Post.DATETIME": "2018-01-22",
"Post.POSTER_ID": 1
},
"isCoordinatedWithProperty": false
}
And
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(
...
)
private List<User> followers = new ArrayList<>();
Which still returns all followers (which I want!) I just don't want the followers.followers and followers.posts..
Best guess: it’s not actually fetching these objects until you try to dereference them.
Be default, JPA will eager fetch #OneToOne and #OneToMany relations, but not #ManyToOne or #ManyToMany. What happens is that when you reference these fields, it will then go and fetch the actual contents of the list.
How can you tell this is happening? Check the list’s class using getFollowers().getClass()
What you see won’t be a LinkedList or an ArrayList but a class from your JPA provider, probably with “Lazy” somewhere in the name. When you call Size or Get on the list, it will perform the fetch.
You can set OneToOne and OneToMany relations to be lazy as well, and use EntityGraphs to determine what entities you want to eagerly fetch as well. JPA has a number of gotchas like this.
I’ve seen GSON mentioned, and just a warning: it won’t be aware of the lazy loading lists, so you MUST tell It to avoid the properties you don’t want it to follow.
Typically with JSON marshaling, you’ll want it to ignore the parent object, so in Post, User should be ignored for example. Additionally links to same types should typically be ignored (followers) or else mapped specially, such that it doesn’t Marshall the entire object, but only produces an array of usernames. You can tell it to ignore the actual followers field, and have it marshal a getter which returns an array of usernames to implement this.
You can specify fetch=FetchType.LAZY in the annotation you don't want to fetch immediately. The downside is, that if you need the data later you have to access it in the scope of the still open session.
There are two ways to handle this -
You can either use #JsonIgnoreProperties(ignoreUnknown=true) anotation on attributes you want to skip while serializing the object.
Or you change your FetchType to FetchType.LAZY so that you can get the required data on need basis while preparing your JSON , rather than getting all records at once.
I have two entities, Company and Job, with an OneToMany bidirectional relationship. My problem is that i can't lazy load the Company's List<Job> jobs.
For example when i do:
GET /api/companies/1 this is the JSON response:
{
"id": 1,
"name": "foo",
...
"_embedded": {
"jobs": [
{...},
...
{...}
],
"employees": [
{...},
{...}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/api/companies/1"
},
"jobs": {
"href": "http://localhost:8080/api/companies/1/jobs"
},
"employees": {
"href": "http://localhost:8080/api/companies/1/employees"
}
}
}
I don't want to have the _embedded since i didn't set the FetchType=EAGER.
Here are my models:
Company.java
#Entity
public class Company {
#Column(nullable = false, unique = true)
private String name;
#OneToMany(mappedBy = "company", fetch = FetchType.LAZY)
private List<Job> jobs;
...
public Company() {
}
...
}
Job.java
#Entity
public class Job {
#Column(nullable = false)
public String title;
#Column(length = 10000)
public String description;
#ManyToOne(fetch=FetchType.LAZY)
private Company company;
...
public Job() {
}
...
}
As you can see the same thing happens for other OneToMany relationships (employees). Can i avoid returning the whole list of job openings or employees every time?
EDIT: from the Job side the lazy load works fine! I don't get in the response the company that is related with a Job. I have to explicitly do /api/jobs/123/company in order to get the company.
EDIT2: Projections only work for collections. In this case it's not what i need. Excerpts could work, but i want to avoid them. I don't want to explicilty do /api/companies/1?projection=MyProjection since i won't use more than one. I want to change the default behavior, just like the projections do in collections.
EDIT3: i tried this
#RestResource(exported = false)
#OneToMany(mappedBy = "company")
private List<Job> jobs;
and i get the error Detected multiple association links with same relation type! Disambiguate association.
it's really annoying. I just need to get rid of _embedded. Anything?
You can use Entity Graph.Entity graphs are used to override at runtime the fetch settings of attribute mappings.For example
#Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {
#EntityGraph(attributePaths = { "members" })
GroupInfo getByGroupName(String name);
}
From Spring Data Jpa Documentation "4.3.10. Configuring Fetch- and LoadGraphs"
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
In addition;
I have a problem with #JsonIdentityInfo. I'm getting two id in json file.
{
"name" : "Tim",
"#id" : 1, // fasterxml garbage
"id" : 3,
"company" : {
"name" : "Microsoft",
"employees" : [1], // garbage too
"#id" : 2, // fasterxml garbage
"id" : 3
}
}
Here is my entity:
#Entity
#Table(name = "company")
#JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "id")
public class Company implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#Column
private String name;
#OneToMany(mappedBy = "company")
#Cascade(value = CascadeType.ALL)
private Collection<Employee> employees;
How to delete unnecessary Ids?
UPD:
I'm using com.fasterxml.jackson
This is an old question and you probably got the answer by now, but from reading it, it would seem your problem with additional ids comes from the generator you use.
IntSequenceGenerator will have Jackson generates auto-incremented ids, and you specified that you want them as id property (and as you already have an id property, I guess that's why Jackson produces #id). What you want is Jackson to use your existing id property, and for that you simply need to use the PropertyGenerator.
You will also need to use the scope attribute on the annotation as well as use it also on the Employee class, as you have two independent id sequences.
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope=Company.class)
public class Company {}
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope=Employee.class)
public class Employee {}
This should produce for your example
{
"name" : "Tim",
"id" : 3,
"company" : {
"name" : "Microsoft",
"employees" : [3],
"id" : 3
}
}
You can use JSON annotation like this to ignore them:
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
#JsonIgnoreProperties({"id", "name"})
When creating JSON object, "id" and "name" will not be in the JSON object.