Spring - POSTing JSON body with nested entity does not work - java

I'm new to spring and java, so apologises if this is obvious.
TLDR
When I send JSON with nested resources, it creates the subresource. EVEN when it already exists, causing a persistence issue, how do you stop this in Spring?
I have two entities, Book and Shelf. A shelf can have multiple books but a book can only be on one shelf. So Shelf (1) <-> (*) Book.
#Entity
public class Book {
#Id
#GenericGenerator(name = "uuid-gen", strategy = "uuid2")
#GeneratedValue(generator = "uuid-gen")
#Type(type = "pg-uuid")
private UUID id;
#Column(nullable = false)
private String name;
private String description;
#ManyToOne(fetch=FetchType.EAGER)
#JoinColumn(foreignKey = #ForeignKey(name = "shelf_id"))
private Shelf shelf;
public Book() {
}
public Book(UUID id, String name, String description, Shelf shelf) {
this.id = id;
this.name = name;
this.description = description;
this.shelf = shelf;
}
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setShelf(Shelf shelf) {
this.shelf = shelf;
}
}
Shelf
#Entity
public class Shelf {
#Id
#GenericGenerator(name = "uuid-gen", strategy = "uuid2")
#GeneratedValue(generator = "uuid-gen")
#Type(type = "pg-uuid")
private UUID id;
#Column(nullable = false)
private String name;
private String description;
#OneToMany(fetch=FetchType.LAZY, mappedBy = "shelf", cascade = CascadeType.ALL)
private Set<Book> books;
public Dashboard() {
}
public Dashboard(UUID id, String name, String description, Set<Book> books) {
this.id = id;
this.name = name;
this.description = description;
this.book = book;
}
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setBooks() {
for (Book book : books)
book.setShelf(this);
}
public Set<Book> getBooks() {
return books;
}
}
I have a ShelfRepository which extends JpaRepository.
When I make a request with the body
{"name":"ShelfName", "books": [{"name": "bookName"}]}
it will return create the resource Book and Shelf but does not link them as Book is created first without Shelf to reference. So calling setBooks on the Shelf is needed. Not ideal but I cant figure out another way.
Creating a book and using the id as the reference in the books array (which is what I would like in my API) like below:
{"name":"otherShelfName", "books": [{"id": "7d9c81c2-ac25-46ab-bc4d-5e43c595eee3"}]}
This causes a persistence issue as the book already exist
org.hibernate.PersistentObjectException: detached entity passed to persist
Is there a way to have a nested resource like above in Spring and be able to associate without a persistence issue?
-------- Services
#Service
public class BookService {
#Autowired
private BookRepository bookRepository;
public List<Book> findAll() {
return bookRepository.findAll();
}
public Book create(Book book) {
return bookRepository.save(book);
}
}
#Service
public class ShelfService {
#Autowired
private ShelfRepository shelfRepository;
public List<Shelf> findAll() {
return shelfRepository.findAll();
}
public Book create(Book book) {
Shelf newShelf = shelfRepository.save(shelf);
shelf.setBooks();
return newShelf;
}
}

Welcome to the painful ... ehhh I meant wondeful ... world of ORMs where doing simple things is trivial, but doing things even a tad complicated becomes increasingly complicated :P
Few pointers that might help:
If a book HAS to belong to a shelf, the #JoinColumn should probably also have nullable=false; this should force Hibernate to persiste the shelf first since it will need the ID to persist the book's shelf_id FK.
You have a bidirectional relationship which, in a way, can be seen as an infinite loop (shelf contains books, that references shelf, that contains books, that ...); there are ways to handle that using Jackson, you can read about it here.
Going back to both points above, although your shelf contains books in your JSON data, that same data doesn't explicitly have the book point back to shelf AND, from an Hibernate perspective the relationship is optional anyway so it probably just didn't bother doing the link.
Now the "nullable=false" trick might solve all of that if you are lucky, but nothing is ever easy so I would doubt it; as I said before, if you look only at the JSON for books, they have no references to the parent shelf so when Jackson converts the books to Java object, the "shelf" property most probably stays null (but you cannot reference the shelf's ID since it wasn't created yet ... how fun!). What you will have to do is, in your ShelfRepository, when you create the shelf, you will first have to go through all the books it contains and set the reference to the parent shelf as explained in this article.
Finally, regarding the "detached entity passed to persist" exception, Hibernate will always do that if you give it an object with its identity field populated, but the actual object was never fetched using Hibernate; if you are merely referencing an object in your JSON using the ID, you will have to first fetch the real entity from Hibernate and use that exact instance in your persistence.
I hope all this info will help you.

