Spring HATEOAS. Embedded resource + ability to CRUD it? - json

In typical situation with embedded resources:
#Entity #Data
class Item {
id, name
...
#ManyToOne
#JoinColumn(name="status", referencedColumnName="ID")
private Status status;
}
#Entity #Data
class Status {
id, name
...
#JsonIgnore //break infinite reference loop during serialization
#OneToMany(mappedBy="status")
private List<Item> items;
}
Instead of having links to Status id's in Item JSON, I want to INCLUDE Status object in Item JSON
{
"itemName": "abc",
... ,
"status": {
"statusName":"ACTIVE",
...
}
"_links": {
...
}
}
I managed embedding doing any of the following:
Marking Item class status property as #RestResource(exported=false)
#Entity #Data
class Item {
...
#RestResource(exported=false) // <-- HERE
#ManyToOne
#JoinColumn(name="status", referencedColumnName="ID")
private Status status;
Marking Status repo interface #RepositoryRestResource(..., exported=false)
#RepositoryRestResource(collectionResourceRel="statuses", path="status", exported=false)
public interface StatusRepository extends JpaRepository<Status, String>
Deleteting repository for Status entity
// DELETED
#RepositoryRestResource
public interface StatusRepository extends JpaRepository<Status, String>{}
QUESTION:
Any of that embeds Status into Item JSON like I want, but I do not have an access to Status Repository anymore to get a Status object by it's ID or do any CRUD on it.
How to embed status in parent Item JSON and still CRUD status via url?

I implemented it as follows:
#Entity #Data
class Item {
#Id
#GeneratedValue(generator = "uuid2")
#GenericGenerator(name = "uuid2", strategy = "uuid2")
String id;
String name
#RestResource(exported = false) // <---- ADDED
#ManyToOne
#JoinColumn(name="status", referencedColumnName="ID")
private Status status;
}
#Entity #Data
class Status {
#Id
#GeneratedValue(generator = "uuid2")
#GenericGenerator(name = "uuid2", strategy = "uuid2")
String id
String name
// <-------- DELETED REFERENCE TO PARENT OBJECT
//#OneToMany(mappedBy="status")
//private List<Item> items;
}
Repositories for both Entities exist so it is possible to do CRUD on Status entity as well
#RepositoryRestResource(collectionResourceRel="items", path="items")
public interface StatusRepository extends JpaRepository<Item, String>
{}
#RepositoryRestResource(collectionResourceRel="statuses", path="statuses")
public interface StatusRepository extends JpaRepository<Status, String>
{}
For future generations I'll leave it here.
For convenience:
1) Exposed ids for entities in JSON
/**
* Exposing Ids as properties for entities specified
*/
#Configuration
public class ExposeIdConfig extends RepositoryRestMvcConfiguration {
#Override
protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.exposeIdsFor(Item.class, Status.class);
}
}
So getting list of items looks like:
{
"_links": {
"self": {
"href": "http://localhost:8080/app/items{?page,size,sort}",
"templated": true
}
},
"_embedded": {
"as": [
{
"id": "29117425-f011-4ff9-8952-38b05d3df7f0",
"name": "item 1",
"status": {
"id": "e9192ae7-29f8-4d5e-ad62-cad8d87de9e2",
"name": "ACTIVE"
},
"_links": {
"self": {
"href": "http://localhost:8080/app/items/29117425-f011-4ff9-8952-38b05d3df7f0"
}
},
...
}

Related

JSON don't repeat some attributes and respective values

for all instances serialized, from the second occurrence onwards, of the same model class, the objects only have a part of the attributes and their respective values ​​that they should. This way the JSON file structure is not homogeneous and uniform as expected by the old part of the application (Frontend).
[
{
"id": 1,
"fornecedor": {
"cnpj": "80000876000177",
"nome": "ATACADÃO DIA-A-DIA"
},
"itens": [
{
"id": 2,
"produto": {
"gtin": "7891991010856",
"nome": "CERVEJA BUDWEISER"
},
"quantidade": 3,
"preco": 3.5,
"total": 10.5
}
]
},
{
"id": 3,
"fornecedor": {
"cnpj": "19600347000101",
"nome": "SUPERMERCADO BELLA-VIA"
},
"itens": [
{
"id": 4,
"produto": {
"gtin": "7891991010857",
"nome": "CERVEJA HEINEKEN"
},
"quantidade": 4,
"preco": 5.4,
"total": 21.6
}
]
},
{
"id": 5,
"fornecedor": "19600347000101",
"itens": [
{
"id": 6,
"produto": "7891991010856",
"quantidade": 4,
"preco": 3.2,
"total": 12.8
}
]
},
{
"id": 7,
"fornecedor": "80000876000177",
"itens": [
{
"id": 8,
"produto": "7891991010857",
"quantidade": 5,
"preco": 4.9,
"total": 24.5
}
]
}
]
In the JSON above, the instances with id:1 and id:7 have a field named fornecedor that is structurally different and that shouldn't be because the Frontend expects them to be the same. Other samples are id:2 and id:6 with the field produto and so on.
The JSON above is the serialization of the list of model class Pedido below:
#Data
#Entity
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Pedido {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView(View.External.class)
private Long id;
#ManyToOne
#JoinColumn(name = "fornecedor_cnpj")
#JsonView(View.External.class)
private Fornecedor fornecedor;
#OneToMany(mappedBy = "pedido", fetch = FetchType.LAZY,
cascade = CascadeType.ALL)
#JsonView(View.External.class)
private List<Item> itens;
public List<Item> getItens() {
if(itens == null){
itens = new ArrayList<>();
}
return itens;
}
}
The model class Fornecedor is:
#Data
#Entity
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "cnpj")
public class Fornecedor {
#Id
#JsonView(View.External.class)
private String cnpj;
#JsonView(View.External.class)
private String nome;
#OneToMany(mappedBy = "fornecedor", fetch = FetchType.LAZY,
cascade = CascadeType.ALL)
#JsonView(View.Internal.class)
private List<Pedido> pedidos;
#OneToMany(mappedBy = "fornecedor", fetch = FetchType.LAZY,
cascade = CascadeType.ALL)
#JsonView(View.Internal.class)
private List<Disponibilidade> disponilidades;
}
And the model class Produto is:
#Entity
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "gtin")
#NoArgsConstructor
#Data
public class Produto {
#Id
#JsonView(View.External.class)
private String gtin;
#JsonView(View.External.class)
private String nome;
#OneToMany(mappedBy = "produto", fetch = FetchType.LAZY,
cascade = CascadeType.ALL)
#JsonView(View.Internal.class)
private List<Disponibilidade> disponibilidades;
public Produto(String gtin) {
this.gtin = gtin;
}
}
Question: The structure of JSON is the representation of expected behavior? If so, could someone tell me how do I make all instances from the first one to always have all attributes with their respective values ​​(as much as that means repeated attributes and values!).
Thanks!
The JSON you are getting is a consequence of the usage of #JsonIdentityInfo that you or someone else may have added to solve an infinite recursion while serializing your Java objects to JSON (you can read more about this at https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion). This means that the only way to have the complete set of attributes in fornecedor and produto is to get rid of #JsonIdentityInfo on those classes. Keep in mind that this might create the infinite recursion problem mentioned above.
#Data
#Entity
public class Fornecedor {
(...)
}
#Entity
#NoArgsConstructor
#Data
public class Produto {
(...)
}

