i have a class named "BaseEntity". other entities extended from Base Entity.
Base Entity
#MappedSuperclass
public abstract class BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
private Long id;
#ManyToOne
#JsonIgnoreProperties({"createdBy", "lastModifiedBy", "manager"})
private User createdBy;
private Instant createdDate = Instant.now();
#ManyToOne
#JsonIgnoreProperties({"createdBy", "lastModifiedBy", "manager"})
private User lastModifiedBy;
private Instant lastModifiedDate = Instant.now();
// getters & setters
}
User entity extended from Base Entity.
#Entity
public class User2 extends BaseEntity {
private String userName;
#ManyToOne
#JsonIgnoreProperties({"createdBy", "lastModifiedBy", "manager"})
private User manager;
// getters & setters
}
Error
When i try to serialize user i got this error.
org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class a.b.c.d.domain.User]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle (through reference chain: java.util.Collections$UnmodifiableRandomAccessList[0]->a.b.c.d.domain.User["createdBy"])
if i change only createdBy and lastModifiedBy like this it works. manager user does not cause error
#ManyToOne
#JsonIgnore
private User createdBy;
But this time the json does not contain any createdBy or lastModifiedBy info.
{
"id": 2,
"userName": "admin",
"manager": {
"id": 1,
"username": "system"
}
}
Simply i want to see this json. The first step must contains createdBy and lastModifiedBy.
{
"id": 2,
"userName": "admin",
"manager": {
"id": 1,
"username": "system"
},
"createdBy": {
"id": 1,
"username": "system"
},
"lastModifiedBy": {
"id": 2,
"username": "admin"
}
}
The correct way to use is at the class level.These properties are considered to be ignored in JSON serialization and deserialization.
Example :-
#JsonIgnoreProperties({ "bookName", "bookCategory" })
public class Book {
#JsonProperty("bookId")
private String id;
#JsonProperty("bookName")
private String name;
#JsonProperty("bookCategory")
private String category;
}
If you want to ignore at field level you can use #JsonIgnore
I think #JsonIgnoreProperties should be placed at class level, right before you declare your class.
#JsonIgnoreProperties(ignoreUnknown = true) try using this above your class name.
When we pass true to ignoreUnknown element, then in deserialization if JSON data has a field for which there is no logical property then that JSON field will be ignored and no error will be thrown.
Something like this :
#JsonIgnoreProperties(ignoreUnknown = true)
public class Book {
#JsonProperty("bookId")
private String id;
}
Related
I have two entities: Customer and Address.
It is a bidirectional relationsship - one address can have many customers (oneToMany) and one customer can only have one address (manyToOne).
Executing GET request for customers returns:
[
{
"id": 1,
"name": "Foo",
"contact": "5512312",
"email": "Foo#gmail.com",
"address": {
"id": 1,
"street": "X",
"postalCode": 123,
"houseNo": "10",
"city": "New York"
}
}
]
When a new customer, with the exact same address properties as the one exists in DB - is being added with POST request, the json response returns the foreign key related to the existing object in DB instead of the object itself:
[
{
"id": 1,
"name": "Foo",
"contact": "5512312",
"email": "Foo#gmail.com",
"address": {
"id": 1,
"street": "X",
"postalCode": 123,
"houseNo": "10",
"city": "New York"
}
},
{
"id": 2,
"name": "Bar",
"contact": "5512312",
"email": "Bar#gmail.com",
"address": 1 <----------- it returns the foreign key instead of the object
}
]
So what I expect is that whenever a new customer, that has an address that already exists in the DB, is being added - it should return the address object instead of the foreign key from the json response.
Code:
Customer.java
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
#Entity
#Table
public class Customer {
#Id
#SequenceGenerator(
name = "customer_sequence",
sequenceName = "customer_sequence",
allocationSize = 1
)
#GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "customer_sequence"
)
private Long id;
private String name;
private String contact;
private String email;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "address_id", nullable = false)
private Address address;
[...]
Address.java
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
#Entity
#Table
public class Address {
#Id
#SequenceGenerator(
name = "address_sequence",
sequenceName = "address_sequence",
allocationSize = 1
)
#GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "address_sequence"
)
private Long id;
private String street;
private int postalCode;
private String houseNo;
private String city;
#JsonIgnore
#OneToMany(mappedBy = "address")
private Set<Customer> customers;
[...]
CustomerController.java
//...
#PostMapping
public void createCustomer(#RequestBody Customer customer) {
customerService.createCustomer(customer);
}
[...]
And the service that saves the customer to the DB which also makes sure that no address is being created if already exists in database (it checks on every property to be equal from the param):
//...
public void createCustomer(Customer customer) {
Optional<Customer> customerWithExistingAddress = customerRepository.findAll()
.stream()
.filter(x -> x.getAddress().equals(customer.getAddress()))
.findFirst();
customerWithExistingAddress.ifPresent(c -> customer.setAddress(c.getAddress()));
customerRepository.save(customer);
}
[...]
You're probably getting this behavior because of JsonIdentityInfo, so it's a serialization problem you have not a persistence one. I'm assuming you're using a relational database (Hibernate for NoSql has Jpa like annotations but then that would make this a different problem) and that data is being persisted correctly.
See the javadocs:
In practice this is done by serializing the first instance as full object and object identity, and other references to the object as reference values
I am very new to Hibernate and I am trying to solve an issue similar to this question, specifically the answer.
I have something very similar set up (in my case it's Author, Book, and the mapping table AuthorBook). I am using this in Spring Data JPA, so I have the following components:
Repositories: AuthorRepository, BookRepository, AuthorBookRepository
Services: AuthorService, BookService, AuthorBookRepository
Controllers: AuthorController, BookController, AuthorBookController
My entities are:
#Entity
public class Author {
#Id
#GeneratedValue
private Long id;
private String name;
#OneToMany(mappedBy = "author")
private Set<AuthorBook> authorBooks;
// getters and setters
}
#Entity
public class Book {
#Id
#GeneratedValue
private Long id;
private String name;
#OneToMany(mappedBy = "book")
private Set<AuthorBook> authorBooks;
// getters and setters
}
#Entity
public class AuthorBook {
#Id
#GeneratedValue
private Integer id;
#ManyToOne
#JoinColumn(name = "user_id")
private Author author;
#ManyToOne
#JoinColumn(name = "book_id")
private Book book;
#Column(name = "isMainAuthor")
private boolean isMainAuthor;
// getters and setter
}
My understanding is that I should make the following POST requests:
Create an author:
{
"name": "Test Author"
}
Create a book:
{
"name": "Test Book"
}
Create the mapping:
{
"author": {
"id": 1
},
"book": {
"id": 2
},
"isMainAuthor": true
}
First of all: is this the correct way to use this? If no, how should I use it instead? If yes, how is serialisation supposed to work? Because if I do it like this, and then fetch the list of books, the response will be infinitely long like so:
[
{
"id": 2,
"name": "Test Book",
"authorBooks": [
{
"id": 3,
"author": {
"id": 1,
"name": "Test Author",
"authorBooks": [
{
"id": 3,
"author": {
"id": 1,
"name": "Test Author",
"authorBooks": [
{
"id": 3,
"author": {
"id": 1,
"name": "Test Author",
"authorBooks": [
...
I know that I could use #JsonIgnore on the Author and Book getters in AuthorBook, and #JsonProperty on the setters so that deserialisation still works. However, as someone who is unexperienced with Hibernate, this seems like a hack to me. What is the cleanest and best way to solve this?
Also, is there a way to update the mapping via the author and book endpoints directly? I might already have an author in the database and want to just add a new book he wrote by adding a new Book entity and providing the relation as part of it, but I seem not to be able to do that.
This is absolutely not a hacker trick. With the #JsonIgnore annotation, this problem is solved. This problem is called infinite recursion with the release of Jackson JSON and nothing can be done with it. For this, they came up with an annotation.
You probably need to set an embedded ID within the mapping table. Keeping the Author and Book table as such, create a new class
#Embeddable
public class AuthorBookId{
#Column(name = "user_id")
private String authorId;
#Column(name = "book_id")
private String bookId;
// Constructors
// Getters and Setters
}
And change the AuthorBook table as
#Entity
public class AuthorBook {
#EmbeddedId private AuthorBookId id = new AuthorBookId();
#ManyToOne
#MapsId("user_id")
#JsonIgnore
private Author author;
#ManyToOne
#MapsId("book_id")
#JsonIgnore
private Book book;
#Column(name = "isMainAuthor")
private boolean isMainAuthor;
// getters and setter
}
I have following entities:
#Entity
#Table(name = "profile")
public class Profile {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#OneToOne(cascade = CascadeType.ALL)
private ProfileContacts profileContacts;
...
}
and
#Entity
#Table(name = "profile_contacts")
public class ProfileContacts {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "description")
private String description;
#Column(name = "first_name")
private String firstName;
#Column(name = "last_name")
private String lastName;
}
I am trying to update it by sending this JSON with update to REST controller:
{
"id": 1,
"description": "an update",
"profileContacts": {
"firstName": "John",
"lastName": "Doe"
}
}
so in the end it calls
profileRepository.save(profile);
where profileRepository is instance of ProfileRepository class:
public interface ProfileRepository extends JpaRepository<Profile, Long> {
}
which is spring-data-jpa interface.
But each time after such update it updates profile table but adds new row to profile_contacts table (table which corresponds to ProfileContactsentity) instead of updating existing ones.
How can I achieve updating?
As per your JSON structure. Yes it will create new profileContacts entry for every time.
The problem every time while saving profile entity you are passing "id": 1 that means Hibernate can identify the entity by this id value (primary key) but for profileContacts mapping you are not sending the id that's why Hibernate considering it has a new entity every time.
To update your profileContacts entity make sure to pass the id of it.
Example:
{
"id": 1,
"description": "an update",
"profileContacts": {
"id" : yourEntityId
"firstName": "John",
"lastName": "Doe"
}
}
Well, that's the expected behavior.
You're not telling hibernate to update the profileContacts.
For the framework to be able to update it, you need to send the profileContact's primary key - which in your case is the ProfileContacts#id.
Something like this:
{
"id": 1,
"description": "an update",
"profileContacts": {
"id": 1
"firstName": "John",
"lastName": "Doe"
}
}
Need to specify the join column in the parent Entity.
#Entity
#Table(name = "profile")
public class Profile {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#OneToOne(cascade = CascadeType.ALL)
**#JoinColumn(name = "id")** //
private ProfileContacts profileContacts;
...
}
Now when you try to save Profile entity it will save the child entity also.
And also needs to include Id in jason request for child entity also
{
"id": 1,
"description": "an update",
"profileContacts": {
"id": 1,
"firstName": "John",
"lastName": "Doe"
}
}
Ok, I see the problem. As #Matheus Cirillo pointed out, you need to tell the hibernate to update the row.
Now, how do you tell the hibernate to update a row - By providing the primary key of the existing row.
But, creating an object with the primary key set is not enough. You need that entity class to be attached to the entity manager and the persistence context.
You can have something like,
//This attaches the entity to the entity manager
ProfileContacts existingProfileContacts = profileContactRepository.getOne(2);
Profile profile = new Profile();
....
....
profile.setProfileContacts(existingProfileContacts);
profileRepository.save(profile);
I hope this helps.
I am working on validating the request body in Spring boot.
When the post controller using the below JSON to create record in the DB. It works fine.
{
"test1": "string",
"test2": "string",
"test3": "string", <--this has #Null in the entity
"test4": "string"
}
However, when one of the key is #NULL in the entity, it will still able to create a record in the DB. I am wondering if there is something that can validate the key and return error.
{
"test1": "string",
"test2": "string",
"test5": "string", <- wrong key by mistake
"test4": "string"
}
Entity class
#Entity
#Table(name = "test")
#Data
#NoArgsConstructor
public class Test implements Serializable {
#Id
#Column(name = "test1")
private String test1;
#Column(name = "test2")
#NotNull
private String test2;
#Column(name = "test3")
private String test3;
#Column(name = "test4")
private String test4;
}
You can use Jackson for parsing JSON and handle unknown properties. It will automatically throw UnrecognizedPropertyException if an unknown property is found as described here
If u want to Validate request body in JSON u can use #Valid
#PostMapping("/books")
Book newBook(#Valid #RequestBody Test test) {
return repository.save(Test);
}
#Column(name = "test3")
#NotNull(message = "Please provide a test3")
private String test3;
if u want on key order
JsonPropertyOrder({ "test1", "test2", "test3", "test4" })
public class Test implements Serializable {
}
I have an object that contains a list of other objects that I cannot seem to get to update correctly. I can create object(Product) with the list of objects(ProductItemQuantity) no problem. I can also do a PUT with the list of objects, but it creates a new list of objects everything I do a PUT. I want the list of objects I provide to be updated instead of having it create a new list every time I do a put of the parent object.
If I add an ID to the ProductItemQuantity, I get an exception:
detached entity passed to persist
Here are my classes:
Product.java
#Entity
public class Product {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#ManyToOne
private Organization org;
private String barCode;
private String name;
#ManyToOne(cascade=CascadeType.MERGE)
private Status status;
#ManyToMany(cascade=CascadeType.MERGE)
private List<Fee> fees;
#ManyToMany(cascade=CascadeType.ALL)
private List<Note> notes;
#ManyToMany(cascade=CascadeType.ALL)
private List<ProductItemQuantity> productItems;
private Integer stock;
private BigDecimal msrp;
#CreationTimestamp
private LocalDateTime createdOn;
#UpdateTimestamp
private LocalDateTime updatedOn;
// Getter & Setters
ProductItemQuantity.java
#Entity
public class ProductItemQuantity {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private Integer count;
#ManyToOne
private ProductItem productItem;
#CreationTimestamp
private LocalDateTime createdOn;
#UpdateTimestamp
private LocalDateTime updatedOn;
// Getters / setters
ProductItem.java
#Entity
public class ProductItem {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#ManyToOne
private Organization org;
#ManyToOne
private Supplier supplier;
private String barCode;
private String description;
private String name;
private Integer stock;
private Integer caseQty;
private BigDecimal caseCost;
#ManyToMany(cascade=CascadeType.ALL)
private List<Note> notes;
#CreationTimestamp
private LocalDateTime createdOn;
#UpdateTimestamp
private LocalDateTime updatedOn;
ProductController.java
#PutMapping("/{id}")
public Product update(#RequestBody Product product, #PathVariable long id) {
Product savedProduct = productService.save(product);
return savedProduct;
}
Working CRUD PUT request: http://localhost:8080/product/1
{
"barcode":"12347163",
"name":"Product 1",
"stock": 12,
"msrp": 29.99,
"org": {
"id":1
},
"status":{
"id":1
},
"productItems":[{
"count":30
},{
"count":30
}
],
"fees":[{
"id":1
},{
"id":2
}],
"notes":[{
"title":"Product Created",
"description":"Note created by user X on 12/16/2019 11:00PM"
},{
"title":"Product Updated",
"description":"Product updated stock by user X on 12/16/2019 11:00PM"
}]
}
Broken CRUD PUT request: http://localhost:8080/product/1
{
"barcode":"12347163",
"name":"Product 1",
"stock": 12,
"msrp": 29.99,
"org": {
"id":1
},
"status":{
"id":1
},
"productItems":[{
"id":1,
"count":30
},{
"id":2,
"count":30
}
],
"fees":[{
"id":1
},{
"id":2
}],
"notes":[{
"title":"Product Created",
"description":"Note created by user X on 12/16/2019 11:00PM"
},{
"title":"Product Updated",
"description":"Product updated stock by user X on 12/16/2019 11:00PM"
}]
}
Your objects are detached because the relations (OneToMany, ManyToMany) is set only from one direction. In order to persist them, you must set the relationship bidirectional. Your relation is unidirectional, because the parser (jackson) will make the following objects:
Product product = new Product();
Fee fee = new Fee();
fee.setId(1);
product.setFees(Arrays.asList(fee));
In a bidirectional relationship, both sides must be set :
product.getFees().forEach(fee-> fee.getProducts().add(product));
It is a good practice to keep separated the persistence object from the controller object, because the persistence object deals with relationships as well.
From my experience, if you want to use GeneratedValue, you have first to select the entity from the database and after to modify it. Does not make much sense to create a new object and set the id on it, if you expect hibernate to generate it.
So, you may need to do a select first :
List fees = // select all fees with id in the product fee list
and after:
product.setFees(fees);
fees.forEach(fee -> fee.getProducts().add(product));
Your method is a PUT, so you should not save directly the product object (it will create a new entry in the database).
#PutMapping("/{id}")
public Product update(#RequestBody Product product, #PathVariable long id) {
Optional<Product> originalProductOptional = productRepository.findById(id);
// you should add a check here : if originalProduct is not found, return 404
Product originalProduct = originalProductOptional.get();
originalProduct.setName(product.getName());
// here update all fields and relations
productRepository.save(originalProduct);
return originalProduct;
}
The answer was to update the property to this:
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
private List<ProductItemQuantity> productItemQuantities;
And then set it manually in the put
List<ProductItemQuantity> piqs = product.getProductItemQuantities();
if (piqs != null) {
List<ProductItemQuantity> piiList = new ArrayList<ProductItemQuantity>();
for (ProductItemQuantity pii : piqs) {
// Check for ID in here?
ProductItemQuantity curPII = piqService.getOne(pii.getId());
curPII.setCount(pii.getCount());
piiList.add(curPII);
}
originalProduct.setProductItemQuantities(piiList);
}