Related

Spring Data JDBC - Many-to-One Relationship

I can't seem to find any reference online with regards to using a Many-To-One mapping in Spring JDBC. I just saw in the documentation that is not supported but I'm not sure if this is the case.
My example is that I want to map my AppUser to a particular Department.
For reference, AppUser joins to Department table using DEPARTMENT_ID
#Table(value="m_appuser")
public class AppUserProjectionTwo {
#Id
private Long id;
private String firstname;
private String middlename;
private String lastname;
#Column("DEPARTMENT_ID")
private DepartmentProjection departmenProjection;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
However, it seems that it won't map properly.
#Table("M_DEPARTMENT")
public class DepartmentProjection {
#Id
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
The created query looks like this. I was looking for something more of the opposite in which M_APPUSER.department_ID = Department.id
[SELECT "m_appuser"."ID" AS "ID", "m_appuser"."LASTNAME" AS "LASTNAME", "m_appuser"."FIRSTNAME" AS "FIRSTNAME", "m_appuser"."MIDDLENAME" AS "MIDDLENAME", "departmenProjection"."ID" AS "DEPARTMENPROJECTION_ID" FROM "m_appuser" LEFT OUTER JOIN "M_DEPARTMENT" AS "departmenProjection" ON "departmenProjection"."DEPARTMENT_ID" = "m_appuser"."ID" WHERE "m_appuser"."FIRSTNAME" = ?];
Thanks
I just saw in the documentation that is not supported but I'm not sure if this is the case.
I can confirm it is not supported.
Many-To-One relationships cross the boundaries of aggregates.
References across aggregates must be modelled as ids of the referenced aggregate.
If you don't do this Spring Data JDBC will consider the reference a One-To-One relationship and part of the same aggregate which will have effects you don't want for a Many-To-One relationship, like the referenced entity getting deleted when the referenced entity gets deleted. Which would be correct for a One-To-One relationship within the same aggregate.
This is explained in more detail in https://spring.io/blog/2018/09/24/spring-data-jdbc-references-and-aggregates

Infinite recursion for null ids when using #JsonIdentityInfo

I have 2 DTO bidirectional structures Category and Product where Product is the many side in one-to-many relationship. I want to transfer them as JSON to the front-end layer by REST. I don't have any problems when ids are already assigned (for update operation), but I face well-known infinite recursion when ids are empty (create).
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property="categoryId")
public class CategoryDTO implements Serializable {
private Long categoryId;
private String categoryName;
private List<ProductDTO> products = new LinkedList<>();
public void addProduct(ProductDTO product) {
products.add(product);
product.setCategory(this);
}
// remove synchronization method, setters, getters
}
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property="productId")
public class ProductDTO implements Serializable {
private Long productId;
private String productName;
private CategoryDTO category;
// setters, getters
}
However, when I use #JsonManagedReference and #JsonBackReference all is fine. I receive beautiful json:
{
"categoryId":null,
"categoryName":"category_name",
"products": [
{
"productId":null,
"productName":"product1"
}
]
}
public class CategoryDTO implements Serializable {
private Long categoryId;
private String categoryName;
#JsonManagedReference
private List<ProductDTO> products = new LinkedList<>();
public void addProduct(ProductDTO product) {
products.add(product);
product.setCategory(this);
}
// remove synchronization method, setters, getters
}
public class ProductDTO implements Serializable {
private Long productId;
private String productName;
#JsonBackReference
private CategoryDTO category;
// setters, getters
}
In both examples the rest side is following:
#RestController
public class CategoryController {
#GetMapping(path = "/categories")
public ResponseEntity<CategoryDTO> fetchCategories() {
CategoryDTO category = new CategoryDTO();
category.setCategoryName("category_name");
ProductDTO product1 = new ProductDTO();
product1.setProductName("product1");
category.addProduct(product1);
return new ResponseEntity<>(category, HttpStatus.OK);
}
}
Why #JsonManagedReference and #JsonBackReference work, but #JsonIdentityInfo don't?
Thanks for reading.
#JsonManagedReference and #JsonBackReference are using object instance reference, so it works, when you add Product to Category, while #JsonIdentityInfois using id from value of the field.
The answer is in the class WritableObjectId and BeanSerializerBase#_serializeWithObjectId(Object, JsonGenerator, SerializerProvider, boolean).
In other words, for non-null fields serializer remembers, that he serialized given class instance, but he cannot do that for null fields. You can see that, when you hit endpoint /categories. Before the connection is interrupted, infinite JSON is generated.
Please someone correct me, if I'm wrong.
NOTE: Imo you should just remove field private CategoryDTO category or change it to private Long categoryId and you will get rid of any annotation :D Also you won't have any problem with infinite recursion.