How to specify the endoint of a REST API accepting a list of types using a supertype?

I intend to specify a REST endpoint in a Spring Boot application which accepts a list of objects (List<? extends SuperObject>). SuperObject is an abstract base class. The lists sent within the RequestBody of an HttpRequest keeps instances of SuperObject's sub-/childclasses (SubType1OfSuperObject and SubType2OfSuperObject).
Currently I tried this:
#PostMapping(path = "store", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(HttpStatus.OK)
#ResponseBody
public String store(#RequestBody List<? extends SuperObject> oject, #RequestParam Technology technology) {
theService.saveObjectsByTechnology(objects, technology);
return "Perfect!";
}
I tried to send a list of subtype objects using Postman:
{
"subtype1OfSuperObjects": [
{
"prop1": 4,
"prop2": "foo",
"prop3": "xxx"
},
{
"prop1": 7,
"prop2": "Bar",
"prop3": "zzz"
}
]
}
The result is
{
"timestamp": "2021-08-18T12:24:55.797+00:00",
"status": 400,
"error": "Bad Request",
"path": "/admin/error/store"
}
The SuperObject class:
#Data
#SuperBuilder
#NoArgsConstructor
#Validated
public abstract class SuperObject {
private Integer prop1;
private String prop2;
}
The SubType1OfSuperObject class:
#Data
#SuperBuilder
#NoArgsConstructor
#Validated
public class SubType1OfSuperObject extends SuperObject {
private String prop3;
}
The SubType1OfSuperObject class:
#Data
#SuperBuilder
#NoArgsConstructor
#Validated
public class SubType2OfSuperObject extends SuperObject {
private String prop4;
}
How can I achieve this. The classes of the supertype and the subtypes are implemented, of course.
The REST API is accessible as I can invoke another (GET) endpoint (to list all subtypes, for instance).
If I understand it well you have a list of data to send? if that's the case you can have something like below:
In your controller
#PostMapping("/")
#ResponseBody
public String save(#RequestBody List<Data> data){
System.out.println(data);
return "good";
}
And you define your Data pojo like this
class Data{
private List<SubData> subtype1OfSuperObjects;
//getters, setters and toString
}
class SubData{
private int prop1;
private String prop2;
// getters, setters and toString
}
Here is the result of by sysout
[Data{subtype1OfSuperObjects=[SubData{prop1=4, prop2='foo'}, SubData{prop1=7, prop2='Bar'}]}]

