Spring boot JPA: recursion on JSON-view in a self join relationShip - spring-boot

For a project that I am trying to build, I need a manyToMany relationship From the User Class to the Class User.(Users have Friends and Friends are friends of Users).
When trying to get Json out of SpringBoot with JPA. I get a recursive loop on my JSON.(This happens only if two users are friends of each other)
I know what happens but cannot find the solution to the problem.
As you can see I'm using different views to 'filter' the view. The question is: How can I stop the recursion?
#Entity
public class User {
#Id
#GeneratedValue
#JsonView(JsonViews.UserView.class)
private long userId;
#JsonView(JsonViews.UserView.class)
private String username;
#JsonView(JsonViews.UserView.class)
private String password;
#JsonView(JsonViews.UserView.class)
private String email;
#JsonView(JsonViews.UserView.class)
private String location;
//private String avatar;
#JsonView(JsonViews.UserView.class)
private int ranking;
private String UserStatus;
//Friend Relation Many > Many
#JsonView({JsonViews.UserView.class, JsonViews.FriendWith.class})
#ManyToMany()
private List<User> friendsWith;
#ManyToMany(mappedBy = "friendsWith")
private List<User> friendOf;
#JsonView({JsonViews.UserView.class, JsonViews.Player.class})
#OneToMany(mappedBy = "user")
private List<Player> player;
public User() {
this.friendsWith = new ArrayList<>();
this.friendOf = new ArrayList<>();
}
public User(String username, String password, String email, String location, int ranking) {
this();
//this.userId = userId;
this.username = username;
this.password = password;
this.email = email;
this.location = location;
this.ranking = ranking;
}
// and th usual getters and setters
Stack:
2019-12-10 22:17:52.414 ERROR 1618 --- [nio-8084-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service()
for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: org.hibernate.collection.internal.PersistentBag[0]->
nl.hva.ewa.stratego.backend.models.User["friendsWith"]->
org.hibernate.collection.internal.PersistentBag[0]->
nl.hva.ewa.stratego.backend.models.User["friendsWith"]->
org.hibernate.collection.internal.PersistentBag[0]->
nl.hva.ewa.stratego.backend.models.User["friendsWith"]->
org.hibernate.collection.internal.PersistentBag[0]->
nl.hva.ewa.stratego.backend.models.User["friendsWith"]->
ETC.... ETC.....
for completeness the JsonViews class:
public class JsonViews {
public class UserView { };
public class AComplete extends UserView { };
public class BComplete extends UserView { };
public class FriendsWith extends UserView {};
public class FriendsOf extends UserView {};
public class Player extends UserView {};
}

Try the #JsonIdentityInfo annotation, where you can tell Jackson what is the POJO id, so it does not repeat it:
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "userId")
public class User {
public long userId;
public String name;
public List<User> friends;
}
That will generate the following JSON:
{
"userId" : 11,
"name" : "A",
"friends" : [ {
"userId" : 22,
"name" : "B",
"friends" : [ 11, {
"userId" : 33,
"name" : "C",
"friends" : [ ]
} ]
} ]
}
Notice that in the inner user B, the friends list contains just the ID if the user has been already added to the JSON body [ 11, {.

Related

What causes unability to fetch entities in this code?

So i'm developing a REST API for my Spring appplication. I have to store all data in H2 database and i'm trying to find a correct way to do so. I'm new to JPA and databases and general and need help understanding the causes of errors here.
First, i have these entities.
Position.java:
package com.example.offerserver.offerservice.task1;
#Entity
#Table(name = "position_table")
public class Position {
public Position() {
}
public Position(UUID id, String name, Integer salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
#Id
private UUID id;
#Column(name = "name")
private String name;
#Column(name = "salary")
private Integer salary;
//getters and setters
Stuff.java:
package com.example.offerserver.offerservice.task1;
#Entity
#Table(name = "stuff_table")
public class Stuff {
public Stuff(){};
public Stuff(UUID id,
String surname,
String name,
String patronymic,
boolean sex,
LocalDate birthDate,
Double salaryMultiplier,
Position position) {
this.id = id;
this.surname = surname;
this.name = name;
this.patronymic = patronymic;
this.sex = sex;
this.birthDate = birthDate;
this.salaryMultiplier = salaryMultiplier;
this.position = position;
}
#Id
private UUID id;
#Column(name="surname")
private String surname;
#Column(name="name")
private String name;
#Column(name="patronymic")
private String patronymic;
#Column(name="sex")
private boolean sex;
#Column(name="birth_date")
private LocalDate birthDate;
#Column(name="salary_multiplier")
private Double salaryMultiplier;
#OneToOne(fetch = FetchType.LAZY)
private Position position;
And JPA repositories:
package com.example.offerserver.repository;
#Repository
public interface StuffRepository extends JpaRepository<Stuff, String> {
}
package com.example.offerserver.repository;
#Repository
public interface PositionRepository extends JpaRepository<Position, UUID> {
}
And i have this request:
package com.example.offerserver.controller;
#Controller
#RequestMapping("/api/v1/stuff")
public class StuffListController {
#Autowired
StuffRepository repository;
#GetMapping("")
public ResponseEntity<List<Stuff>> getStuffList(){
List<Stuff> stuff = repository.findAll();
return new ResponseEntity<>(stuff, HttpStatus.OK);
Sending this request i'm getting this error:
Could not write JSON: Unable to find com.example.offerserver.offerservice.task1.Position with id f0bd15f2-74c1-4979-854b-ffbe44b3da59; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unable to find com.example.offerserver.offerservice.task1.Position with id f0bd15f2-74c1-4979-854b-ffbe44b3da59 (through reference chain: java.util.ArrayList[0]->com.example.offerserver.offerservice.task1.Stuff["position"]->com.example.offerserver.offerservice.task1.Position$HibernateProxy$E63ZeIxs["id"])
org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Unable to find com.example.offerserver.offerservice.task1.Position with id f0bd15f2-74c1-4979-854b-ffbe44b3da59; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unable to find com.example.offerserver.offerservice.task1.Position with id f0bd15f2-74c1-4979-854b-ffbe44b3da59 (through reference chain: java.util.ArrayList[0]->com.example.offerserver.offerservice.task1.Stuff["position"]->com.example.offerserver.offerservice.task1.Position$HibernateProxy$E63ZeIxs["id"])
In debug every instance of stuff in the list is initialized without its "position" field, throwing an error:
Method threw 'javax.persistence.EntityNotFoundException' exception. Cannot evaluate com.example.offerserver.offerservice.task1.Position$HibernateProxy$2ZiRYZbP.toString()
This is how position repository is initialized on launch:
public static List<Position> POSITIONS = List.of(
new Position(UUID.randomUUID(), "Junior Java Backend Developer", 60000),
new Position(UUID.randomUUID(), "Middle Machine Learning Engineer", 120000),
new Position(UUID.randomUUID(), "Senior DevOps Engineer", 200000),
new Position(UUID.randomUUID(), "Senior DevOps Engineer", 150000),
new Position(UUID.randomUUID(), "Intern System Engineer", 20000)
);
positionRepository.saveAll(POSITIONS);
Stuff repository as well. Position field for every stuff instance is randomly chosen from a POSITIONS list.

How to unwrap custom serialized objects from "content" key with Jackon & Spring Boot

I have customly serialized my object which is represented by this class:
public class Product {
private Long id;
private String name;
private Unit defaultUnit;
private Section section;
}
Serialization:
#Override
public void serialize(Product product, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeNumberField("id", product.getId());
jsonGenerator.writeStringField("name", product.getName());
jsonGenerator.writeStringField("defaultUnit", product.getDefaultUnit().toString());
jsonGenerator.writeObjectField("section", product.getSection());
}
However this produces an error which as I understand means that the default serializer has created a key and I have to provide it with a value:
Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Can not write a field name, expecting a value; nested exception is com.fasterxml.jackson.core.JsonGenerationException: Can not write a field name, expecting a value]
Now it is obvious that the solution is to wrap the fields adding
jsonGenerator.writeStartObject();
jsonGenerator.writeEndObject();
This however results in generating entity wrapped in "content" object:
"content": {
"id": 1,
"name": "Product",
"defaultUnit": "Unit",
"section": {
"name": "Section"
}
}
My question is whether it is possible to write the entity unwrapped that is without the "content" key.
The following is all the code associated with Product:
Product.java
#Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
#RequiredArgsConstructor
#JsonDeserialize(using = ProductDeserializer.class)
#JsonSerialize(using = ProductSerializer.class)
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NonNull
private String name;
#ManyToOne
#NonNull
private Unit defaultUnit;
#ManyToOne
#NonNull
private Section section;
}
ProductExcerpt.java
#Projection(name = "productExcerpt", types = {Product.class})
public interface ProductExcerpt {
String getName();
#Value("#{target.defaultUnit.toString()}")
String getDefaultUnit();
SectionExcerpt getSection();
}
ProductRepository.java
#RepositoryRestResource(excerptProjection = ProductExcerpt.class)
#CrossOrigin
public interface ProductRepository extends JpaRepository<Product, Long> {
}
ProductSerializer.java
public class ProductSerializer extends StdSerializer<Product> {
ProductSerializer(){
super(Product.class);
}
#Override
public void serialize(Product product, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeNumberField("id", product.getId());
jsonGenerator.writeStringField("name", product.getName());
jsonGenerator.writeStringField("defaultUnit", product.getDefaultUnit().toString());
jsonGenerator.writeObjectField("section", product.getSection());
jsonGenerator.writeEndObject();
}
}
There is also ProductDeserializer class however I think that it is irrevelant in this case. I do not have a Controller configured beacause as far as I am concerned there is no need while using spring-data-rest.

Hibernate: How to display data from multiple table

I am new in spring/hibernate technologies, I have tried to find an information about it, but failed, so if you can help I will be so thankful!
I need to display a JSON response in browser of multiple tables, one of the table has primary key for another one.
My entities:
#Entity
#Table
#ToString
public class Book {
#Id
#GeneratedValue(strategy = AUTO)
#JsonView(Views.IdName.class)
private Long book_id;
#JsonView(Views.IdName.class)
private String name;
#Column(length = 1000000)
private String text;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="author_id")
#JsonView(Views.IdName.class)
private Author author;
// ....get/set methods
Another one:
#Entity
#Table
#ToString
public class Page {
#Id
#GeneratedValue(strategy = AUTO)
private Long id;
#Column(length = 1000000)
private String text;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "book_id")
private Book book;
// ...get/set methods
My controllers:
#RestController
#RequestMapping("books")
public class BookController {
private final BookRepo bookRepo;
#Autowired
public BookController(BookRepo bookRepo) {
this.bookRepo = bookRepo;
}
#GetMapping
#JsonView(Views.IdName.class)
public List<Book> getAll() {
return bookRepo.findAll();
}
#GetMapping("{id}")
public Book getOne(#PathVariable("id") Book book) {
return book;
}
}
Another one:
#RestController
#RequestMapping("authors")
public class AuthorController {
private final AuthorRepo authorRepo;
#Autowired
public AuthorController(AuthorRepo authorRepo) {
this.authorRepo = authorRepo;
}
#GetMapping
public List<Author> getAll() {
return authorRepo.findAll();
}
#GetMapping("{id}")
public Optional<Author> getOne(#PathVariable("id") Long id) {
return authorRepo.findById(id);
}
}
And also repo for interaction with DB (they are the similar):
public interface AuthorRepo extends JpaRepository<Author, Long> {
}
So when I make a request for get all books, I take the following JSON:
enter image description here
Bit I want different result, something like:
[
{
"book_id" : 1,
"name": "name 1 book",
"author" :
{
"author_id" : 1,
"name": "some_name"
}
}
]
Also, when I tried to make a request for /authors/1, I will get the following response (something like recursion) :
enter image description here
So any help how can I handle with it? Thank you!
You can use a #NoRepositoryBean
like in this example:
#NoRepositoryBean
public interface MappedTypeRepository<T extends AbstractMappedType>
extends Repository<T, Long> {
#Query("select new com.example.YourObjectWithConstructor(e.attribute, sub.sub_attribute) from entity e inner join e.subtable sub where e.attribute = ?1")
List<YourObjectWithConstructor> findAllByAttribute(String attribute);
}
My example may not be 100% correct, I did not check the syntax. Feel free to explore it
Check this also:
JPQL Create new Object In Select Statement - avoid or embrace?

Spring MongoDB + QueryDSL query by #DBRef related list

#Document(collection="users")
public class User{
#Id
private int id;
private String name;
...
//getters-setters
}
#Document(collection="models")
public class Model{
#Id
private int id;
private String name;
#DBRef
private List<User> users;
...
//getters-setters
}
I tried this solution but it doesnt return anything:
QModel model = new QModel();
Pageable pageable = new PageRequest(0, 100);
return modelsRepository.findAll(model.users.any().id.eq(anUserId), pageable);
I think it all depends on the JSON data in MongoDB collection.
In your case, the "models" collection should have an attribute of "users" Array. As long as the key name is matching (i.e. "users"), it should work.
Detailed Example:-
The following example works fine.
People Collection:-
{
"_id" : ObjectId("57c8269b3ee7df409d4d2b64"),
"name" : "Erin",
"places" : [
{
"$ref" : "places",
"$id" : ObjectId("57c813b33ee7df409d4d2b58")
}
],
"url" : "bc.example.net/Erin"
}
Places Collection:-
{
"_id" : ObjectId("57c813b33ee7df409d4d2b58"),
"name" : "Broadway Center",
"url" : "bc.example.net"
}
Classes:-
Places class:-
#Document(collection = "places")
public class Places implements Serializable {
private static final long serialVersionUID = -5500334641079164017L;
#Id
private String id;
private String name;
private String url;
...get and setters
}
People class:-
#Document(collection = "people")
public class People implements Serializable {
private static final long serialVersionUID = 6308725499894643034L;
#Id
private String id;
private String name;
#DBRef
private List<Places> places;
private String url;
...get and setters
}
Repository class:-
#Repository
public interface PeopleRepository extends PagingAndSortingRepository<People, String> {
public People findById(String id);
#Query(value = "{ 'status' : ?0 }")
public Page<People> findByStatus(String status, Pageable pageable);
}
FindAll:-
public Boolean findAllPeople() {
Page<People> peoplePage = peopleRepository.findAll(new PageRequest(0, 20));
System.out.println("Total elements :" + peoplePage.getTotalElements());
for (People people : peoplePage) {
System.out.println("People id :" + people.getId());
System.out.println("Place size :" + people.getPlaces().size());
people.getPlaces().forEach(p -> System.out.println(p.getName()));
}
return true;
}

Hibernate: org.hibernate.MappingException: Could not determine type for: java.util.Set

I am trying to use Set in hibernate. The problem I don't know to how to write annotation on the Set table.
#Entity
#Table(name="user_settings")
public class UserSettings {
#Id
#GeneratedValue
private int id;
private int userid;
protected Set<Integer>foreignLanguageId;
public UserSettings() {
}
public UserSettings(int userid, int nativeLanguageId,
Set<Integer> foreignLanguageId, Date birthday) {
this.userid = userid;
this.nativeLanguageId = nativeLanguageId;
this.foreignLanguageId = foreignLanguageId;
this.birthday = birthday;
}
#OneToMany
#JoinColumn(name="userid", nullable=false)// This annotation
public Set<Integer> getForeignLanguageId() {
return foreignLanguageId;
}
Error:
org.hibernate.MappingException: Could not determine type for: java.util.Set, at table: user_settings, for columns: [org.hibernate.mapping.Column(foreignLanguageId)]
at org.hibernate.mapping.SimpleValue.getType(SimpleValue.java:314)...........
You should to use entity to make an association not id
#Entity
#Table(name="user_settings")
public class UserSettings {
private Set<Language> foreignLanguages;
#OneToMany
#JoinColumn(name="fk_user_setting", nullable = false)
public Set<Language> getForeignLanguages() {
return foreignLanguages;
}
}
if you want to use Integer you can use #ElementCollection
#Entity
#Table(name="user_settings")
public class UserSettings {
private Set<Integer> foreignLanguages;
#ElementCollection
public Set<Integer> getForeignLanguages() {
return foreignLanguages;
}
}

Resources