Save at the same time an composed object using JpaRepository

Can I save an object that contains another object directly in the database?
My back-end structure is like this:
Rest services
Services
Repository (extends JpaRepository)
Model
Suppose that I have two entity in my model: Company and Address. Both generated by the JPA tool provided by IntelliJ.
Company class model
#Entity
public class Company {
private int idcompany;
private String name;
private Address address;
#Id
#Column(name = "idcompany")
public int getIdcompany() {
return idcompany;
}
public void setIdcompany(int idcompany) {
this.idcompany = idcompany;
}
#Basic
#Column(name = "name")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#ManyToOne
#JoinColumn(name = "idaddress", referencedColumnName = "idaddress", nullable = false)
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
Address class model
#Entity
public class Address {
private long idaddress;
private String zip;
#Id
#Column(name = "idaddress")
public long getIdaddress() {
return idaddress;
}
public void setIdaddress(long idaddress) {
this.idaddress = idaddress;
}
#Basic
#Column(name = "zip")
public String getZip() {
return zip;
}
public void setZip(String zip) {
this.zip = zip;
}
}
Moreover, both entities have an interface that extends the interface JpaRepository<T,ID>. So, I have CompanyRepository and AddressRepository. I use these two interfaces in they respective Service classes: CompanyService and AddressService. This is where I put the business logic. All ok!
Now, I recive using a REST service, through POST an object Company that contains the object Address. I need to save them into the database (MySql).
In the json file there are Company that contains Address!
Until now, I've always done these steps:
Save Address;
Retrieve the Address just saved (i need the idaddress);
I associate the Address to the company using setAddress;
Save Company
I tried to save the object Company received via REST calling the method save from CompanyService (using CompanyRepository) but I got the error:
Column 'idaddress' cannot be null
I ask you. Is there an easier way to save Company and Address at the same time using JpaRepository???
You don't have defined any cascading.
You could define PERSIST to let the Address persist when Company is persisted:
#ManyToOne
#JoinColumn(name = "idaddress", referencedColumnName = "idaddress", nullable = false,
cascade = CascadeType.PERSIST)
public Address getAddress() {
return address;
}
For every method on the EntityManager there is a CascadeType defined.
Read more about the cascade types here:
https://vladmihalcea.com/a-beginners-guide-to-jpa-and-hibernate-cascade-types/

hibernate search critteria by sample entity, id not on a property list

I have a hibernate entity Car
#Entity
#Table(name = "car")
public class Car extends AbstractEntityBean implements java.io.Serializable {
private Integer carId;
private Integer name;
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "carId", unique = true, nullable = false)
public Integer getCarId() {
return this.carId;
}
public void setCarId(Integer carId) {
this.carId = carId;
}
#Column(name = "name")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
Then I try to search a car by example:
crit.add(example);
In "example" I set carName adn search works great, as expected.
But when I set carId in "example", carId is being ignored in search criteria and query returns all cars.
I did some debuggin and I see in hibernate-core-3.6.10.Final-sources.jar!\org\hibernate\tuple\entity\EntityMetamodel.java there is a property
private final String[] propertyNames;
I can see "name" on propertyNames list, but not cardId.
When I add annotation #Id to name instead of carId, carId shows up on propertyNames list and I can search by example where carId is set.
Question: How can I search by example using carId when #Id annotation is set at carId?
Thanks
As I remember from QBE with Hibernate criteria, this is the way it is designed. So short question, you can't query by the id using QBE. And it also doesn't make sense. When querying by id, you can only get one result.
As a side note: Hibernate 3.6 is quite old. Probably way out-of-date!