Spring JSON cast class property to other class dynamically

I have an (more) entity, which has a UserEntity class (creator).
Etc:
#Entity
class TestEntity {
#ManyToOne
private UserEntity creator;
}
Now, on UserEntity i have a many field which not important to use it in some request.
And i created a class (UserEntityMiniFied with just important fields) which has a Constructor with UserEntity,
Well, can i solute this question dynamiccaly with one json annotation, i mean,
i try:
#JsonView(UserEntityMinified.class)
private UserEntity creator;
but it not working.
thanks for any help.
I can say you're on the right track.
First – let's go through a simple example – serialize an object with #JsonView.
Here is our view:
public interface JSONView {
interface JSONBasicView {}
interface JSONAdvancedView extends JSONBasicView {}
}
And the UserEntity entity:
#Entity
class UserEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#JsonView(JSONView.JSONBasicView.class)
private int id;
#JsonView(JSONView.JSONBasicView.class)
private String name;
#JsonView(JSONView.JSONBasicView.class)
private String age;
#JsonView(JSONView.JSONAdvancedView.class)
private String country;
#JsonView(JSONView.JSONAdvancedView.class)
private String city;
public UserEntity() {
}
// getter/setter ..
}
And the TestEntity entity:
#Entity
class TestEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#JsonView(JSONView.JSONBasicView.class)
private int id;
#JsonView(JSONView.JSONBasicView.class)
private String name;
#ManyToOne
#JsonView(JSONView.JSONBasicView.class)
private UserEntity userEntity;
public TestEntity() {
}
// getter/setter ..
}
Now let's serialize a TestEntity instance using our view:
public static void main(String[] args) {
UserEntity userEntity = new UserEntity();
userEntity.setId(1);
userEntity.setName("User Entity Name");
userEntity.setAge("33");
userEntity.setCountry("Country");
userEntity.setCity("City");
TestEntity testEntity = new TestEntity();
testEntity.setId(1);
testEntity.setName("Test Entity Name");
testEntity.setOtherTestEntity(userEntity);
ObjectMapper mapper = new ObjectMapper();
mapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
String basicView = mapper
.writerWithView(JSONView.JSONBasicView.class)
.writeValueAsString(testEntity);
System.out.println(basicView);
String advancedView = mapper
.writerWithView(JSONView.JSONAdvancedView.class)
.writeValueAsString(testEntity);
System.out.println(advancedView);
}
The output of this code will be:
// JSONBasicView
{
"id": 1,
"name": "Test Entity Name",
"userEntity": {
"id": 1,
"name": "User Entity Name",
"age": "33"
}
}
// JSONAdvancedView
{
"id": 1,
"name": "Test Entity Name",
"userEntity": {
"id": 1,
"name": "User Entity Name",
"age": "33",
"country": "Country",
"city": "City"
}
}
You can also check here to do more.

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.

