Infinite recursion for null ids when using #JsonIdentityInfo - java

I have 2 DTO bidirectional structures Category and Product where Product is the many side in one-to-many relationship. I want to transfer them as JSON to the front-end layer by REST. I don't have any problems when ids are already assigned (for update operation), but I face well-known infinite recursion when ids are empty (create).
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property="categoryId")
public class CategoryDTO implements Serializable {
private Long categoryId;
private String categoryName;
private List<ProductDTO> products = new LinkedList<>();
public void addProduct(ProductDTO product) {
products.add(product);
product.setCategory(this);
}
// remove synchronization method, setters, getters
}
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property="productId")
public class ProductDTO implements Serializable {
private Long productId;
private String productName;
private CategoryDTO category;
// setters, getters
}
However, when I use #JsonManagedReference and #JsonBackReference all is fine. I receive beautiful json:
{
"categoryId":null,
"categoryName":"category_name",
"products": [
{
"productId":null,
"productName":"product1"
}
]
}
public class CategoryDTO implements Serializable {
private Long categoryId;
private String categoryName;
#JsonManagedReference
private List<ProductDTO> products = new LinkedList<>();
public void addProduct(ProductDTO product) {
products.add(product);
product.setCategory(this);
}
// remove synchronization method, setters, getters
}
public class ProductDTO implements Serializable {
private Long productId;
private String productName;
#JsonBackReference
private CategoryDTO category;
// setters, getters
}
In both examples the rest side is following:
#RestController
public class CategoryController {
#GetMapping(path = "/categories")
public ResponseEntity<CategoryDTO> fetchCategories() {
CategoryDTO category = new CategoryDTO();
category.setCategoryName("category_name");
ProductDTO product1 = new ProductDTO();
product1.setProductName("product1");
category.addProduct(product1);
return new ResponseEntity<>(category, HttpStatus.OK);
}
}
Why #JsonManagedReference and #JsonBackReference work, but #JsonIdentityInfo don't?
Thanks for reading.

#JsonManagedReference and #JsonBackReference are using object instance reference, so it works, when you add Product to Category, while #JsonIdentityInfois using id from value of the field.
The answer is in the class WritableObjectId and BeanSerializerBase#_serializeWithObjectId(Object, JsonGenerator, SerializerProvider, boolean).
In other words, for non-null fields serializer remembers, that he serialized given class instance, but he cannot do that for null fields. You can see that, when you hit endpoint /categories. Before the connection is interrupted, infinite JSON is generated.
Please someone correct me, if I'm wrong.
NOTE: Imo you should just remove field private CategoryDTO category or change it to private Long categoryId and you will get rid of any annotation :D Also you won't have any problem with infinite recursion.

Related

Stuck in a loop while passing RequestBody in Postman