Hibernate One to many Annotation Mapping

Hi I have a two tables like below .
1) Task - id,name
2) Resource - id,name,defaultTask(foreign key to Task.id)
The mapping is one to Many - one task can have many resource.
The code for Task is like below.
#Entity
public class Task implements Serializable {
private long m_id;
private String m_name;
#Id
#GeneratedValue(
strategy = GenerationType.AUTO
)
public long getId() {
return this.m_id;
}
public void setId(long id) {
this.m_id = id;
}
public String getName() {
return this.m_name;
}
public void setName(String name) {
this.m_name = name;
}
#OneToMany
#JoinColumn(
name = "defaultTask"
)
private List<Resource> m_relatedResources;
public List<Resource> getrelatedResources() {
return m_relatedResources;
}
public void setrelatedResources(List<Resource> relatedResources) {
m_relatedResources = relatedResources;
}
And the code for Resource class is like below.
#Entity
public class Resource implements Serializable {
private Long m_id;
private String m_name;
#Id
#GeneratedValue(
strategy = GenerationType.AUTO
)
public Long getId() {
return this.m_id;
}
public void setId(Long id) {
this.m_id = id;
}
public String getName() {
return this.m_name;
}
public void setName(String name) {
this.m_name = name;
}
Task m_task;
#ManyToOne
#JoinColumn(
name = "defaultTask"
)
public Task getTask() {
return this.m_task;
}
public void setTask(Task task) {
this.m_task = task;
}
}
When i execute it I am getting an error like
Initial SessionFactory creation failed.org.hibernate.MappingException: Could not determine type for: java.util.List, for columns: [org.hibernate.mapping.Column(relatedResources)]
What have i done wrong ?How can i fix the problem ?
You can't apply annotations to methods or fields randomly. Normally, you should apply your annotations the same way as #Id..
In Task class OneToMany should be like
#OneToMany
#JoinColumn(
name = "defaultTask"
)
public List<Resource> getrelatedResources() {
return m_relatedResources;
}
Field access strategy (determined by #Id annotation). Put any JPA related annotation right above each method instead of field / property as for your id it is above method and it will get you away form exception.
Also there appears to be an issue with your bidrectional mapping metntioned by #PredragMaric so you need to use MappedBy which signals hibernate that the key for the relationship is on the other side. Click for a really good question on Mapped by.
Many mistakes here:
you're annotating fields sometimes, and getters sometimes. Half of the annotation will be ignored: you must be consistent. It's one or the other.
You're not respecting the Java Bean naming conventions. The getter must be getRelatedResources(), not getrelatedResources().
A bidirectional association must have an owner side and an inverse side. In a OneToMany, the One is always the inverse side. The mapping should thus be:
.
#ManyToOne
#JoinColumn(name = "defaultTask")
public Task getTask() {
return this.m_task;
}
and
#OneToMany(mappedBy = "task")
public List<Resource> getRelatedResources() {
return m_relatedResources;
}
I also strongly advise you to respect the Java naming conventions. Variables should be named id and name, not m_id and m_name. This is especially important if you choose to annotate fields.
You're mixing annotating fields and getters in the same entity, you should move your #OneToMany to a getter
#OneToMany
#JoinColumn(mappedBy = "task")
public List<Resource> getrelatedResources() {
return m_relatedResources;
}
and yes, as the others mentioned, it should be mappedBy = "task". I'll upvote this teamwork :)
#JoinColumn is only used on owner's side of the relation, ToOne side, which is Resource#task in your case. On the other side you should use mappedBy attribute to specify bidirectional relation. Change your Task#relatedResources mapping to this
#OneToMany(mappedBy = "task")
private List<Resource> m_relatedResources;
Also, as #Viraj Nalawade noticed (and others, obviously), mapping annotations should be on fields or properties, whatever is used for #Id takes precedence. Either move #Id to field, or move #OneToMany to getter.

Categories

Resources