How to map a DTO to multiple entities? - java

I'm writing a Spring Application, which has two entities that are related by a one to many relationship, lets call them mother and kid.
When I create a mother entity via POST request, I want a kid entity be created automatically. Using the #OneToMany and #ManyToOne annotations, that works fine. At least, as long as I provide the kid information within the MotherService.
Here is my code
Mother.java
#Entity
#Table(name="mother")
public class Mother{
#Id
#Column(name="id", updatable = false, nullable = false)
private Long id;
#Column(name="name")
private String name;
#OneToMany(mappedBy = "mother", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Kid> kidList = new ArrayList<>();
//constructor, getter, setter
private void addKid(Kid kid) {
this.kidList.add(kid);
kid.setMother(this);
}
}
Kid.java
#Entity
#Table(name="kid")
public class Kid{
#Id
#Column(name="id", updatable = false, nullable = false)
private Long id;
#Column(name="name")
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "mother_id", nullable=false)
private Mother mother;
//constructor, getter, setter
}
MotherController.java
#RestController
#RequestMapping("mothers")
public class MotherController {
#Autowired
private MotherService motherService;
MotherController(MotherService motherService) {
this.motherService = motherService;
}
#PostMapping
Mother createMother(#RequestBody Mother mother) {
return this.motherService.createMother(mother);
}
}
MotherService.java
#Service
public class MotherService {
private MotherRepository motherRepository;
#Autowired
public MotherService (MotherRepository motherRepository) {
super();
this.motherRepository= motherRepository;
}
public Mother createMother(Mother mother) {
Kid kid = new Kid("Peter");
mother.addKid(kid);
return this.motherRepository.save(mother);
}
}
The repositories for mother and kid extend the JpaRepository without any custom methods so far.
My POST request is something like (using Postman)
{
"name":"motherName"
}
Now a mother is created with a name "motherName" and a kid with the name of "Peter".
My idea: Using a DTO
I now try to implement a DTO, that contains the mothers name and the kids name, map this information in the MotherService to the entities and save them via the corresponding repository, so I can define both names in the POST request.
motherDto.java
public class mother {
private String motherName;
private String kidName;
//getter, setter
}
So when I POST
{
"motherName":"Susanne",
"kidName":"Peter"
}
or even better
{
"mother": {
"name":"Susanne"
},
"kid": {
"name":"Peter"
}
}
a mother with name Susanne and a kid with name Peter are created.
My question is
How do I map a DTO to two entities?
Or do I not get something right? Is there an easier way to achieve my goal?

I know this is old and probably long solved, but let me offer a different take on the subject.
Another option would be to design a DTO solely for the purpose of creating the two entities you mentioned. You could call this MotherChildCreationDTO or something like that so the name already conveys its use and maybe create a REST-target consuming the DTO.
Asymmetric DTOs (receiving and sending) are an established pattern, and the DTOs are closely coupled to the REST controller any way.

First solution:
You can don't use DTO and send your JSON with same structure of Mother and kids and Jackson in Spring MVC deserialize it correctly for you.
{
id:2,
name:'sarah'
kidList:[{id:546,name:'bob'},{id:478,name:'tom'}]
}
Second solution:
If you want to different structure in JSON and Models and you can use Jackson annotation like #JsonProperty or #JsonDeserialize. Read this like for more information.
Third solution:
You can use DozzerMapper for complex mapping between your DTO and your Model. you define XML's file for mapping each model to your DTO and DozzerMapper map your DTO to your models.Read this link for more information.

You have 2 ways:
Map DTO to entities by yourself. In this case, you should create custom mapper and define how exactly DTO should be converted to entity. Then just inject and use your custom mapper in service.
Use one of existing mapper libraries. For example, good candidates are MapStruct and ModelMapper. You can find usage examples in corresponding getting started guides.

Related

How to handle #JsonManagedReference and #JsonBackReference for three connected tables