#Entity
#Table(name = "customers")
public class Customer implements Serializable{
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private int custID;
private String custName;
#Id
private String email;
private int phone;
#OneToMany (mappedBy = "customer", fetch = FetchType.LAZY)
private List<Transaction> transaction;
#Entity
#Table(name = "transactions")
public class Transaction implements Serializable{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int transID;
private Date date;
private int amount;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "custID", nullable= false)
private Customer customer;
These are my entities, and I have a method:
#PostMapping("/record-transaction")
public Transaction recordTransaction(#RequestBody Transaction transaction) {
return transactionService.addTransaction(transaction);
}
But when I try to create JSON in postman, I get into a loop where while entering values for transaction, at the end I must enter the Customer object as well and when I am entering customer object at the end I again reach to enter the transaction's values. Its like a never ending loop. Help
I couldn't think of anything to do at all. My mind enters the loop itself.
Decouple your DB entities from your request/response by using an intermediate DTO.
Controller:
#PostMapping("/record-transaction")
public TransactionResponse recordTransaction(#RequestBody TransactionRequest body) {
return TransactionResponse.from(transactionService.addTransaction(
body.getDate();
body.getAmount();
body.getCustomerId();
));
}
TransactionRequest:
public class TransactionRequest {
//don't need ID here it'll be auto generated in entity
private Date date;
private int amount;
private int customerId;
}
TransactionResponse:
public class TransactionResponse {
private int id;
private Date date;
private int amount;
private int customerId;
public static TransactionResponse from(Transaction entity) {
return //build response from entity here
}
}
TransactionService:
//when your entity is lean may as well pass the values directly to reduce boilerplate, otherwise use a DTO
public Transaction addTransaction(Date date, int amount, int customerId) {
Customer customerRepo = customerRepo.findById(customerId).orElseThrow(
() -> new CustomerNotFoundException();
);
Transaction trans = new Transaction();
trans.setDate(date);
trans.setAmount(amount);
trans.setCustomer(customer);
return transactionRepository.save(trans);
}
If you want to embed the customer model inside TransactionResponse or TransactionRequest it'll be fairly easy to do and this solution will produce way nicer contract and swagger docs than a bunch of use case specific annotations in your entity.
In general decoupling you request/response payloads, service dtos and entities from each other results in code with more boilerplate but easier to maintain and without weird unexpected side effects and specific logic.

Java Spring Reactive, returning one Mono<..> from many multiple requests

[Java, Spring Reactive, MongoDB]
I'm currently trying to learn Reactive programming by doing and I found a challenge.
I have db object CategoryDB which looks like this:
#NoArgsConstructor
#Getter
#Setter
#Document(collection = DBConstraints.CATEGORY_COLLECTION_NAME)
class CategoryDB {
#Id
private String id;
private String name;
private String details = "";
#Version
private Long version;
private String parentCategoryId;
private Set<String> childCategoriesIds = new HashSet<>();
}
In a service layer I want to use model object Category.
#Getter
#Builder
public class Category {
private String id;
private String name;
private String details;
private Long version;
private Category parentCategory;
#Builder.Default
private Set<Category> childCategories = new HashSet<>();
}
I want to create Service with method Mono<Category getById(String id). In this case I want to fetch just one level of childCategories and direct parent Category. By default repository deliver Mono findById(..) and Flux findAllById(..) which I could use, but I'm not sure what would be the best way to receive expected result. I would be grateful for either working example or directions where can I find solution for this problem.
I've spent some time to figure out solution for this problem, but as I'm learning I don't know if it's good way of solving problems.
Added some methods to Category:
#Getter
#Builder
public class Category {
private String id;
private String name;
private String details;
private Long version;
private Category parentCategory;
#Builder.Default
private Set<Category> childCategories = new HashSet<>();
public void addChildCategory(Category childCategory) {
childCategory.updateParentCategory(this);
this.childCategories.add(childCategory);
}
public void updateParentCategory(Category parentCategory) {
this.parentCategory = parentCategory;
}
}
Function inside service would look like this:
#Override
public Mono<Category> findById(String id) {
return categoryRepository.findById(id).flatMap(
categoryDB -> {
Category category = CategoryDBMapper.INSTANCE.toDomain(categoryDB);
Mono<CategoryDB> parentCategoryMono;
if(!categoryDB.getParentCategoryId().isBlank()){
parentCategoryMono = categoryRepository.findById(categoryDB.getParentCategoryId());
}
else {
parentCategoryMono = Mono.empty();
}
Mono<List<CategoryDB>> childCategoriesMono = categoryRepository.findAllById(categoryDB.getChildCategoriesIds()).collectList();
return Mono.zip(parentCategoryMono, childCategoriesMono, (parentCategoryDB, childCategoriesDB) -> {
Category parentCategory = CategoryDBMapper.INSTANCE.toDomain(parentCategoryDB);
category.updateParentCategory(parentCategory);
childCategoriesDB.forEach(childCategoryDB -> {
Category childCategory = CategoryDBMapper.INSTANCE.toDomain(childCategoryDB);
category.addChildCategory(childCategory);
});
return category;
});
}
);
}
Where mapper is used for just basic properties:
#Mapper
interface CategoryDBMapper {
CategoryDBMapper INSTANCE = Mappers.getMapper(CategoryDBMapper.class);
#Mappings({
#Mapping(target = "parentCategoryId", source = "parentCategory.id"),
#Mapping(target = "childCategoriesIds", ignore = true)
})
CategoryDB toDb(Category category);
#Mappings({
#Mapping(target = "parentCategory", ignore = true),
#Mapping(target = "childCategories", ignore = true)
})
Category toDomain(CategoryDB categoryDB);
}
As I said I don't know if it's correct way of solving the problem, but it seem to work. I would be grateful for review and directions.

One to Many Json Reponse With Jackson is not Working