Retrieving details from multiple tables using SpringMVC

I am developing a Shopping Application, In my Application i have table tenant and in tenant table i have have column Binary_id which is primary key in binary table in database. Now when i making a get request to tenant table i am getting all the tenant table fields as JSON. But I have #ManyToOne relation from binary table to tenant i.e tenant can have multiple records in binary. So, while making GET call from POSTMAN client instead of getting tenant details, i need to get all the binary records related to that tenant as JSON.
Now i am getting JSON as follows when making a call to http://localhost:8080/sportsmvc/rest/tenant from POSTMAN Client
[
{
"id": 2,
"binaryId": "1002",
"name": "AltisArena"
},
{
"id": 9,
"binaryId": "1001",
"name": "Agon"
}
]
But i need the responce JSON As below:
[
{
"id": 2,
"name": "AltisArena",
"listOfBinary": [
{
"tenant_id": 2,
"location": "location1",
"description": "ABC"
},
{
"tenant_id": 2,
"location": "location2",
"description": "ABCD"
}
]
},
{
"id": 9,
"name": "Agon",
"listOfBinary": [
{
"tenant_id": 9,
"location": "location3",
"description": "desc1"
},
{
"tenant_id": 9,
"location": "location4",
"description": "desc2"
}
]
}
]
Code snippets:
Tenant Entity:
#Entity
#Table(name="tenant", catalog="db_sports" )
// Define named queries here
#NamedQueries ( {
#NamedQuery ( name="TenantEntity.countAll", query="SELECT COUNT(x) FROM TenantEntity x" )
} )
public class TenantEntity implements Serializable {
private static final long serialVersionUID = 1L;
//----------------------------------------------------------------------
// ENTITY PRIMARY KEY ( BASED ON A SINGLE FIELD )
//----------------------------------------------------------------------
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name="id", nullable=false)
private Integer id ;
#Column(name="name", nullable=false, length=300)
private String name ;
//----------------------------------------------------------------------
// ENTITY LINKS ( RELATIONSHIP )
//----------------------------------------------------------------------
#ManyToOne
#JoinColumn(name="binary_id", referencedColumnName="id")
private SwaBinaryEntity swaBinary ;
SWA_Binary Entity:
#Entity
#Table(name="SWA_Binary", catalog="db_sports" )
// Define named queries here
#NamedQueries ( {
#NamedQuery ( name="SwaBinaryEntity.countAll", query="SELECT COUNT(x) FROM SwaBinaryEntity x" )
} )
public class SwaBinaryEntity implements Serializable {
private static final long serialVersionUID = 1L;
//----------------------------------------------------------------------
// ENTITY PRIMARY KEY ( BASED ON A SINGLE FIELD )
//----------------------------------------------------------------------
#Id
#Column(name="id", nullable=false, length=100)
private String id ;
#Column(name="file_location", nullable=false, length=400)
private String fileLocation ;
#Column(name="description", nullable=false, length=200)
private String description ;
TenantRestController:
#RequestMapping( value="/tenant",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(HttpStatus.OK)
#ResponseBody
public List<Tenant> findAll() {
return tenantService.findAll();
}
TenantServiceImpl:
#Override
public List<Tenant> findAll() {
List<TenantEntity> entities = tenantPersistence.loadAll();
List<Tenant> beans = new ArrayList<Tenant>();
for(TenantEntity entity : entities) {
beans.add(tenantServiceMapper.mapTenantEntityToTenant(entity));
}
return beans;
}
TenantServiceMapper:
public Tenant mapTenantEntityToTenant(TenantEntity tenantEntity) {
if(tenantEntity == null) {
return null;
}
//--- Generic mapping
Tenant tenant = map(tenantEntity, Tenant.class);
//--- Link mapping ( link to SwaBinary )
if(tenantEntity.getSwaBinary() != null) {
tenant.setBinaryId(tenantEntity.getSwaBinary().getId());
}
return tenant;
}
Can anyone please help to to solve this issue.
Thanks in Advance.
Maybe I misunderstood model, but it seems like it's a bit wrong. In what you need JSON you have tenant that have multiple binaries, but in JPA model it's vise versa and tenant have 1 binary.
In TenantEntity shouldn't it be like this ?:
#OneToMany
private List<SwaBinaryEntity> swaBinary