I am stuck on given scenario:
There are three entity
a) Bill (many to one relationship with vendor) [bi-directional]
b) Vendor (one to many relationship with both vendor and vendorbank)
c) VendorBank (many to one relationship with vendor)[uni-directional]
Bills : Showing limited fields
#Entity
#Getter
#Setter
#ToString
#NoArgsConstructor
#Table(name="bill_details")
public class Bills {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="bill_id")
private int bill_id;
#Column(name="bill_no")
private String billno;
#ManyToOne(cascade = {CascadeType.MERGE})
#JoinColumn(name="b_vendor_id")
private Vendors vendors;
/* Args contructor code here */
#JsonManagedReference
public Vendors getVendors() { return vendors; }
VendorBank:Showing limited fields
public class VendorBank {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="v_bank_id")
private int vendorBankId;
#Column(name="v_acc_no")
private String accountNumber;
#Column(name="v_vendor_id")
private int vendor_id;
/*Both constructor code here*/
}
Vendor Class:Showing imp fields only
/*Lombok code here*/
public class Vendors {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="vendor_id")
private int vendor_id;
#Column(name="vendor_name")
private String vendor_name;
#OneToMany(fetch = FetchType.LAZY,cascade = CascadeType.ALL)
#JoinColumn(name="v_vendor_id")
private List<VendorBank> vendorBank;
#OneToMany(mappedBy = "vendors",cascade = {CascadeType.DETACH,CascadeType.MERGE,
CascadeType.PERSIST,CascadeType.REFRESH})
private List<Bills> bills;
#JsonBackReference
public List<Bills> getBills() {
return bills;
}
//#JsonManagedReference()
// #JsonIgnore
public List<VendorBank> getVendorBank() {
return vendorBank;
}
Output:
{
"bill_id": 102,
"billno": "B-858",
"vendors": {
"vendor_id": 3,
"vendor_name": "ABC Company",
"vendorBank": [
{
"vendorBankId": 14,
"accountNumber": "502998745002",
"vendor_id": 3
}
]
}
}
1.When I call vendor endpoint I get data from vendor + vendor bank as desired.
2.But When I call the Bill endpoints then I get data from Bill + vendor + vendorbank as above. I don't want vendor bank to come.[If I use JsonIgnore on vendorbank then i get the correct output but then above point 1 goes wrong]
OK, so what you want to achieve is to include some data from the entity in one context and not include it in another context.
I don't think you can do this by using purely annotations you put on entities, since they don't have the calling context.
So, what can we do here ?
We can use Jackson Mixins. For example:
class YourClass {
public int ignoreThis() { return 0; }
}
With this Mixin
abstract class MixIn {
#JsonIgnore abstract int ignoreThis(); // we don't need it!
}
With this:
objectMapper.getSerializationConfig().addMixInAnnotations(YourClass.class, MixIn.class)
And you can then use this to serialize the object into json in your controller (where you get it to the service) and then add it as the response body and send to the user.
It would be ideal if you could somehow configure the ObjectMapper, which is used by Spring behind the scenes to do this, however, this is not possible ( at least easily ), because you need to tie this objectMapper to your specific controller only and not all controllers.

Perform Mapstruct mapping outside of Hibernate session

