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()
Related
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.
In my application I am using projections to map *Entity objects to simplified or modified versions of the actual record in the database.
However, I have a particular use case where I am required to replace a certain value from one of the nested projections. Since these are interfaces and also get proxied by Spring, I am not sure if what I want is actually possible but to bring it down to one very simpel example:
Assume I have a UserEntity and a User projection. For my User projection I can simply execute:
User user = this.userEntityRepository.findById(userId);
However, if I want to change something, I am not sure if that is possible. Namely, I cannot do something like this:
if (user.getAge() < 18) {
user.setDisplayName(null);
}
Now, I am aware that I could create an anonymous class new User() { .. } and just pass in the values I required but in my case the objects are nested and hence this is not an option.
The question
Is there another way to replace a value, e.g. displayName as above, without using an anonymous class?
Elaborative example
Reading the following is not really necessary but in order to illustrate my issue in more detail I have pseudo-coded an example that shows a bit closer what the problem is in my particular case.
We have a simple UserEntity:
#Entity
#Table(name = "app_user")
public class UserEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column
private String firstName;
#Column
private String lastName;
#Column
private Integer age;
// Setter & Getter ..
}
#Entity
#Table(name = "event")
public class EventEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(mappedBy = "event")
private List<EventAttendeeEntity> attendees;
// ..
}
We have a table which maps users to events:
#Entity
#Table(name = "attendee")
public class AttendeeEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
private EventEntity event;
#ManyToOne
private UserEntity user;
// ..
}
Further, we have projections for these entities which we prepare as views for our clients:
/*
* Projection for User
*/
public interface User {
// All the properties ..
}
/*
* Projection for AttendeeEntity
*/
public interface Attendee {
Long getId();
User getUser();
}
/*
* Projection for EventEntity
*/
public interface Event {
Long getId();
String getName();
List<Attendee> getAttendees();
}
In one of the services we fetch UserEvent. Here, let's say, we want to remove the names of all users below 18 and still return userEvent we just fetched.
public Event getEvent(Long id, Boolean anonymize) {
Event event = this.eventRepository.findById(id);
// The "anonymize" is to highlight that I cannot
// simply solve this in a User-projection
if (!anonymize) {
return event;
}
event
.getAttendees();
.stream()
.peek(attendee -> {
User user = attendee.getUser();
if(user.getAge() < 18) {
// Here we create a new user object without a name
User newUser = new User() {
#Override
String getDisplayName() { return null; }
#Override
Integer getAge() { return user.getAge(); }
}
// !! This is where we hit the problem since we cannot
// !! replace the old user object like this
attendee.setUser(newUser);
}
});
return event;
}
One solution is to use SPEL in you projection selector. Please try
public interface Attendee {
Long getId();
#Value("#{target.user.age >= 18 ? target.user : new your.package.UserEntity()}")
User getUser();
}
Replace you.package with the package of UserEntity. Pay attention to put new UserEntity() and not new User(). This way an empty model will be projected as an empty interface User.
You can't use projections to update your code.
As a final note, it's important to remember that projections and excerpts are meant for the read-only purpose.
API Data Rest Projections Baeldung
I am doing a REST API using springboot and JPA.
I am trying to lazy fetching an entity in a One to Many relationship. Teacher to courses.
I can see the sql statements done by JPA as I have the debuging option on.
In the controller, when calling a path all works great, but I can see that JPA is executing two queries. One for the teacher and another one for its courses. As I know, the lazy loading does not query until the data is required and I am not requiring it.
I have checked and conirmed that in the controller, when I retrieve the teacher data JPA does not query for the courses, but AFTER the return statement of the controller, somewhere, the courses are required and it loads everything when I call the teacher info from postman with a GET call.
It seems as if the LAZY loading is working correctly, but after the controller JPA loads the course list. If I do the EAGER fetching everything is loaded before the return statemnt.
I am not writing any code as I guess the question is more theorical than practical.
Does anyone know how this works?
Thank you so much!!!!
EDIT:
Teacher table
#Entity
#Table(name="profesores")
public class Profesor implements Serializable{
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#Column(name="nombre")
private String nombre;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "profesor_id", referencedColumnName = "id")
private List<Curso> cursos = new ArrayList<>();
}
Course Table
#Entity
#Table(name = "curso")
public class Curso implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long curso_id;
private String nombre;
#Column(name="profesor_id")
private Long profesorId;
}
Controller
#GetMapping("/profesor/{id}")
public ResponseEntity<?> getProfesor(#PathVariable(value = "id") Long id){
Profesor p = profesorService.findById(id);
if(p!=null) {
ResponseEntity<?> re = new ResponseEntity<>(p, HttpStatus.OK);
//Just one query executed. I don't know the courses yet
return re;
}
else {
return new ResponseEntity<Void>(HttpStatus.NOT_FOUND);
}
}
After the return re; statement, somewhere, the courses are retrieved and JPA queries for them. I don't know what does the controller call, as I do directly from PostMan.
After returned Entity Profesor is serialized for response when serializer try to access courses to serialized for response then JPA load courses also. To solve this issue, You can create a response class for response (without courses field)
public class ProfesorResponse {
private Long id;
private String number;
...constructor
}
then map your entity in response object and return it.
Profesor p = profesorService.findById(id);
ProfesorResponse response = new ProfesorResponse(p.getId(), p.getNumber());
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.
I have tried several things I found while searching but nothing helped or I did not implement it correctly.
Error I'm getting
Direct self-reference leading to cycle (through reference chain: io.test.entity.bone.Special["appInstance"]->io.test.entity.platform.ApplicationInstance["appInstance"])
Both these extend the base entity and in the base (super class) it has an appInstance as well.
Base entity looks similar to this
#MappedSuperclass
public abstract class BaseEntity implements Comparable, Serializable {
#ManyToOne
protected ApplicationInstance appInstance;
//getter & setter
}
Application entity looks like this
public class ApplicationInstance extends BaseEntity implements Serializable {
private List<User> users;
// some other properties (would all have the same base and application instance . User entity will look similar to the Special.)
}
Special entity
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "objectType")
#JsonIgnoreProperties({"createdBy", "appInstance", "lastUpdatedBy"})
public class Special extends BaseEntity implements Serializable {
#NotNull
#Column(nullable = false)
private String name;
#Column(length = Short.MAX_VALUE)
private String description;
#NotNull
#Column(nullable = false)
private Double price;
#OneToOne
private Attachment image;
#Enumerated(EnumType.STRING)
#ElementCollection(targetClass = SpecialTag.class)
#CollectionTable(name = "special_tags")
#Column(name = "specialtag")
private List<SpecialTag> specialTags;
#Temporal(TemporalType.TIME)
private Date specialStartTime;
#Temporal(TemporalType.TIME)
private Date specialEndTime;
#Enumerated(EnumType.STRING)
#ElementCollection(targetClass = WeekDay.class)
#CollectionTable(name = "available_week_days")
#Column(name = "weekday")
private List<WeekDay> availableWeekDays;
#OneToMany(mappedBy = "special", cascade = CascadeType.REFRESH)
private List<SpecialStatus> statuses;
#OneToMany(mappedBy = "special", cascade = CascadeType.REFRESH)
private List<SpecialReview> specialReviews;
#Transient
private Integer viewed;
private Boolean launched;
#OneToMany(mappedBy = "special")
private List<CampaignSpecial> specialCampaigns;
#Override
#JsonIgnore
public ApplicationInstance getAppInstance() {
return super.getAppInstance();
}
}
All entities in Special inherits from BaseEntity which contains AppInstance
then i have a method to get the special
#GET
#Path("{ref}")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(value = MediaType.TEXT_PLAIN)
public Special findByGuestRef(#PathParam("ref") String pRefeference) {
// find the special and return it
return special;
}
On the special entity I tried the following
Added jsonIgnoreProperties
Added an override for appInstance to annotate with #JsonIgnore
#JsonIdentityInfo
links for the above
https://stackoverflow.com/a/29632358/4712391
Jackson serialization: how to ignore superclass properties
jackson self reference leading to cycle
none of those solutions works. Am I doing something wrong?
Note: Would it also just be possible to edit special, since the other entities are in a different package and would not like to edit them.
Usually excluding attributes in a response is as easy as adding a #JsonIgnore annotation to their getters, but if you don't want to add this annotation to a parent class, you could override the getter and then add the annotation on it:
public class Special extends BaseEntity implements Serializable {
...
#JsonIgnore
public ApplicationInstance getAppInstance() {
return this.appInstance;
}
...
}
NOTE: As there are several frameworks, make sure that you are using the correct #JsonIgnore annotation or it will be ignored, see this answer for instance.
Another option, more "manual", is just creating a bean for the response which would be a subset of the Special instance:
#GET
#Path("{ref}")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(value = MediaType.TEXT_PLAIN)
public SpecialDTO findByGuestRef(#PathParam("ref") String pRefeference) {
// find the special and return it
return new SpecialDTO(special);
}
public class SpecialDTO {
//declare here only the attributes that you want in your response
public SpecialDTO(Special sp) {
this.attr=sp.attr; // populate the needed attributes
}
}
To me, problem seems to be in the Special object and the fields being initialized in it.
I guess that there is a circular reference detected when serialisation happens.
Something similar to:
class A {
public A child;
public A parent;
}
A object = new A();
A root = new A();
root.child = object;
object.parent = root;
In the above code, whenever you will try to seralize either of these objects, you will face the same problem.
Note that public fields are not recommended.
I'll suggest to peek into your Special object and the references set in it.