I have a problem using JPA and RelationsShips One to Many with Jackson and Spring Rest ... I try to find multiples solutions but anything is working for me , and I don't kno where is the problem.
For example I have a table Team that has One to Many/Many To One relationship
I have two repository one for Team and another for Player
Team >>> has Many >> Player
Player >>> many to one >> Team
My entity Team has the following content
#Entity
#Table(name = "teams")
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
public class Team {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private long teamId;
private String abbreviation;
private String team;
private String simpleName;
private String logo;
#OneToMany(cascade = {CascadeType.ALL,CascadeType.PERSIST,CascadeType.MERGE}, mappedBy = "team")
#Column(nullable = false)
private List<Player> players;
Theirs getters/setter , hashcodes and string similars.
On the other hand the entity Player
#Entity
#Table(name = "player")
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
public class Player {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private long id;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "teams_id", nullable=true)
private Team team;
private String name;
So , I have the typical get call in a controller in a repository.
#RestController
#RequestMapping("/api/public/team")
public class TeamController {
#Autowired
private TeamRepository teamRepository;
#Autowired
private GenericMethods genericMethods;
#GetMapping(value = "/{id}")
public Team getPersona(#PathVariable("id") int id) {
return teamRepository.findOne(genericMethods.toLong(id));
}
And repository
#Repository
public interface TeamRepository extends JpaRepository<Team, Long> {
}
Now , when I call this endpoint I receive the following answer and I think that is incorrect , I only need a List With Players
{
"id":2,
"teamId":0,
"abbreviation":null,
"team":null,
"simpleName":"Betis",
"logo":null,
"players":[
{
"id":1,
"team":2,
"category":{
"id":1,
"nombre":"juvenil a",
"language":null,
"description":null,
"league":[
],
"players":[
1,
{
"id":2,
"team":2,
"category":1,
"name":"hulio"
}
]
},
"name":"pepe"
},
2
]
}
I need to acces at information with Player and Team so I can't use #JsonIgnoreProperties
Could anyone help to solve this problem ?
Depending on what you really want to achieve you may try different options. I'm not sure if you're using (or intending to use) spring-data-rest or not.
1. Dedicated repository
Spring data rest will embed the related entities if they don't have their own repository. Try creating public interface PlayersRepository extends JpaRepository...
2. Lazy loading
Why are you using FetchType.EAGER ? Try without it.
3. Projections
Projections are only applicable to lists, not to individual entities (i.e. not explicitly what you're asking for). You can hide players from the Teams collection even if it was returned by default like so:
#Projection(name = "noPlayers", types = { Team.class })
public interface TeamWithoutPlayers {
Long getId();
long getTeamId();
String getAbbreviation();
String getTeam();
String getSimpleName();
String getLogo();
}
More info - Spring Data Rest Projections
4. Ignore during serialization in Team Entity using #JsonIgnore
#JsonIgnore
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "teams_id", nullable=true)
private Team team;
Final thought
With spring-data-rest you can extend a CrudRepository instead of JpaRepository and access the item directly through the repository. That way you don't need to write a controller.

Deserializing JSON from Hibernate

I'm having a problem de deserializing a class in Spring Boot. When my controller tries to deserialize it, it crashes. Here is the class:
#Entity
#Table(name="trash_cans")
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property="id")
public class TrashCan {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name="TRASH_CAN_ID")
long id;
#Column(name="TRASH_CAN_NAME")
String name;
#ManyToOne
#JoinColumn(name="PLACE_ID")
private Place place;
#OneToMany(mappedBy="trashCan", targetEntity=TrashMeter.class, fetch=FetchType.LAZY)
private List<TrashCan> trashMeterList;
#OneToMany(mappedBy="trashCan", targetEntity=TrashSensor.class, fetch=FetchType.LAZY)
private List<TrashSensor> trashSensorList;
public TrashCan() {
}
public TrashCan(long id, String name) {
super();
this.id = id;
this.name = name;
}
[getters and setters]
}
That depends on this one:
#Entity
#Table(name="trash_sensor")
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property="id")
public class TrashSensor {
#Id
#Column(name="id")
#GeneratedValue(strategy=GenerationType.AUTO)
private long id;
#Column(name="name")
private String name;
#Column(name="description")
private String description;
#ManyToOne
#JoinColumn(name="TRASH_CAN_ID")
private TrashCan trashCan;
#OneToMany(mappedBy = "trashSensor", targetEntity = Measurement.class, fetch = FetchType.LAZY)
private List<Measurement> measurementList;
public TrashSensor() {
super();
}
And Trash Sensor Depends on this Class:
#Entity
#Table(name="measurement")
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property="id")
public class Measurement {
#Id
#Column(name="id")
#GeneratedValue(strategy=GenerationType.AUTO)
private long id;
#Column(name="value")
private float value;
#Column(name="last_measure")
private LocalDateTime dateTime;
#ManyToOne
#JoinColumn(name="trash_sensor_id")
private TrashCan trashSensor;
public Measurement() {
}
}
My Controler:
#RequestMapping(value="/trashCan", method=RequestMethod.GET)
public ResponseEntity<Iterable<TrashCan>> getPlaces(){
Iterable<TrashCan> trashCanIterable = trashCansRepository.findAll();
return new ResponseEntity<>(trashCanIterable, HttpStatus.OK);
}
When I call the webservice, I get this error:
Failed to write HTTP message: org.springframework.http.converter.HttpMessageNotWritableException: Could not write content: could not deserialize (through reference chain: java.util.ArrayList[0]-br.com.simplepass.cleanerway.domain.TrashCan["trashSensorList"]-org.hibernate.collection.internal.PersistentBag[0]-br.com.simplepass.cleanerway.domain.TrashSensor["measurementList"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: could not deserialize (through reference chain: java.util.ArrayList[0]-br.com.simplepass.cleanerway.domain.TrashCan["trashSensorList"]-org.hibernate.collection.internal.PersistentBag[0]-br.com.simplepass.cleanerway.domain.TrashSensor["measurementList"])
I can't interpret this error =/. Any help with this problem greatly appreciated.
You are getting this error since your json is entering a loop, to avoid this, use #JsonIgnore annotation:
#OneToMany(mappedBy = "trashSensor", targetEntity = Measurement.class, fetch = FetchType.LAZY)
#JsonIgnore
private List<Measurement> measurementList;
It happens when you use relations between entities. Imagine that your TrashCan has link to Trash in it. And your trash has link to it's wrapper - trashcan. So what you try to serialize TrashCan entity you also serializing Trash. And then when you are serializing trash trashcan is serialized again inside it. And so on. It's a loop. You can use #JsonIgnore on every entity that may cause loop.
#JsonIgnore
#ManyToOne
#JoinColumn(name="PLACE_ID")
private Place place;
#JsonIgnore
#OneToMany(mappedBy="trashCan", targetEntity=TrashMeter.class, fetch=FetchType.LAZY)
private List<TrashCan> trashMeterList;
#JsonIgnore
#OneToMany(mappedBy="trashCan", targetEntity=TrashSensor.class, fetch=FetchType.LAZY)
private List<TrashSensor> trashSensorList;
But it's a bad way. It's strongly recommended to use DTO (Data transfer object) pattern for you serialization/deserialization. It also gives you more flexibility. You can read about it here
If you need trashMeterList and trashSensorList in response then follow this answer.
Due to hibernate lazy loading and no session while deserialisation, you are getting this exception.
To fix just change your controller:
#RequestMapping(value="/trashCan", method=RequestMethod.GET)
public ResponseEntity<Iterable<TrashCan>> getPlaces(){
Iterable<TrashCan> trashCanIterable = trashCansRepository.findAll();
List<TrashCan> responseList = new ArrayList<TrashCan>(trashCanIterable.size())
while(trashCanIterable.hasNext()){
TrashCan trashCan = trashCanIterable.next();
for(TrashMeter trashMeter : trashCan.trashMeterList){
}
for(TrashSensor trashSensor : trashCan.trashSensorList){
}
responseList.add(trashCan);
}
return new ResponseEntity<>(responseList, HttpStatus.OK);
}

How to avoid cyclic reference annotating with JPA?

I'm annotating my domain model for a shop (with JPA 2, using a Hibernate Provider).
In the shop every product can have a Category. Each category can be assigned to several super- and subcategories, meaning a category "candles" can have "restaurant" and "decoration" as parents and "plain candles" and "multi-wick candles" as children, etc.
Now I want to avoid cyclic references, i. e. a category "a" that has "b" as its parent which in turn has "a" as its parent.
Is there a way to check for cyclic references with a constraint in JPA? Or do I have to write some checks myself, maybe in a #PostPersist-annotated method?
Here's my Category class:
#Entity
public class Category {
#Id
#GeneratedValue
private Long id;
private String name;
#ManyToMany
private Set<Category> superCategories;
#ManyToMany(mappedBy="superCategories")
private Set<Category> subCategories;
public Category() {
}
// And so on ..
}
I believe you would have to check this through a business rule in your code. Why don't you separate these ManyToMany mappings in a separate Entity ? Like for example:
#Entity
#Table(name = "TB_PRODUCT_CATEGORY_ROLLUP")
public class ProductCategoryRollup {
private ProductCategory parent;
private ProductCategory child;
#Id
#GeneratedValue
public Integer getId() {
return super.getId();
}
#Override
public void setId(Integer id) {
super.setId(id);
}
#ManyToOne(fetch=FetchType.LAZY)
#JoinColumn(name="ID_PRODUCT_CATEGORY_PARENT", nullable=false)
public ProductCategory getParent() {
return parent;
}
public void setParent(ProductCategory parent) {
this.parent = parent;
}
#ManyToOne(fetch=FetchType.LAZY)
#JoinColumn(name="ID_PRODUCT_CATEGORY_CHILD", nullable=false)
public ProductCategory getChild() {
return child;
}
public void setChild(ProductCategory child) {
this.child = child;
}
}
In this way, you could before Saving a new entity, query for any existing Parent-Child combination.
I know I come back to the problem after several years but, I faced this problem, followed all of your resolutions and it didn't work for me. But I found the best solution using #JsonIgnoreProperties which solved the problem perfectly. In fact, I injected #JsonIgnoreProperties into the entity classes linked by a mapping like here:https://hellokoding.com/handling-circular-reference-of-jpa-hibernate-bidirectional-entity-relationships-with-jackson-jsonignoreproperties/

Categories

Resources