Spring CrudRepository save excluding child entity in JSON? - json

I have a SpringBoot app and am using the CrudRepository to persist objects to the DB. I have an Product entity class which has a many to one relationship with a Vendor entity. I'm passing JSON that includes the details of both Product and the embedded vendor, but I'd ultimately like to pass just the Product details and include the vendorID. Is there some annotation that can resolve this for me?
Here is my code:
#Entity
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#ManyToOne
#JoinColumn(name="VendorID")
private Vendor vendor;
#Column(name="partnumber")
#JsonProperty("PartNumber")
private String vendorPartNumber;
#JsonProperty("Name")
private String name;
#Entity
public class Vendor {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#JsonProperty("Code")
private String code;
#JsonProperty("Name")
private String name;
....
#OneToMany(mappedBy = "vendor", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JsonIgnore
private List<Product> products;
And the JSON I'm passing (which works) looks like this:
{
"Vendor": {
"Id":1,
"Code": "BB-1001",
"Name": "Best Buy",
"Address": "100 Best Buy Street",
"City": "Louisville",
"State": "KY",
"Zip": "40207",
"Phone": "502-111-9099",
"Email": "geeksquad#bestbuy.com",
"IsPreApproved": "false"
},
"PartNumber":"TEST01",
"Name":"Test Product 01",
"Price":99.99
}
I'd ultimately like to remove the JSON object reference to vendor and replace with VendorId.

Solution by annotation
You can use #JsonSerialize to provide a custom serialization to achieve what you want.
#Entity
public class Product {
...
#JsonSerialize(using = VendorToIdSerializer.class)
#ManyToOne
#JoinColumn(name="VendorID")
private Vendor vendor;
...
}
public class VendorToIdSerializer extends JsonSerializer<Vendor> {
#Override
public void serialize(Vendor vendor, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
String vendorIdAsString = String.valueOf(vendor.getId());
jsonGenerator.writeObject(vendorIdAsString);
}
}
Solution by DTO
However, as others have pointed out, the cleaner approach would be to create a DTO class which serves the role of the "view" of Product.
public class ProductView {
private final int vendor;
...
public ProductView(Vendor vendor) {
this.vendor = vendor.getId();
...
}
// getters
}

My issue was answered by using the #JsonBackReference and #JsonManagedReference annotations. I'm going to continue to research the DTOs as suggested above though. Thank-you!

I would also suggest to create a DTO that fit your needs.
With JPA you can create DTO instances using the constructor expression:
Here an example from a great article written by Vlad Mihalcea
https://vladmihalcea.com/the-best-way-to-map-a-projection-query-to-a-dto-with-jpa-and-hibernate/
List<PostDTO> postDTOs = entityManager.createQuery(
"select new " +
" com.vladmihalcea.book.hpjp.hibernate.query.dto.projection.jpa.PostDTO(" +
" p.id, " +
" p.title " +
" ) " +
"from Post p " +
"where p.createdOn > :fromTimestamp", PostDTO.class)
.setParameter( "fromTimestamp", Timestamp.from(LocalDateTime.of( 2016, 1, 1, 0, 0, 0 )
.toInstant( ZoneOffset.UTC ) ))
.getResultList();

Related

Ignore Parent Object to be sent in the JSON response - Cyclic Problem - JPA

Please be patient with me. tried my best to explain with sample easy code.
Two Entities - Shop and Product.
Relationship - A Shop can have many Product.
I return a Shop object, it keeps printing like this -
{
"shopId": 1,
"shopName": "S1",
"productList": [
{
"productId": 100,
"productName": "MOBILE",
"shop": {
"shopId": 1,
"shopName": "S1",
"productList": [
{
"productId": 100,
"productName": "MOBILE",
"shop": {
Before i start with the actual issue, i did solve the Cyclic issue partially but arrived at a new problem. I stopped it with the help of #JsonIgnore
Basically when i print my parent(Shop) json object i stopped the cyclic response by using #JsonIgnore in child (Product) class field.
#JsonIgnore
private Shop shop
So, now
API 1 =
#GetMapping("/getShopById")
public Shop getShopById(){
return shopRepo.findById(1L).get();
}
GIVES ME OUTPUT - (Which is perfect as i avoid printing Shop back);
{
"shopId": 1,
"shopName": "S1",
"productList": [
{
"productId": 100,
"productName": "MOBILE"
},
{
"productId": 101,
"productName": "EARPHONE"
}
]
}
But now anytime i want to fetch the Shop from a Product object and send the response i get an error, which is because of the #JsonIgnore i guess, which basically is completely stopping the serialization of the field from Product object.
API 2 =
#GetMapping("/getShopFromTheProductId")
public Shop getShopFromProductId() {
Shop s = productRepo.findById(100L).get().getShop();
return s;
}
GIVES ME ERROR -
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.doubt.StackOverFlow.Shop$HibernateProxy$YEW0qvzw["hibernateLazyInitializer"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77) ~[jackson-databind-2.12.3.jar:2.12.3]
at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1276) ~[jackson-databind-2.12.3.jar:2.12.3]
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400) ~[jackson-databind-2.12.3.jar:2.12.3]
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.failForEmpty
So to summarize how can i ignore printing/getting the Parent back from Child until and unless i require it explicitly ?
PROBABLE SOLUTION 1 - remove the getter for Shop (private Shop getShop()) from Product entity . But this is not a solution for me as i will never be able to track back to the parent when i may need it in business logic.
MY classes -
Controller -
#RestController
public class MainController {
#Autowired
private ShopRepo shopRepo;
#Autowired
private ProductRepo productRepo;
#GetMapping("/getShopById")
public Shop getShopById(){
return shopRepo.findById(1L).get();
}
#GetMapping("/getShopFromTheProductId")
public Shop getShopFromProductId() {
Shop s = productRepo.findById(100L).get().getShop();
return s;
}
}
Shop Entity -
#Entity
#Table(name = "SHOP")
public class Shop {
#Id
#Column(name = "SHOP_ID")
private Long shopId;
#Column(name = "SHOP_NAME")
private String shopName;
#OneToMany(fetch = FetchType.LAZY,targetEntity = Product.class, mappedBy = "shop")
private List<Product> productList;
........
all the getters and setters
Product Entity -
#Entity
#Table(name = "PRODUCT")
public class Product {
#Id
#Column(name = "PRODUCT_ID")
private Long productId;
#Column(name = "PRODUCT_NAME")
private String productName;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "SHOP_ID")
#JsonIgnore
private Shop shop;
........
all getters and setters
To avoid the cyclic problem Use #JsonManagedReference, #JsonBackReference as below.
Add #JsonManagedReference on Parent class, shop entity.
#JsonManagedReference
#OneToMany(fetch = FetchType.LAZY,targetEntity =
Product.class, mappedBy = "shop")
private List<Product> productList;
Add #JsonBackReference on child class as below.
#JsonBackReference
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "SHOP_ID")
#JsonIgnore
private Shop shop;
So i have come to terms with this problem.
Firstly this is wrong sending a domain object directly as a response. Not at all the best practice.
Best practice is to have a RequestShopDTO Object and Similarly and ResponseShopDTO. We should have DTO with getters and setters same as the domain object, in this case Shop.
Rest API should receive RequestShopDTO object.
Use a factory class/ adapter classes to convert the RequestShopDTO to a Shop Domain object and forward it to the Business layer.
Similarly we should convert the Response Shop domain object to ResponseShopDTO object and send it as response.
we should have like BaseRequest class, extended by something like CreateRequest, UpdateRequest, GetRequest etc... where properties common to all get requests are in GetRequest which is then extended by more specific request classes such as RequestShopDTO.
Similarly we can have a abstract Adapter class like this RequestDtoToDomainBaseAdapter that gets extended by something like
ShopDtoToShopDomainAdapter.
reference - inor's answer - Design Pattern to model Request and Response Objects for Webservices
P.S. - DTO - Data Transfer Object

How to post ManyToOne entity using JSON format

I've two entities issue and user I would like to assign the user to an issue but when trying to do that using this JSON the user_id in the issues table is NULL.
#Data
#Entity
#Table(name = "issues")
public class Issue {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column
private String number;
#Column
private String title;
#Column
private String description;
#Column
#Enumerated(EnumType.STRING)
private State state;
#JsonIgnore
#CreationTimestamp
#Column
private Timestamp createDate;
#JsonIgnore
#UpdateTimestamp
#Column
private Timestamp modifyDate;
#ManyToOne(targetEntity = User.class)
#JoinColumn
private User user;
public Issue() {
}
#Data
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column
private String fullName;
#Column
private String username;
#Column
private String email;
#Column
private String password;
#Column
#Enumerated(EnumType.STRING)
private Role role;
#JsonIgnore
#CreationTimestamp
#Column
private Timestamp createDate;
#JsonIgnore
#UpdateTimestamp
#Column
private Timestamp modifyDate;
public User() {
}
public User(String fullName, String username, String email, String password, Role role) {
this.fullName = fullName;
this.username = username;
this.email = email;
this.password = password;
this.role = role;
}
Firstly I've created an user without any issues, but do not know how to manage with the issue here is the JSON I am using via postman.
{
"number": "3",
"title": "Create an working service",
"description": "The problem is that we do not have an working service.",
"state": "NEW",
"user_id": "1"
}
This part is responsible for saving the issue.
public void save(Issue issue) {
if (issue == null)
return;
Issue actual = issueRepository.findByNumber(issue.getNumber());
if (actual != null) {
actual.setNumber(issue.getNumber());
actual.setTitle(issue.getTitle());
actual.setDescription(issue.getDescription());
actual.setState(issue.getState());
actual.setUser(issue.getUser());
issueRepository.save(actual);
} else {
issueRepository.save(issue);
}
}
In the controller I just have #Valid #RequestBody Issue issue and the service saves the issue.
What you're sending as JSON is not an Issue.
First because it does not represent a database-persistent issue (and that's what the Issue class is for), but rather the JSON structure the public API expects from its clients.
Second because an Issue doesn't have any field named user_id, but your JSON does. So use a different class than Issue, which actually matches with the JSON structure that the API expects, and thus has a user_id property.
Then use this user_id to find the User by its ID using the UserRepository, and set that User into the Issue you're creating.
I would rename user_id to userId to respect the Java conventions, too.
You need to post json like this.
{
"number": "3",
"title": "Create an working service",
"description": "The problem is that we do not have an working service.",
"state": "NEW",
"user":{
"id":1
}
}
Here issue have join column of user so with user entity you have to pass userid.
Your save would be like this.
public void save(Issue issue) {
if (issue == null)
return;
Issue actual = issueRepository.findByNumber(issue.getNumber());
if (actual != null) {
actual.setNumber(issue.getNumber());
actual.setTitle(issue.getTitle());
actual.setDescription(issue.getDescription());
actual.setState(issue.getState());
actual.setUser(userRepositoy.findById(issue.getUser().getId()));
issueRepository.save(actual);
} else {
issueRepository.save(issue);
}
}
user_id key was not presented in your Item class.
As per entity mapping,
Please change your JSON to this.
{
"number": "3",
"title": "Create an working service", "description": "The problem is that we do not have an working service.",
"state": "NEW",
"user":{
"fullName":"USER",
"email":"user#gmail.com",
"username":"user"
}
}

Spring sends empty JSON despite of object being not null

In my controller I have the following method:
#RequestMapping(value = "/getAll", method = RequestMethod.GET)
public List<Topic> getAllTopics() {
List<Topic> allTopics = service.getAllTopics();
assert allTopics.size() > 0; // is not empty
System.out.println(allTopics.get(0)); // Topic{id=1, name='bla', description='blahhh'}
return allTopics;
}
When I go to http://localhost:8080/getAll I get [{},{},{},{}] as a result but service.getAllTopics() returns non empty List So the list to be send is not empty but the browser receives invalid JSON. However, there is no problem in serializing objects since the following method return valid JSON. What's the problem?
#GetMapping("/json")
public List<Locale> getLocales() {
return Arrays.asList(DateFormat.getAvailableLocales());
}
I'm running latest Spring Boot, i.e. 2.1.3.RELEASE.
Update
Here's my entity class - Topic
#Entity
#Table(name = "topic", schema="tetra")
public class Topic {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private String description;
public Topic() {
}
public Topic(String name, String description) {
this.name = name;
this.description = description;
}
#Override
public String toString() {
return "Topic{" +
"id=" + id +
", name='" + name + '\'' +
", description='" + description + '\'' +
'}';
}
}
By default , Jackson will only serialise the public fields and public getters into JSON. As the Topic neither have public fields nor the public getter , nothing will be serialised and you get an empty JSON object.
There are plenty of ways to configure it such as:
(1) Simply add public getter for all fields
(2) Use #JsonAutoDetect(fieldVisibility = Visibility.ANY) such that private fields can also be auto detected :
#Entity
#Table(name = "topic", schema="tetra")
#JsonAutoDetect(fieldVisibility = Visibility.ANY)
public class Topic {
}
(3) Use #JsonProperty to explicitly pick what fields/getter to be serialised .The nice things of this approach is that the field name in JSON can be different from the POJO :
#Entity
#Table(name = "topic", schema="tetra")
public class Topic {
#JsonProperty("id")
private Integer id;
#JsonProperty("name")
private String name;
#JsonProperty("description")
private String description;
}

Foreign key is always null in one to many relation - Spring Boot Data with JPA

I have two entity classes Country and Language having bi-directional one to many relationship.
Below are the entity classes:
#Entity
#Table(name = "COUNTRY")
public class Country {
#Id
#GeneratedValue
#Column(name = "COUNTRY_ID")
private Long id;
#Column(name = "COUNTRY_NAME")
private String name;
#Column(name = "COUNTRY_CODE")
private String code;
#JacksonXmlElementWrapper(localName = "languages")
#JacksonXmlProperty(localName = "languages")
#OneToMany(mappedBy = "country", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
List<Language> languages;
// getters and setters
}
And...
#Entity
#Table(name = "LANGUAGE")
public class Language {
#Id
#GeneratedValue
#Column(name = "LANGUAGE_ID")
private Long id;
#Column(name = "LANGUAGE_NAME")
private String name;
#ManyToOne
#JoinColumn(name = "COUNTRY_ID")
#JsonIgnore
private Country country;
//getters and setters
}
Below is my Rest controller:
#RestController
#RequestMapping("/countries")
public class CountryRestController {
private final ICountryRepository iCountryRepository;
#Autowired
public CountryRestController(ICountryRepository iCountryRepository) {
this.iCountryRepository = iCountryRepository;
}
#PostMapping("/country")
public ResponseEntity<?> postCountryDetails(#RequestBody Country country) {
Country savedCountry = this.iCountryRepository.save(country);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
.buildAndExpand(savedCountry.getId()).toUri();
return ResponseEntity.created(location).build();
}
//other methods
}
I'm trying to save below JSON:
{
"name": "Ireland",
"code": "IRE",
"languages": [
{
"name": "Irish"
}
]
}
The problem is that the language (child) foreign key is always null but other properties are getting inserted. I have used #JsonIgnore on property Country country of Language class because it was causing issues with request size as I have another API fetching data of Country along with its Languages.
Please guide.
You can do it in this way :
Country newCountry = new Country(country.getName());
ArrayList < Language > langList = new ArrayList<>();
for (Language lang : country.getLanguages()) {
langList.add( new Language(language.getName(), newCountry ) ) ;
}
newCountry.setLanguages( langList );
iCountryRepository.save(newCountry);
PS : Don't forget to add appropriate constructors.
Also it is mandatory to add a default constructor if you are doing constructor overloading like this :
public Country() {}
public Country(String name) {this.name = name }
You can do it in this way also.
Here it doesn't create new objects. In the same object which is parsing it creates the relationship in language objects.
#PostMapping("/country")
public Country postCountryDetails(#RequestBody Country country) {
if( country.getLanguages().size() > 0 )
{
country.getLanguages().stream().forEach( countryItem -> {
countryItem.setCountry( country );
} );
}
return country;
}
Update the setter for languages in Country class to the below :
public void setLanguages(List<Language> languages) {
this.languages = languages;
languages.forEach(entity -> entity.setCountry(this));
}

Duplicate entry instead of using existing entity

I'm getting MySQLIntegrityConstraintViolationException while trying to add new Product. Each Product has Category which has unique value name. I'm getting this exception when I try to add new Product with already existing Category. Example below:
POST 1
{
"name" : "apple",
"categoryName" : "fruit"
}
Response
{
"name": "apple",
"categoryName": "fruit",
"kcal": null
}
Post 2:
{
"name" : "banana",
"categoryName" : "fruit"
}
Response:
{
"timestamp": 1533451793052,
"status": 500,
"error": "Internal Server Error",
"exception": "org.springframework.dao.DataIntegrityViolationException",
"message": "could not execute statement; SQL [n/a]; constraint [UK_8f25rdca1qev4kqtyrxwsx0k8]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement",
"path": "/product/add"
}
Which is obviously not what I'd expect, instead I want banana to use same category as apple.
Ok, the code, first entities and dto's
#Entity
#Table(name = "tbl_product")
public class Product implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double kcal;
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn
private Category category;
}
public class ProductDto {
private String name;
private String categoryName;
private Double kcal;
}
#Entity
#Table(name = "tbl_category")
public class Category implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(unique = true)
private String name;
public Category(){ }
public Category(String name){
this.name = name;
}
}
Post from controller
#PostMapping("/add")
public ProductDto addProduct(#Valid #RequestBody ProductDto productDto){
Product product = productRepository.save(dtoToEntityTranslator.translate(productDto));
return entityToDtoTranslator.translate(product);
}
And pretty straight-forward translators
public class DtoToEntityTranslator {
public Product translate(ProductDto productDto){
Product product = new Product();
product.setName(productDto.getName());
product.setCategory(new Category(productDto.getCategoryName()));
product.setKcal(productDto.getKcal());
return product;
}
}
public class EntityToDtoTranslator {
public ProductDto translate(Product product){
ProductDto productDto = new ProductDto();
productDto.setName(product.getName());
if(product.getCategory() != null) {
productDto.setCategoryName(product.getCategory().getName());
}
productDto.setKcal(product.getKcal());
return productDto;
}
}
Not sure if it's worth mention, my repository for Product
#Repository
public interface ProductRepository extends CrudRepository<Product, Long> {
}
The error is caused by this line
product.setCategory(new Category(productDto.getCategoryName()));
You are tolding Hibernate that this is a new Category because category name is not the Id for Category.
To solve this, you can get the Category with the provided and set to the Category.
Another way is that for existing Category, instead of sending the name to server, you can consider sending the category id.