Spring Data Rest output join column object as JSON field when querying single resource - spring

I have Apartment entity:
#Entity
public class Apartment extends AbstractEntity {
private String name;
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn(nullable = false)
#RestResource(exported = false)
private Address address;
private String website;
#OneToMany(mappedBy = "apartment")
#RestResource(exported = false)
private Set<FloorPlan> floorPlans;
...
FloorPlan entity:
#Entity
public class FloorPlan extends AbstractEntity {
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "apt_id", nullable = false)
private Apartment apartment;
private float bed;
private float bath;
private int priceFrom;
...
I applied excerptProjection to Floorplan to only show bed, bath and priceFrom. When I query apartments collection, the json output looks ok:
{
"_embedded": {
"apartments": [
{
"name": "Avalon Silicon Valley",
"website": "https://www.avaloncommunities.com/california/sunnyvale-apartments/avalon-silicon-valley",
"address": {
"streetNumber": "1257",
"street": "Lakeside Drive",
"city": "Sunnyvale",
"state": "CA",
"zipCode": "94085",
"fullAddress": "1257 Lakeside Drive, Sunnyvale, CA 94085"
},
"floorPlans": [
{
"bed": 3,
"bath": 3,
"priceFrom": 4495
},
{
"bed": 3,
"bath": 2,
"priceFrom": 4760
},
However if I do a single resource like http://localhost:8080/ag-api/apartments/1
floorplans will output Apartment Object as one of its field:
{
"name": "Avalon Silicon Valley",
"address": {
"streetNumber": "1257",
"street": "Lakeside Drive",
"city": "Sunnyvale",
"state": "CA",
"zipCode": "94085",
"fullAddress": "1257 Lakeside Drive, Sunnyvale, CA 94085"
},
"website": "https://www.avaloncommunities.com/california/sunnyvale-apartments/avalon-silicon-valley",
"floorPlans": [
{
"bed": 3,
"bath": 3,
"priceFrom": 4495,
"_embedded": {
"apartment": {
"name": "Avalon Silicon Valley",
"website": "https://www.avaloncommunities.com/california/sunnyvale-apartments/avalon-silicon-valley",
"address": {
"streetNumber": "1257",
"street": "Lakeside Drive",
"city": "Sunnyvale",
"state": "CA",
"zipCode": "94085",
"fullAddress": "1257 Lakeside Drive, Sunnyvale, CA 94085"
},
"floorPlans": [
Anyone got an idea what might be going on? really appreciate it. Thanks

Not sure if it will help, but in documentation I found in order to omit "_embedded" in you response you'll need to add:
"spring.hateoas.use-hal-as-default-json-media-type=false" to application.properties.

Related

java8 stream not showing the expected output

Iterate the list and create multiple objects.
Below is the sample json:
"accountRequest": [
{
"accountId": "10EIIP",
"custId": "11EE",
"custName": "XYZ",
"comments": null,
"status": "active",
"linkedDetails": [
{
"custCode": "001",
"startOn": "2023-01-01",
"loanType": "auto"
},
{
"custCode": "002",
"startOn": "2023-01-15",
"loanType": "home"
},
{
"custCode": "003",
"startOn": "2023-02-10",
"loanType": "home"
}
]
}
]
Account.java
class Account{
#JsonProperty("accountId")
private Long accountId;
#JsonProperty("custId")
private String custId;
//custName, comments, status
private String custCode;
private LocalDate startOn;
private String loanType;
}
I need to create list of "Account" by storing each of linkedDetails. The sample code below does not store accountId, custId, custName, comments, status in the "Account" object. Expected output is to create List of size 3..
List<Account> accountList = new ArrayList<>();
List<AccountRequest> accountRequestList = request.getAccountRequest();
accountRequestList.get(0).getLinkedDetails().forEach(linkedDetails -> {
Account account = new Account();
//need to set accountId, custId, custName, comments, status in each account object
account.setCustCode(linkedDetails.getCustCode());
account.setStartOn(linkedDetails.getStartOn());
account.setLoanType(linkedDetails.getLoanType());
accountList.add(account);
});
Expected Output :
"accountList": [
{
"accountId": "10EIIP",
"custId": "11EE",
"custName": "XYZ",
"comments": null,
"status": "active",
"custCode": "001",
"startOn": "2023-01-01",
"loanType": "auto"
},
{
"accountId": "10EIIP",
"custId": "11EE",
"custName": "XYZ",
"comments": null,
"status": "active",
"custCode": "002",
"startOn": "2023-01-15",
"loanType": "home"
},
{
"accountId": "10EIIP",
"custId": "11EE",
"custName": "XYZ",
"comments": null,
"status": "active",
"custCode": "003",
"startOn": "2023-02-10",
"loanType": "home"
}
]
Instead of creating a mutable list and modifying it in a foreach you should directly collect to the accountList:
List<AccountRequest> accountRequestList = request.getAccountRequest();
List<Account> accountList = accountRequestList.get(0).getLinkedDetails().stream().map(linkedDetails -> {
Account account = new Account();
//need to set accountId, custId, custName, comments, status in each account object
account.setCustCode(linkedDetails.getCustCode());
account.setStartOn(linkedDetails.getStartOn());
account.setLoanType(linkedDetails.getLoanType());
return account;
}).collect(Collectors.toList());

How to map query result to List<Object> in R2DBC?

I am trying to migrate my JDBC based application to R2DBC, however I struggle with List based objects. How can I map the result from query to "List" field?
I was using ResultSetExtractor, but in R2DBC there is no option like that so I try to do it manually. I use DatabaseClient with manual mapping, however this mapping does not work with List type fields.
public class Employee {
private Long id;
private String name;
private String surname;
private List\<Account\> accounts;
}
public class Account {
private String employeeId;
private String accountNumber;
}
public class EmployeeRepository {
private final DatabaseClient databaseClient;
public static final BiFunction<Row, RowMetadata, Employee> EMPLOYEE_MAPPER =
(row, rowMetadata) -> Employee.builder()
.id(row.get("id", String.class))
.name(row.get("name", String.class))
.surname(row.get("surname", String.class))
.accounts(List.of(new Account((row.get("employeeId"), (row.get("accountNumber")))))
.build();
public Flux<Employee> findCardByPan(Long id) {
return databaseClient.sql("SELECT id, name, surname, a.accountNumber FROM employee e JOIN account a on e.id = a.employeeId WHERE e.id = :id")
.bind("id", id)
.map(EMPLOYEE_MAPPER)
.all();
}
}
Current response model:
[
{
"id": "1",
"name": "Test User",
"surname": "Test User",
"accounts": [
{
"employeeId": "1",
"accountNumber": "98034"
}
]
},
{
"id": "1",
"name": "Test User",
"surname": "Test User",
"accounts": [
{
"employeeId": "1",
"accountNumber": "12456"
}
]
}
]
Expected response model:
[
{
"id": "1",
"name": "Test User",
"surname": "Test User",
"accounts": [
{
"employeeId": "1",
"accountNumber": "98034"
},
{
"employeeId": "1",
"accountNumber": "12456"
}
]
}
]

Spring boot mongodb #DBRef lazy loading not working for hashsets

Issue: Don't want to fetch the full referenced child objects just the Id.
Partner.java
#Data
#Document
#Builder
public class Partner {
#Id
private String id;
#DBRef(lazy = true) // working as expected
private User user;
#DBRef(lazy = true) // not working, still loads full objects
#Builder.Default
private HashSet<Product> products = new HashSet<>();
}
Product.java
#Data
#Document
#Builder
public class Product {
#Id
private String id;
private Name name;
}
I know that #DBRef is not recommended for such cases. But the project is already built that way and is expensive to change at the moment.
So far tried:
excluding the child array field from the query but then I do not get the Ids as well.
excluding the products.$ref from the query.
#Query(value="{ '_id' : ?0 }", fields="{ products.$ref: 0 }")
Optional<Partner> findByIdWithoutProducts(String id);
Expected:
{
"_id": {
"$oid": "634692559f31e246984436c8"
},
"user": {
"$ref": "user",
"$id": "f689bae8-7442-4355-ae9c-65d22a96ear4"
},
"products": [
{
"$id": {
"$oid": "633d92d2310687496fb852e6"
}
},
{
"$id": {
"$oid": "633d92d2310687496fb852d5"
}
}
}
}
Actual:
{
"_id": {
"$oid": "634692559f31e246984436c8"
},
"user": {
"$ref": "user",
"$id": "f689bae8-7442-4355-ae9c-65d22a96ear4"
},
"products": [
{
"$id": {
"$oid": "633d92d2310687496fb852e6"
},
"name": "abcd" // not wanted
},
{
"$id": {
"$oid": "633d92d2310687496fb852d5"
},
"name": "xyz" // not wanted
}
}
}
My goal is to fetch the parent and just list of Ids of the child objects. Any other way is also appreciated.

How to get consolidated Data in many to many with extra entity approach

Below are the Entities I have created to achieve Many to Many with an extra entity concept.
Company :
#Getter
#Setter
#Entity
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "companyId")
public class Company {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long companyId;
private String companyName;
#OneToMany(mappedBy = "company", fetch = FetchType.EAGER)
Set<CompanyUserMapping> companyUsers;
}
User :
#Getter
#Setter
#Entity
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "userId")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
private String email;
private String originalCompanyName;
#OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
Set<CompanyUserMapping> companyUserMapping;
}
CompanyUserMapping
#Getter
#Setter
#Entity
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class CompanyUserMapping {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name = "company_id")
private Company company;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name = "user_id")
private User user;
private boolean isExternal;
private boolean isActive;
}
Below is the way I am adding company with the user.
{
"company": {
"companyName": "XYZ company"
},
"user": {
"email": "sehwag#gmail.com",
"originalCompanyName": "XYZ company"
},
"active": true,
"external": false
}
And also trying to add the user to the existing company using below snippet.
Optional<User> optionalUser=userRepository.findByEmail(user.getEmail());
Optional<Company> optionalCompany = companyRepository.findById(companyId);
if(optionalUser.isPresent()) {
User existingUser=optionalUser.get();
Optional<CompanyUserMapping> optionalCompanyUserMapping=companyUserMappingRepository.findByCompanyAndUser(optionalCompany.get(),existingUser);
if(optionalCompanyUserMapping.isPresent()) {
throw new UserExistsForCompanyException("User exists for this company "+companyId);
}
CompanyUserMapping companyUserMapping =new CompanyUserMapping();
companyUserMapping.setCompany(optionalCompany.get());
companyUserMapping.setUser(existingUser);
companyUserMapping.setActive(true);
companyUserMapping.setExternal(false);
CompanyUserMapping savedcompanyUserMapping = companyUserMappingRepository.save(companyUserMapping);
return new ResponseEntity<CompanyUserMapping>(savedcompanyUserMapping, HttpStatus.CREATED);
}
CompanyUserMapping companyUserMapping =new CompanyUserMapping();
companyUserMapping.setCompany(optionalCompany.get());
companyUserMapping.setUser(user);
companyUserMapping.setActive(true);
companyUserMapping.setExternal(false);
CompanyUserMapping savedcompanyUserMapping = companyUserMappingRepository.save(companyUserMapping);
Here If I try to get Company details by Id, there is repeated information and also unwanted company information which is associated to few common users.The output Json is as below
{
"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
}
]
}
How do I get the consolidated data like below.
{
"companyId": 1,
"companyName": "ABC company",
"companyUsers": [
{
"companyUserMappingId": 1,
"user": {
"userId": 1,
"email": "sachintendulkar#gmail.com"
},
"active": true,
"external": false
},
{
"companyUserMappingId": 3,
"user": {
"userId": 3,
"email": "shanewarne#gmail.com"
},
"active": true,
"external": false
}
]
}
And then if I call get User by UserId, the consolidated data should be like below.
{
"userId": 1,
"email": "sachintendulkar#gmail.com",
"associatedCompany": [
{
"companyId": 1,
"companyName": "ABC company"
}
]
}

Show only object id in OneToMany List

I have these two classes:
Quota Class
#Entity
#Data
#EqualsAndHashCode(callSuper=false)
#Table(name="quotas")
#Relation(collectionRelation = "quotas", itemRelation = "quota")
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Quota extends RepresentationModel<Quota> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "quota")
#JsonIdentityReference(alwaysAsId = true)
private List<Customer> customers;
private String type;
private boolean flatrate;
#CreatedDate
private long createdDate;
#LastModifiedDate
private long lastModifiedDate;
And Customer:
#Entity
#Data
#EqualsAndHashCode(callSuper=false)
#Table(name = "customers")
#Relation(collectionRelation = "customers", itemRelation = "customer")
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Customer extends RepresentationModel<Customer> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private String customerNumber;
#CreatedDate
private long createdDate;
#LastModifiedDate
private long lastModifiedDate;
#ManyToOne
#JoinColumn(name = "quota_id", referencedColumnName = "id")
#JsonProperty("quotaId")
#JsonIdentityReference(alwaysAsId = true)
private Quota quota;
}
What i get when i make a GET request on all Customers:
{
"_embedded": {
"customers": [
{
"id": 3,
"name": "Customer Test3",
"customerNumber": "45678",
"createdDate": 1596117132045,
"lastModifiedDate": 1596117132045,
"_links": {
"self": {
"href": "http://localhost:2502/seminars/3"
}
},
"quotaId": 1
},
{
"id": 1,
"name": "test3",
"customerNumber": "12345",
"createdDate": 1596111304535,
"lastModifiedDate": 1596186450456,
"_links": {
"self": {
"href": "http://localhost:2502/seminars/1"
}
},
"quotaId": 1
},
{
"id": 2,
"name": "Customer Test2",
"customerNumber": "23456",
"createdDate": 1596112131934,
"lastModifiedDate": 1596112131934,
"_links": {
"self": {
"href": "http://localhost:2502/seminars/2"
}
},
"quotaId": 2
},
{
"id": 4,
"name": "Customer Test4",
"customerNumber": "34567",
"createdDate": 1596117145795,
"lastModifiedDate": 1596117145795,
"_links": {
"self": {
"href": "http://localhost:2502/seminars/4"
}
},
"quotaId": 2
},
{
"id": 6,
"name": "Customer Test6",
"customerNumber": "12345",
"createdDate": 1596187250598,
"lastModifiedDate": 1596187250598,
"_links": {
"self": {
"href": "http://localhost:2502/seminars/6"
}
},
"quotaId": null
}
]
},
"_links": {
"self": {
"href": "http://localhost:2502/seminars?page=0&size=20&sort=quota,asc"
}
},
"page": {
"size": 20,
"totalElements": 5,
"totalPages": 1,
"number": 0
}
}
What i get when i make a GET request in all Quotas:
{
"_embedded": {
"quotas": [
{
"id": 1,
"customers": [
{
"id": 3,
"name": "Customer Test3",
"customerNumber": "45678",
"createdDate": 1596117132045,
"lastModifiedDate": 1596117132045,
"quotaId": 1
},
{
"id": 1,
"name": "test3",
"customerNumber": "12345",
"createdDate": 1596111304535,
"lastModifiedDate": 1596186450456,
"quotaId": 1
}
],
"type": "paid",
"createdDate": 1596111304535,
"lastModifiedDate": 1596111304535,
"flatrate": true,
"_links": {
"self": {
"href": "http://localhost:2502/quotas/1"
}
}
},
{
"id": 2,
"customers": [
{
"id": 2,
"name": "Customer Test2",
"customerNumber": "23456",
"createdDate": 1596112131934,
"lastModifiedDate": 1596112131934,
"quotaId": 2
},
{
"id": 4,
"name": "Customer Test4",
"customerNumber": "34567",
"createdDate": 1596117145795,
"lastModifiedDate": 1596117145795,
"quotaId": 2
}
],
"type": "demo",
"createdDate": 1596111304535,
"lastModifiedDate": 1596111304535,
"flatrate": true,
"_links": {
"self": {
"href": "http://localhost:2502/quotas/2"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:2502/quotas?page=0&size=10&sort=id,asc"
}
},
"page": {
"size": 10,
"totalElements": 2,
"totalPages": 1,
"number": 0
}
}
I am happy with the result from Customers, but the result from quotas i would want to look like this:
{
"_embedded": {
"quotas": [
{
"id": 1,
"customers": [3,1],
"type": "paid",
"createdDate": 1596111304535,
"lastModifiedDate": 1596111304535,
"flatrate": true,
"_links": {
"self": {
"href": "http://localhost:2502/quotas/1"
}
}
},
{
"id": 2,
"customers": [2,4],
"type": "demo",
"createdDate": 1596111304535,
"lastModifiedDate": 1596111304535,
"flatrate": true,
"_links": {
"self": {
"href": "http://localhost:2502/quotas/2"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:2502/quotas?page=0&size=10&sort=id,asc"
}
},
"page": {
"size": 10,
"totalElements": 2,
"totalPages": 1,
"number": 0
}
}
For json serialization i use 'com.fasterxml.jackson.annotation.*'
I already tried it with using the #JsonBackRefference, #JsonIgnore, #JsonIngoreProperties and #JsonIdentityReference annotations but never got the wanted result.
I would recommend you introduce DTOs to model this rather than trying to annotate the entity classes with JSON annotations. At some point you will probably have conflicting needs i.e. one use case needs a field and another doesn't and then you will have to think about abstracting this anyway.
Having said that, this is a perfect use case for Blaze-Persistence Entity Views.
Blaze-Persistence is a query builder on top of JPA which supports many of the advanced DBMS features on top of the JPA model. I created Entity Views on top of it to allow easy mapping between JPA models and custom interface defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure the way you like and map attributes(getters) via JPQL expressions to the entity model. Since the attribute name is used as default mapping, you mostly don't need explicit mappings as 80% of the use cases is to have DTOs that are a subset of the entity model.
A DTO mapping for your model could look as simple as the following
#EntityView(Quota.class)
public abstract class QuotaDto extends RepresentationModel<QuotaDto> {
public abstract Integer getId();
#Mapping("customers.id")
public abstract List<Integer> getCustomers();
public abstract String getType();
public abstract boolean isFlatrate();
public abstract long getCreatedDate();
public abstract long getLastModifiedDate();
}
#EntityView(Customer.class)
public abstract class CustomerDto extends RepresentationModel<CustomerDto> {
public abstract Integer getId();
public abstract String getName();
public abstract String getCustomerNumber();
public abstract long getCreatedDate();
public abstract long getLastModifiedDate();
#Mapping("quota.id")
public abstract Integer getQuotaId();
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
CustomerDto dto = entityViewManager.find(entityManager, CustomerDto.class, id);
But 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
You are in control of the structure and can map anything to the properties. It will only fetch the mappings that you tell it to fetch.

Resources