I'm using Spring Boot 2, Spring Data REST, Spring HATEOAS.
Let's say I've a model:
#EntityListeners({ContactListener.class})
#Data
#EqualsAndHashCode(callSuper = true)
#NoArgsConstructor
#AllArgsConstructor
#Builder
public class Contact extends AbstractEntity {
#NotNull
#Enumerated(EnumType.STRING)
#Column(nullable = false, columnDefinition = "VARCHAR(30) DEFAULT 'CUSTOMER'")
private ContactType type = ContactType.CUSTOMER;
#NotNull
#Enumerated(EnumType.STRING)
#Column(nullable = false, columnDefinition = "VARCHAR(30) DEFAULT 'NATURAL_PERSON'")
private PersonType personType = PersonType.NATURAL_PERSON;
private String firstName;
private String lastName;
private String companyName;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "store_id", updatable = false)
private Store store;
and Store:
#Data
#EqualsAndHashCode(callSuper = true)
#NoArgsConstructor
#AllArgsConstructor
#Builder
public class Store extends AbstractEntity {
#NotBlank
#Column(nullable = false)
private String name;
#Username
#NotBlank
#Length(max = 16)
#Column(nullable = false/*, unique = true*/)
#ColumnTransformer(write = "UPPER(?)")
private String code;
private String address;
private String zipCode;
private String city;
private String district;
When I get a contact the response looks like this:
{
"sid": "962732c2-68a8-413b-9762-f676d42046b4",
"createdBy": "1ccf2329-4aa3-4d55-8878-25517edf1522",
"createdDate": "2019-05-28T14:06:07.011Z",
"lastModifiedDate": "2019-06-04T08:46:02.591Z",
"lastModifiedBy": "system",
"createdByName": "Rossi Mario",
"lastModifiedByName": null,
"type": "CUSTOMER",
"personType": "NATURAL_PERSON",
"firstName": "Mario",
"lastName": "Rossi",
"companyName": null,
"fullName": "Rossi Mario",
"gender": "MALE",
"birthDate": "2019-05-21T00:00:00Z",
"birthCity": null,
"job": null,
"billingAddress": "Via 123",
"billingZipCode": "14018",
"billingCity": "Roatto",
"billingDistrict": "AT",
"billingCountry": "IT",
"shippingAddress": "Via 123",
"shippingZipCode": "14018",
"shippingCity": "Roatto",
"shippingDistrict": "AT",
"shippingCountry": "IT",
"taxCode": "XXXX",
"vatNumber": null,
"landlinePhone": null,
"mobilePhone": null,
"fax": null,
"email": "aaa#sdfg.it",
"certifiedEmail": null,
"survey": null,
"iban": null,
"swift": null,
"publicAdministration": false,
"sdiAccountId": "0000000",
"preset": false,
"_links": {
"self": {
"href": "http://localhost:8082/api/v1/contacts/1"
},
"contact": {
"href": "http://localhost:8082/api/v1/contacts/1{?projection}",
"templated": true
},
"store": {
"href": "http://localhost:8082/api/v1/contacts/1/store{?projection}",
"templated": true
}
}
}
as you can see the link of store it's not the self link of the resource Store.
I'd like to override that link setting the self resource. So I created this processor:
#Component
public class DocumentRowProcessor implements ResourceProcessor<Resource<Contact>> {
#Autowired
private BasePathAwareLinks service;
#Autowired
private EntityLinks entityLinks;
#Override
public Resource<Contact> process(Resource<Contact> resource) {
Store store = resource.getContent().getStore();
if(store != null){
resource.add(entityLinks.linkToSingleResource(store.getClass(), store.getId()).withRel("store"));
}
return resource;
}
}
Unfortunately, the link is now overriden but I find 2 links inside "store". Debugging I saw that inside the resource is present just the self link. My guess is that related links are added in following steps.
How can I accomplish my goal in a clean way?
The hateoas links are added the the result during serialization (using a specific JSON serializer), so you cannot remove it using a ResourceProcessor.
The hateoas link in the result is the proper link for that resource. http://localhost:8082/api/v1/contacts/1/store is the endpoint where you can check which store is linked to this contant, or you can delete/modify the association between this two object.
However in certain use-cases you need the self-link for further actions and you don't want to send an extra request from the client.
Do the following:
1. Create a projection for the contant.
2. Include all the properties you need and also the store.
3. If you don't need any properties of the store here - only the self link - then create an 'empty projection' for the store entoty and include that projection as store property into the contact property.
When you get this projection of the contact then the result will contain the self-link of the store inside the store property. So the main _links collection will be still a regular hateos link-collection but there will be a store._links.self.href property which will contain the self link of the associated store.
Related
How can I tackle a #Embedded class inside of a Model class?
I'm trying to use a DTO to filter and check the requests being sent to the POST mapping.
I want to check, let's say, if the fullName is not blank.
(request body below)
DentistModel.java
public class DentistModel {
#Id
#GeneratedValue(generator = "UUID")
#GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
#Column(name = "dentist_id", updatable = false, nullable = false)
#Type(type = "org.hibernate.type.UUIDCharType")
private UUID id;
#Column(nullable = false, unique = true, length = 10)
private String croNumber;
#Embedded
private Person person;
}
DentistDto.java
public class DentistDto {
#NotBlank
private String croNumber;
}
DentistController.java
#PostMapping
public ResponseEntity<Object> saveDentist(#RequestBody #Valid DentistDto dentistDto) {
if (dentistService.existsByCroNumber(dentistDto.getCroNumber())) {
return ResponseEntity.status(HttpStatus.CONFLICT).body("Conflict: CRO number is already in use.");
}
var dentistModel = new DentistModel();
BeanUtils.copyProperties(dentistDto, dentistModel);
return ResponseEntity.status(HttpStatus.CREATED).body(dentistService.save(dentistModel));
}
Dentist request body
{
"croNumber":"12345",
"person": {
"fullName": "John",
"birthDate": "2000-01-27",
"cpfNumber": "11408247910",
"telephoneNumber": "47996034002",
"emailAddress": "john#gmail.com",
"address": {
"mainAddress": "Rua Amoroso Costa",
"numberAddress": "171",
"neighborHood": "Jardim das Américas",
"complementInfo": "Casa",
"zipCode": "81530-150"
}
}
}
Problem summary
I am getting unrelated data from the Many-to-Many With a New Entity concept for the first two cases.
Fetching a particular company and associated users of the company.
Fetching particular user and associated company.
Not able to feth company and user whose company status is not equal to 3 (CompanyStatus=1 "Pending" ,CompanyStatus=2 "Other",CompanyStatus=1 "Active")
To work on this Many-to-Many With a New Entity concept I have used Company, User, and CompanyUserMapping as an entity.
The blog that I have referred https://www.baeldung.com/jpa-many-to-many section 4.2
As mentioned above I have created Company, User, and CompanyUserMapping as below
Company
#Getter
#Setter
#Entity
public class Company {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long companyId;
private String companyName;
private String companyDescription;
private String companyWebsite;
private String companyEmailDomain;
private Integer companyTypeID;
private Integer numberOfEmployeesID;
private String companyLogo;
private Integer companyStatus;
private Boolean active;
#OneToMany(mappedBy = "company",fetch = FetchType.EAGER)
Set<CompanyUserMapping> companyUserMapping;
}
User
#Getter
#Setter
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
private String firstName;
private String lastName;
private String email;
private String encryptedEmail;
private String username;
private String password;
private String userStatus;
private String guid;
private Boolean isNotlocked;
private Date lastLogin;
private String profilePic;
#OneToMany(mappedBy = "user",fetch = FetchType.EAGER)
Set<CompanyUserMapping> companyUserMapping;
}
CompanyUserMapping
#Getter
#Setter
#Entity
public class CompanyUserMapping {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long companyUserMappingId;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "company_id")
private Company company;
#ManyToOne(cascade = CascadeType.ALL,fetch = FetchType.EAGER)
#JoinColumn(name = "user_id")
private User user;
private String role;
private String [] authorities;
private boolean isExternal;
private boolean isActive;
private Long customerId;
}
Then I inserted two companies and a user in each of them. Then I have also inserted a common user for both of the companies.
{
"companyId": 1,
"companyName": "ABC company",
"companyUsers": [
{
"companyUserMappingId": 1,
"company": 1,
"user": {
"userId": 1,
"email": "sachintendulkar#gmail.com",
"companyUserMapping": [
1
]
},
"active": true,
"external": false
},
{
"companyUserMappingId": 3,
"company": 1,
"user": {
"userId": 3,
"email": "shanewarne#gmail.com",
"companyUserMapping": [
3,
{
"companyUserMappingId": 4,
"company": {
"companyId": 2,
"companyName": "XYZ company",
"companyUsers": [
{
"companyUserMappingId": 2,
"company": 2,
"user": {
"userId": 2,
"email": "sehwag#gmail.com",
"companyUserMapping": [
2
]
},
"active": true,
"external": false
},
4
]
},
"user": 3,
"active": true,
"external": false
}
]
},
"active": true,
"external": false
}
]
}
In the above JSON, ABC and XYZ are two companies and a common user for both of the companies is shanewarne#gmail.com
As mentioned in the summary If I try to pull a particular company(ABC company) and associated users, it also brings another company and users which is unrelated.
To resolve this problem I have Implemented like below
public CompanyDto getConsolidatedCompanyData(Long companyId) {
Company existingCompany = companyRepository.findById(companyId).orElseThrow(() -> new ResourceNotFoundException("Company not found for the companyId :: "+companyId));
CompanyDto companyDto=new CompanyDto();
companyDto.setCompanyId(existingCompany.getCompanyId());
companyDto.setCompanyName(existingCompany.getCompanyName());
Set<CompanyUserMappingDto> companyUserMappingDtoList=new LinkedHashSet<>();
Set<CompanyUserMapping> companyUserMappingList=existingCompany.getCompanyUserMapping();
for (CompanyUserMapping cum : companyUserMappingList) {
CompanyUserMappingDto companyUserMappingDto=new CompanyUserMappingDto();
companyUserMappingDto.setCompanyUserMappingId(cum.getCompanyUserMappingId());
CompanyUserDto cdto=new CompanyUserDto();
cdto.setUserId(cum.getUser().getUserId());
cdto.setEmail(cum.getUser().getEmail());
companyUserMappingDto.setUser(cdto);
companyUserMappingDto.setActive(cum.isActive());
companyUserMappingDto.setExternal(cum.isExternal());
companyUserMappingDtoList.add(companyUserMappingDto);
}
companyDto.setAssociatedUsers(companyUserMappingDtoList);
return companyDto;
}
In the same way, if I try to fetch a particular user and associated company I am getting related data so I have implemented code
private UserDto getConslidatedUserData(Long userId) {
User existingUser = userRepository.findById(userId).orElseThrow(() -> new ResourceNotFoundException("User not found for the userId :: "+userId));
Set<UserCompanyMappingDto> associatedCompanies=new HashSet<>();
UserDto userDto=new UserDto();
userDto.setUserId(existingUser.getUserId());
userDto.setEmail(existingUser.getEmail());
Set<CompanyUserMapping> companyUserMapping=existingUser.getCompanyUserMapping();
for (CompanyUserMapping companyUserMapping2 : companyUserMapping) {
UserCompanyMappingDto userCompanyMappingDto=new UserCompanyMappingDto();
userCompanyMappingDto.setCompanyUserMappingId(companyUserMapping2.getCompanyUserMappingId());
UserCompanyDto userCompanyDto=new UserCompanyDto();
userCompanyDto.setCompanyId(companyUserMapping2.getCompany().getCompanyId());
userCompanyDto.setCompanyName(companyUserMapping2.getCompany().getCompanyName());
userCompanyMappingDto.setCompany(userCompanyDto);
associatedCompanies.add(userCompanyMappingDto);
}
userDto.setAssociatedCompanies(associatedCompanies);
return userDto;
}
Do I need to improvise Implementation for the first two cases?
I'm using Jackson to serialise My Rest Api
POJO :
#Data
#Entity
#NoArgsConstructor
#AllArgsConstructor
#JsonTypeInfo(include= JsonTypeInfo.As.WRAPPER_OBJECT,use= JsonTypeInfo.Id.NAME)
public class Project implements Serializable {
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String projectName;
private String resource;
#ManyToMany
private List<Collaborator> collaborators;
#JsonIgnore
#OneToMany(mappedBy = "project",cascade = CascadeType.ALL)
private List<Task> tasks;
public Project(String projectName, String resource) {
this.projectName = projectName;
this.resource = resource;
}
}
output:
{
"id": 1,
"dateDebut": "2022-05-31T13:14:39.091+00:00",
"dateFin": "2022-05-31T13:14:39.091+00:00",
"project": {
"Project": {
"id": 2,
"projectName": "project Suivi Activite 2",
"resource": "resource 2",
"collaborators": []
}
},
"collaborator": null,
"days": []
}
how can i avoid field "Project" inside "project" ?
#JsonTypeInfo(include= JsonTypeInfo.As.WRAPPER_OBJECT,use= JsonTypeInfo.Id.NAME) ?
I have the following in my code:
CompanyEntity
#Entity
#Table(name = "company")
public class Company{
#OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
#JsonUnwrapped
private Set<User> users;
}
UserEntity
#Entity
#Table(name="user")
public class User{
#ManyToOne(cascade = CascadeType.REFRESH)
#JoinColumn(name="company_id")
private Company company;
}
CompanyController
#GetMapping("/company")
public ResponseEntity<Object> getAllCompanies(){
List<Company> allCompanies = companyService.findAll();
return ResponseEntity.ok(allCompanies);
}
problem is when i call /company in the browser i am getting the users object including the company object. something like this
[
{
"id": 1,
"name": "company",
"users": [
{
"id": 14,
"firstName": "Yamen",
"lastName": "Nassif",
"company": {
"id": 1,
"name": "company",
"users": [
{
"id": 14,
"firstName": "Yamen",
"lastName": "Nassif",
"company": {
"id": 1,
"name": "company",
"users": [
...
same goes when i getAllUsers companies and users are also exanding.
my database looks just fine.
and its endless and of course Stackoverflow error is in the console. How can i fix this ?
You have this error because of the infinite recursion.
Company has a link on User and User has a link on Company.
You have at least two options:
use #JsonManagedReference and #JsonBackReference annotation on the relation fields.
create a pair of DTOs and fill them manually with data from you entities.
e.g.
#GetMapping("/company")
public ResponseEntity<Object> getAllCompanies() {
List<Company> allCompanies = companyService.findAll();
List<CompanyDto> allCompanyDtoList = convertToCompanyDtoList(allCompanies);
return ResponseEntity.ok(allCompanyDtoList );
}
Personally, I'd prefer the second option, since returning Entities is NOT a good practice.
You can use #JsonIgnore annotation to prevent this type of behavior. This usually happens with bidirectional mapping within your entities. It is caused by infinite recursion.
#Entity
#Table(name="user")
public class User{
#ManyToOne(cascade = CascadeType.REFRESH)
#JoinColumn(name="company_id")
#JsonIgnore
private Company company;
}
I have a bidirectional relationship between two entities. Here are the two classes, getters/setters omitted:
#Entity
#Table(name = "strategy")
public class Strategy {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String name;
#OneToMany(mappedBy = "strategy", cascade = { CascadeType.ALL })
private List<StrategyDetail> details;
}
And
public class StrategyDetail {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String content;
#ManyToOne()
private Strategy strategy;
}
I then have a repository and controller over this, but nothing worth mentionning (my repository only extends the JpaRepository and the controller has a get/post mapping).
When I try to create a strategy, I send this JSON:
{
"name": "my strat",
"details": [{
"content": "my content"
}]
}
and the response is this:
{
"id": 1,
"name": "my strat",
"details": [
{
"id": 2,
"content": "my content",
"strategy": null
}
]
}
I don't get why the strategy is null here, the mapping is fairly obvious. When I then try to get the strategy, the answer is this:
[
{
"id": 1,
"name": "my strat",
"details": []
}
]
Which I guess is working as intended (foreign key failure so the detail has not been saved?).
What should I change to make it work (meaning having access to the strategy inside the strategyDetail)? Even better I think would be having the strategy id rather than the whole strategy.
I've tried removing the bidirectionality by removing the strategy property in StrategyDetail and then it does properly save the details, but then I obviously don't have access to the strategy field inside the detail...
I've been looking for a solution on this for the past hour, looking at tutorials and what not, but I have not found a working solution.