Duplicate entry instead of using existing entity - mysql

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.

Related

Spring boot & JSON: How to add foreign key when using POST request

I can't seem to figure out how to add an entity that has a foreign key, though JSON.
I have a user model, and a post model. A user can make different posts on a website.
This is a many-to-one relationship. A user can have several posts, while a post can only have one user (the poster). The post as a foreign key representing the id of the user that made the post.
This is the User model:
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
#Builder
#Table(name = "user")
public class User {
//ID
#Id
#SequenceGenerator(
name = "user_sequence",
sequenceName = "user_sequence",
allocationSize = 1
)
#GeneratedValue(
strategy = GenerationType.IDENTITY,
generator = "user_generator"
)
#Column(name = "id",nullable = false)
private int id;
private String username;
private String password;
private String email;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd")
#Column(name = "creation_date")
private Date creationDate;
//RELATIONSHIP
#OneToMany(cascade = CascadeType.ALL, mappedBy = "user")
private List<Post> posts = new ArrayList<>();
/* =========== GETTERS AND SETTERS ===========*/
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
public List<Post> getPosts() {
return posts;
}
public void setPosts(List<Post> posts) {
this.posts = posts;
}
}
This is the Post model:
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
#Builder
#Table(name = "post")
public class Post {
//ID
#Id
#SequenceGenerator(
name = "post_sequence",
sequenceName = "post_sequence",
allocationSize = 1
)
#GeneratedValue(
strategy = GenerationType.IDENTITY,
generator = "post_generator"
)
#Column(name = "id", nullable = false)
private int id;
#Column(name = "post_content")
private String postContent;
private String title;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd")
#Column(name = "creation_date")
private Date creationDate;
//RELATIONSHIP
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "user_id", referencedColumnName = "id")
private User user;
/* ======== GETTERS AND SETTERS ======== */
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getPostContent() {
return postContent;
}
public void setPostContent(String postContent) {
this.postContent = postContent;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
This is the postController:
#RestController
public class PostController {
#Autowired
private PostService postService;
#PostMapping("/savePost")
public Post getPost(#Validated #RequestBody Post post) {
return postService.savePost(post);
}
#GetMapping("getPost/{id}")
public Post getPost(#PathVariable int id) {
return postService.getPost(id);
}
#PutMapping("/deletePost/{id}")
public void deletePost(int id) {
postService.deletePost(id);
}
}
This is the JSON I send in to add a post. Request to: http://localhost:8080/savePost JSON body:
{
"postContent": "some content",
"creationDate": "2022-07-31",
"title": "my title",
"user": 1
}
But in postMan i get this error:
{
"timestamp": "2022-08-02T10:40:11.794+00:00",
"status": 400,
"error": "Bad Request",
"path": "/savePost"
}
And in spring i get this error: JSON parse error: Cannot construct instance of x.model.User (although at least one Creator exists): no int/Int-argument constructor/factory method to deserialize from Number value (1);
If i send in a JSON where I call the user for "user_id" or "uderId", then Im able to send the request, but then the foreign key turns into null
{
"creationDate": "2022-07-31",
"postContent": "some content",
"title": "my title",
"user_id": 1
}
what gets sent in:
{
"id": 2,
"postContent": "some content",
"title": "my title",
"creationDate": "2022-07-31",
"user": null
}
Does anyone know what im doing wrong?
Firstly, your APIs are not correct in terms of REST concept.
Here is a nice explanation.
You better should rework it to deal with Post entities:
add a userId param to the controller's value and remove it from models.
Use different classes for transport and business processes. It'll give you at least two prefenecies: first one is ability to pass any extra data through model objects, or wider, control which properties can be passed to be inserted/updated (as well as possibility of a separated validation for post/put operations), and the second one is guarantee that you won't face an Open session in View problem.
#RestController("/user/{userId}/post")
public class PostController {
#Autowired
private PostService postService;
#PostMapping("/save")
public PostResponseDTO addPost(#Validated #RequestBody PostAddDTO postModel, #PathVariable Long userId) {
return postService.savePost(userId, postModel);
}
#PutMapping("/{id}")
public PostResponseDTO updatePost(#Validated #RequestBody PostUpdateDTO postModel, #PathVariable Long userId, #PathVariable Long id) {
return postService.updatePost(userId, postModel);
}
#GetMapping("/{id}")
public PostResponseDTO getPost(#PathVariable Long userId, #PathVariable Long id) {
return postService.getPost(userId, id);
}
#DeleteMapping("/{id}")
public void deletePost(Long userId, Long id) {
postService.deletePost(userId, id);
}
}
You must:
add parameter fetch = FetchType.LAZY to both of #OneToMany and #ManyToOne declarations;
add a userId property to the PostUpdateDTO (if changing of post's owner is allowed)
At the service layer you can:
if POST: find a User by userId, validate weither exists it and probably raise some exception if doesn't or create and persist a new Post entity:
#Transactional
public PostResponseDTO addPost(PostAddDTO postModel, Long userId) {
User user = getValidUser(userId);
Post post = new Post(postModel);
UserDTO userDTO = new UserDTO(user); // here copy only simple properties, not the list of user's posts
post.setUser(user);
postRepository.save(post);
PostAddDTO result = new PostAddDTO(post);
result.setUser(userDTO);
return result;
}
/**
- will be used in both post and put operations
- #param userId user id from a controller
- #return {#link User} entity if found
- #throws RuntimeException if the entity has not been found
*/
private User getValidUser(Long userId) {
Optional<User> userOpt = userRepository.findById(userId);
if (!userOpt.isPresent()) {
log.WARN("addPost. user with id={userId} not found!", userId);
throw RuntimeException("some exception"); //!!!do not place any business info such as "user with id={userId} not found" because of a scam risc reasons
}
return userOpt.get();
}
if PUT: find a Post entity by userId and id, validate weither exists it or not and implement preferable logic. I don't know is it allowed to reassign a user? If so, check new user's existance first.
If DELETE, you may raise an exceptions in case of entities absence, but many not and just do nothing to success response be sent
One more reason why we do use transport objects. If you let it be as it is, it'll lead to an infinite loop while serialization: post.user -> (post: user.posts) {post.user -> ...}.
Of course all of this stuff is not the only way of solving of this problem and it doesn't answer all the questions about Java Persistance API, but this is the way that I went for some time ago within a concrete project.
Here is a REST guide made by a Spring team

Many-To-One : JSON parse error Cannot construct instance

I devleloping a REST API using spring boot
I have two entities with bidirectional OneToMany
Product class
public class Product {
private Long productId;
private String name;
private String description;
private List<ProductList> productList;
public Product() {
}
public Product(Long productId, String name, String description, List<ProductList> productList) {
this.productId = productId;
this.name = name;
this.description = description;
this.productList = productList;
}
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public List<ProductList> getProductList() {
return productList;
}
public void setProductList(List<ProductList> productList) {
this.productList = productList;
}
ProductList class
public class ProductList {
private Long productListId;
private String productListName;
private Product product;
public ProductList(Long productListId, String productListName, Product product) {
this.productListId = productListId;
this.productListName = productListName;
this.product = product;
}
public ProductList() {
}
public Long getProductListId() {
return productListId;
}
public void setProductListId(Long productListId) {
this.productListId = productListId;
}
public String getProductListName() {
return productListName;
}
public void setProductListName(String productListName) {
this.productListName = productListName;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
}
ProductEntity class:
#Entity
#Data
#Table(name = "PRODUCT")
public class ProductEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column
private Integer productId;
#Column
private String name;
#Column
private String description;
#OneToMany(cascade = CascadeType.ALL, mappedBy="product", fetch =
FetchType.EAGER)
private List<ProductListEntity> productList;
// Getters,Setters,No-ArgCOnstructor, All-ArgConstructor
ProductListEntity class:
#Table
#Entity
#Data
public class ProductListEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long productListId;
private String productListName;
#ManyToOne(fetch = FetchType.EAGER, targetEntity = ProductEntity.class)
#JoinColumn(name = "FK_Product", referencedColumnName = "productId")
private ProductEntity product;
Service to save data:
public void addProduct(Product product) {
ProductEntity productEntity = new ProductEntity();
BeanUtils.copyProperties(product, productEntity);
productRepository.save(productEntity);
}
When i try to post I get this error:
"message": "JSON parse error: Cannot construct instance of
eteosf.hexagonal.domain.model.Product (although at least one Creator
exists): no String-argument constructor/factory method to deserialize
from String value ('string'); nested exception is
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot
construct instance of eteosf.hexagonal.domain.model.Product
(although at least one Creator exists): no String-argument
constructor/factory method to deserialize from String value
('string')\n at [Source: (PushbackInputStream); line: 9, column: 18]
(through reference chain:
eteosf.hexagonal.domain.model.Product["productList"]->java.util.ArrayList[0]->eteosf.hexagonal.domain.model.Product$ProductList["product"])",
"path": "/product"
JSON request body:
{
"productId": 0,
"name": "string",
"description": "string",
"productList": [
{
"productListId": 0,
"productListName": "string",
"product": "string"
}
]
}
in your json_request, inside of productList you are sending "product" as a "string". But Deserializer can not turn that string into a Product object. It has to be sent as object {}. You can leave that object empty - just not send it if all it does is point at itself.
You basically have made a mistake of confusing different principles - the whole bidirectional relationship is only to be applied at persistence level. Json requests are being sent at controller/view level and therefore you can't display the bidirectional nature in the same way. This is why you don't use Entities as controller params but use DTOs.
In your case just don't send the "product" field for the controller:
{
"productId": 0,
"name": "string",
"description": "string",
"productList": [
{
"productListId": 0,
"productListName": "string",
}
]
}
and just add it in the controller method right after receiving the parameter:
//the receiving controller method which got Parameter `ProductEntity product`
product.getProductList().forEach(productList -> productList.setProduct(product);
Like I said you shouldn't use entities in Controller method, and it should be a DTO class in order to avoid exactly this kind of issues

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.

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));
}

Spring Data, MySQL and not working unique value

I'm having troubles getting into Spring Data
I got entity Product which has Category (I'm guessing relation type is right? Product has one Category, Category has many products)
#Entity
public class Product implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "category_id")
private Category category;
}
#Entity
class Category implements Serializable {
public Category() {
}
public Category(String name){
this.name = name;
}
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "category_id")
private Long id;
#Column(unique = true)
private String name;
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Set<Product> products;
}
Now I try to add new Product via Postman, calling my RestController
#PostMapping("/add")
public Product addProduct(#Valid #RequestBody Product product){
return repository.save(product);
}
With 2 following requests
{
"name" : "pork",
"category" : "meat"
}
{
"name" : "chicken",
"category" : "meat"
}
In the result I got 2 following responses
{
"id": 1,
"name": "pork",
"category": {
"id": 1,
"name": "meat",
"products": null
}
}
{
"id": 2,
"name": "chicken",
"category": {
"id": 2,
"name": "meat",
"products": null
}
}
And on database I actually got 2 categories named "meat" (even tho it should be unique. What's more, do I actually need Set<Product> in my Category class? TBH, Category has no intrest in that at all.
There are a few problems with your code.
You are directly using entity as the rest API model. Suggest to create a separate ProductModel with only fields that client has access to.
You mixing category creation together inside product creation, but your category in the request only contains name. To the backend, unless you check whether such a category exists, it's always treated as a new category.
Before you call repository.save, you need let category knows what's the product inside. In your current code, only product know its category.
You don't need Set products in your Category class (and it's recommended to use only #ManyToOne).