I am using Spring Data and Mapstruct and I don't want hibernate to blindly load all the elements while mapping entity to dto.
Example:
public class VacancyEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "job_category_id", nullable = false)
JobCategoryEntity jobCategory;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "company_id", nullable = false)
CompanyEntity company;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "employer_created_by", nullable = false)
EmployerProfileEntity employerCreatedBy;
#Column(nullable = false)
String title;
.... }
DTO:
public class VacancyDto {
Integer id;
String title;
CompanyDto company;
EmployerProfileDto employerCreatedBy;
JobCategoryDto jobCategory;
...}
So I have two methods findByIdWithCompanyAndCity and findByIdWithJobAndCityAndEmployer in VacancyRepository to perform only one SQL request.
And two #Transactional methods in my VacancyService: findWithCompanyAndCity and findWithCompanyAndCityAndEmployer.
Best practice is returning Dto from Service layer, so we need to parse Entity to Dto in the Service.
And I really don't want to just leave whole mapping in #Transactional (session) because if I add some field really deep into my entity, Mapstruct just trigger N+1 problem.
Best that I came up with, is to include each inner entity into method and check manually that Mapstruct don't add some new methods. (it is faster then checking names)
Ex:
#Mapping(target = "id", source = "entity.id")
#Mapping(target = "description", source = "entity.description")
#Mapping(target = "jobCategory", source = "jobCategoryDto")
#Mapping(target = "employerCreatedBy", source = "employerProfileDto")
#Mapping(target = "city", source = "cityDto")
#Mapping(target = "company", ignore = true)
VacancyDto toDto(VacancyEntity entity,
JobCategoryDto jobCategoryDto,
EmployerProfileDto employerProfileDto,
CityDto cityDto);
....
But this doesn't fix the real issue. There are still session while mapping, so it can lead to N+1 problem.
So I came up with several solutions
Use special method in Service to trigger #Transactional method and then map into DTO out of session scope. But it seems really ugly to double methods in Service
Return Entity from Service (which is Bad Practice) and map into DTO there.
I know that I'll get LazyInitializationException in both cases, but it seems to me like it more robust and scalable then just unpredictably SELECT.
How do I perform the mapping from entity to DTO in the service layer but outside the Hibernate session in an elegant way?
You didn't ask a question but it seems the question is supposed to be:
How do I perform the mapping from entity to DTO in the service layer but outside the Hibernate session in an elegant way.
I'd recommend the TransactionTemplate for this.
Usage looks like this:
#Autowired
VacancyRepository repo;
#Autowired
TransactionTemplate tx;
void someMethod(String company, String city){
VacancyEntity vac = tx.execute(__ -> repo.findWithCompanyAndCity(company, city));
return mappToDto(vac);
}
That said, I think you are using the wrong a approach to solve the underlying problem.
I suggest you take a look at having a test to verify the number of SQL statements executed.
See https://vladmihalcea.com/how-to-detect-the-n-plus-one-query-problem-during-testing/ for a way to do that.
To avoid the N + 1 problem you still need to use an entity graph, although I think this is a perfect use case for Blaze-Persistence Entity Views.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(VacancyEntity.class)
public interface VacancyDto {
#IdMapping
Integer getId();
String getTitle();
CompanyDto getCompany();
EmployerProfileDto getEmployerCreatedBy();
JobCategoryDto getJobCategory();
#EntityView(CompanyEntity.class)
interface CompanyDto {
#IdMapping
Integer getId();
String getName();
}
#EntityView(EmployerProfileEntity.class)
interface EmployerProfileDto {
#IdMapping
Integer getId();
String getName();
}
#EntityView(JobCategoryEntity.class)
interface JobCategoryDto {
#IdMapping
Integer getId();
String getName();
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
VacancyDto a = entityViewManager.find(entityManager, VacancyDto.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
Page<VacancyDto> findAll(Pageable pageable);
The best part is, it will only fetch the state that is actually necessary!

Dynamic fetching for relations in Spring JPA

I want to be able to dynamically load the relations of my entity, depending on which RestService got called.
Entity classes:
#Entity
public class Order {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#ManyToOne
private Buyer buyer;
// some more attributes
}
#Entity
public class Buyer {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
// some more attributes
}
RestController class:
#GetMapping
public Iterable<Order> getAll() {
// here I want JPA to NOT load the buyers for the order
return orderRepository.findAll();
}
#GetMapping("/{id}")
public Order get(#PathVariable("id") String id) {
// here I want JPA to load the buyers for the order
return orderRepository.findById(Long.parseLong(id)).orElseThrow();
}
None of the two fetchtypes LAZY and EAGER or json annotations (like #JsonIgnore, #JsonIdentityInfo, #JsonManagedReference and #JsonBackReference) seem to make this possible as far as I understood and tried.
If this is not possible, maybe someone can explain how to solve this problem then. On the one hand I sometimes need those relations in my frontend to display some values and on the other hand when I always load them I get huge performance problems or infinity recursions.
I don't think JPA supports your use case directly.
One option is to create the same entity twice - one with eager and the other with lazy. Switch them in the methods.
Another option is to use a DTO (Data Transfer Object) as the response, instead of the entity class itself. You will have to write a mapper logic to convert an entity to DTO though.

API Rest with Spring Boot

I'm seeing some videos about API Rest with Spring Boot and so far I've done some basics and when I tried to increase the complexity I'm getting caught.
My idea is in the Post / class, create a new class with students getting the following json:
{
"nome": "Primeira Serie - A".
"alunos": [
"João",
"José",
"Maria"
]
}
And return:
{
"id_classe": 101
}
It happens that it saves the class, but it does not save the students and I have no idea how to show only the id of the class.
I have created the following classes in Java:
Model
Classe.java
package com.example.classe.model;
//Import's suppressed
#Entity
#Table(name = "classe")
public class Classe {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String nome;
#OneToMany(mappedBy = "classe")
private Set<Aluno> alunos = new HashSet<Aluno>();
//Get's e Set's suppressed
}
Aluno.java
package com.example.classe.model;
//Import's suppressed
#Entity
#Table(name = "aluno")
public class Aluno {
private static int tempID = 0;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String nome;
#ManyToOne
#JoinColumn(name = "id_classe")
#JsonBackReference
private Classe classe;
public Aluno(String nome) {
tempID++;
this.id = tempID;
this.nome = nome;
}
public Aluno() {
}
//Get's e Set's suppressed
}
Repository
ClasseRepository.java
package com.example.classe.repository;
//Import's suppressed
#Repository
public interface ClasseRepository extends JpaRepository<Classe, Integer> {
public List<Classe> findAll();
}
Controller
ClasseController.java
package com.example.classe.controller;
//Import's suppressed
#RestController
#RequestMapping("/classe")
public class ClasseController {
#Autowired
private ClasseRepository classeRepo;
#RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<Collection<Classe>> getClasse() {
return new ResponseEntity<>(classeRepo.findAll(), HttpStatus.OK);
}
#RequestMapping(method = RequestMethod.POST)
public ResponseEntity<?> salvarClasse(#RequestBody Classe classe) {
return new ResponseEntity<>(classeRepo.saveAndFlush(classe), HttpStatus.CREATED);
}
}
Am I doing everything wrong or did I not understand the concept? But I wanted to understand how to do it that way.
Thanks in advance.
Cesar Sturion
What you want to achieve is totally doable, but requires several changes.
I split my answer into 2 parts:
Save the students
There are several problems with saving:
On POST your incoming json deserialized into objects in which Classe has a reference to Anuli, but Anuli doesn't have a reference toClasse. To check it you can add a break point at the line: return new ResponseEntity<>(... , run in debug mode and check fields of Anuli in Classe. To fix it you can add #JsonManagedReference on aluni field in Classe. Related question
Hibernate can't save referenced objects by default. You have to save them one by one after saving your Classe object or just turn on Cascade persisting. Related question
So, to fix 1 and 2 Classe should have:
#OneToMany(mappedBy = "classe", cascade = CascadeType.PERSIST)
#JsonManagedReference
private Set<Aluno> alunos = new HashSet<Aluno>();
You have to remove custom id generation in Alumi (I am talking about static int tempID). Annotation #GeneratedValue will perfectly generate id for you as soon as you persist an object. This custom generation breaks Hibernate support. I even not talking about that it also breaks the app after restart, not threadsafe etc.
Return id only
On POST returned json represent what was returned in classeRepo.saveAndFlush(classe) so it's an object of Classe.
If you want to return exactly this:
{
"id_classe": 101
}
Then create new class like this:
public class ClasseIdVO {
#JsonProperty("id_casse")
private Integer id;
// Constructors, getter, setter
VO - means View Object, so this object only for representation, not for persisting, etc.
You can use field name id_casse, but it's against Java code convention, so better add #JsonProperty.
Also change your saving code to new ClasseIdVO(classeRepo.saveAndFlush(classe).getId())
Or you can just return id as a number: classeRepo.saveAndFlush(classe).getId()

spring-data-rest integration test fails with simple json request

My spring-data-rest integration test fails for a simple json request. Consider the below jpa models
Order.java
public class Order {
#Id #GeneratedValue//
private Long id;
#ManyToOne(fetch = FetchType.LAZY)//
private Person creator;
private String type;
public Order(Person creator) {
this.creator = creator;
}
// getters and setters
}
Person.java
ic class Person {
#Id #GeneratedValue private Long id;
#Description("A person's first name") //
private String firstName;
#Description("A person's last name") //
private String lastName;
#Description("A person's siblings") //
#ManyToMany //
private List<Person> siblings = new ArrayList<Person>();
#ManyToOne //
private Person father;
#Description("Timestamp this person object was created") //
private Date created;
#JsonIgnore //
private int age;
private int height, weight;
private Gender gender;
// ... getters and setters
}
In my test I created a person by using personRepository and inited order by passing person
Person creator = new Person();
creator.setFirstName("Joe");
creator.setLastName("Keith");
created.setCreated(new Date());
created.setAge("30");
creator = personRepository.save(creator);
Order order = new Order(creator);
String orderJson = new ObjectMapper().writeValueAsString(order);
mockMvc.perform(post("/orders").content(orderJson).andDoPrint());
Order is created but creator is not associated with the order. Also I want to pass request body as a json object. In this my json object should contain creator as follows
{
"type": "1",
"creator": {
"id": 1,
"firstName": "Joe",
"lastName": "Keith",
"age": 30
}
}
If I send request body with the following json, the call works fine
{
"type": "1",
"creator": "http://localhost/people/1"
}
But I don't want to send the second json. Any idea how to solve the issue. Because already my client is consuming the server response by sending first json. Now I migrated my server to use spring-data-rest. After that all my client code is not working.
How to solve this?
You are correctly associating order with the creator, however the Person is not associated with the orders. You are missing the List<Order> orders field in Person class. Add this, add annotations, add methods for adding order to person and then before sending JSON you should call something like this:
creator.addOrder(order);
order.setCreator(cretr);
Did you try using cascade = CascadeType.ALL in #ManyToOne annotation
public class Order {
#Id #GeneratedValue//
private Long id;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)//
private Person creator;
private String type;
public Order(Person creator) {
this.creator = creator;
}
// getters and setters
}
Both your Order and Person classes should implement Serializable to properly break them down into and rebuild them from JSON.
There are some ways to solve your problem, but I want give you a hint. You just can save only "id" of your person and get the person by "id" from your database, when you need this.
It solves your problem and it also saves the memory.
I believe you need to do two things to get this work.
Handle the deserialization properly. As you expect Jackson to populate the nested Person object via the constructor you need to annotate this with #JsonCreator. See here:
http://www.cowtowncoder.com/blog/archives/2011/07/entry_457.html
One of more powerful features of Jackson is its ability to use arbitrary >constructors for creating POJO instances, by indicating constructor to use with
#JsonCreator annotation
...........................................
Property-based creators are typically used to pass one or more
obligatory parameters into constructor (either directly or via factory
method). If a property is not found from JSON, null is passed instead
(or, in case of primitives, so-called default value; 0 for ints and so
on).
See also here on why Jackson may not be able to automatically work this out.
https://stackoverflow.com/a/22013603/1356423
Update your JPA mappings. If the associated Person is now populated correctly by the Jackson deserializer then by adding the necessary JPA cascade options to the relationship then both instances should be persisted.
I think then the following should work as expected:
public class Order {
#Id
#GeneratedValue(...)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, cascade = cascadeType.ALL)
private Person creator;
private String type;
#JsonCreator
public Order(#JsonProperty("creator") Person creator) {
this.creator = creator;
}
}

Categories

Resources