Spring Data JPA - Persisting new Object with existing nested child - java

I do have two entities called School and Level with a Many-To-Many relationship.
#Data
#Entity
public class School {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "school_generator")
#SequenceGenerator(name="school_generator", sequenceName = "school_seq", allocationSize=1)
#Column(name = "school_id")
private Long id;
#NotBlank
private String name;
#NotBlank
private String city;
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinTable(
name = "school_level",
joinColumns = #JoinColumn(name = "school_id"),
inverseJoinColumns = #JoinColumn(name = "level_id"))
private Set<Level> levels = new HashSet<>();
}
#Data
#Entity
public class Level implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "level_generator")
#SequenceGenerator(name = "level_generator", sequenceName = "level_seq", allocationSize = 1)
#Column(name = "level_id")
private Long id;
#NotBlank
#Column(nullable = false, unique = true)
private String name;
}
My SchoolController, Rest controller, has a create method which takes a SchoolDTO (for now it has the same structure as the entity).
When I post the DTO, the nested child list Levels are populated only with existing Levels ids (since Levels are created previously).
When I try to save through the JPARepository save method...An exception with this message is thrown:
"detached entity passed to persist: com.salimrahmani.adawat.domain.Level; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: com.salimrahmani.adawat.domain.Level",
"trace": "org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist:
The problem is solved by iterating through each Level Id ...get the references by using the JPA Repository method getOne (which calls entityManager.getReference)
and then loads the right model and then persist successfully.
#PostMapping("/{id}/grades")
public ResponseEntity<SchoolDTO> addLevelToPackage(#PathVariable Long id, #RequestBody #Valid LevelDTO levelDTO) {
return schoolService.findById(id)
.map(school -> {
Level level = levelMapper.map(levelDTO);
if(level.getId() != null) {
level = levelService.getOne(level.getId());
school.getLevels().add(level);
} // else error
School saved = schoolService.save(school);
return ResponseEntity.ok(schoolMapper.map(saved));
}).orElseGet(() -> ResponseEntity.notFound().build());
}
My question is: Is this the "only" correct way to posting association in Spring Data? Or, is there any better way?

In the addLevelToPackage function Why using the map schoolService.findById(id).map.. if as I think the schoolService will return only one instance of the entity School with the given id.
I think replacing the map with the following would be enough:
...
School theSchool = schoolService.findById(id);
Level theLevel = levelService.getOne(levelDTO.getId());
theSchool.getLevels().add(theLevel);
School saved = schoolService.save(theSchool);
return ResponseEntity.ok(schoolMapper.map(saved));
...

Related

How to send only a list of IDs in many-to-many spring boot JPA POST request instead of sending the full object's data

I have 2 DTOs "OrderItem" and "Ingredient", both classes has #ManyToMany annotation:
#Entity
#Table
#NoArgsConstructor
#Data
public class OrderItem {
private #Id #GeneratedValue #NotNull long id;
#ManyToOne(optional = false)
#JoinColumn(nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Order order;
#ManyToOne(optional = false)
#JoinColumn(nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Food food;
private int quantity;
#ManyToMany(cascade=CascadeType.ALL)
#JoinTable(
name = "order_item_ingredient",
joinColumns = #JoinColumn(name = "order_item_id"),
inverseJoinColumns = #JoinColumn(name = "ingredient_name")
)
private Set<Ingredient> ingredients = new HashSet<>();
}
#Entity
#Table
#Data
#NoArgsConstructor
public class Ingredient {
private #Id String ingredientName;
private float basePrice;
private boolean addable;
#ManyToMany(mappedBy = "ingredients",cascade=CascadeType.ALL)
private Set<Food> foods= new HashSet<>();
#ManyToMany(mappedBy = "ingredients",cascade=CascadeType.ALL)
private Set<OrderItem> orderItems= new HashSet<>();
public Ingredient(String ingredientName, float basePrice, boolean addable) {
this.ingredientName = ingredientName.toLowerCase();
this.basePrice = basePrice;
this.addable = addable;
}
}
And I'm looking to add a new OrderItem using a POST request using the following #PostMapping controller function:
#PostMapping("{id}/orderItem")
public ResponseEntity<OrderItem> createMenuItem(
#PathVariable(value = "id") Long orderId,
#RequestBody OrderItem orderItem) {
Order order = orderService.getOrder(orderId)
.orElseThrow(() -> new ResourceNotFoundException("order '" + orderId + "' is not found"));
orderItem.setOrder(order);
orderItemRepository.save(orderItem);
return new ResponseEntity<>(orderItem, HttpStatus.CREATED);
}
When I send a post request to localhost:8080/1/orderItem with the following body:
{
"order":"1",
"food":"burger",
"quantity":"1"
}
It works fine and a new order_item database record is created, but when I send the same request with the following body:
{
"order":"1",
"food":"burger",
"quantity":"1",
"ingredients": [{"ingredientName":"leaf"}]
}
It fails and gives the following SQL error:
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'leaf' for key 'ingredient.PRIMARY'
I know that this record already exists, but how do I tell Spring Boot that I want it to look for an existing Ingredient instead of trying to create a new one?
I have an ugly solution in my mind, and that is to send the OrderItem object alongside a list of strings where each element represents a primary key for Ingredient class, then iterate through that list element by element calling the repository to get the Ingredient object then manually add it to OrderItem.ingredients, but I'm sure that is not the best solution out there.
Being defined on the OrderItem class, the relation ingredients is considered as a composition on the cascading strategy point of view. Therefore, the CascadeType.ALL implies the attempt to create the ingredient.
To avoid this, you can change the direction of this relation reverse the mappedBy information.
But then again, if you keep a CascadeType.ALL on the ingredient side, you will be in trouble if you create an ingredient with an existing orderItem. You can win on both sides an use CascadeType.ALL.
check JPA Hibernate many-to-many cascading

Hibernate doesn't fetch eager

I have tow classes, the "Article" which contains a #ManyToOne reference to a "SurchargeGroup" which specifies the surcharge for that article.
#Entity
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode(doNotUseGetters = true)
#Audited
public final class Article {
#Id
#GeneratedValue(generator = "increment")
#GenericGenerator(name = "increment", strategy = "increment")
#Getter(onMethod_ = {#Key(PermissionKey.ARTICLE_ID_READ)})
#Setter(onMethod_ = {#Key(PermissionKey.ARTICLE_ID_WRITE)})
private int id;
#JoinColumn(nullable = false)
#ManyToOne
#Getter(onMethod_ = {#Key(PermissionKey.SURCHARGE_TABLE_SUPPLIER_READ)})
#Setter(onMethod_ = {#Key(PermissionKey.SURCHARGE_TABLE_SUPPLIER_WRITE)})
private SurchargeGroup surchargeGroup;
}
The other class "SurchargeGroup" contains a parent object reference which can inherit the surcharge to the "SurchargeGroup" if it isn't set the case that no surcharge is provided by any parent is not possible.
#Table
#Entity
#EqualsAndHashCode(doNotUseGetters = true)
#Audited
public class SurchargeGroup implements Serializable, Cloneable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column
#Getter(onMethod_ = {#Key(PermissionKey.SURCHARGE_TABLE_ID_READ)})
#Setter(onMethod_ = {#Key(PermissionKey.SURCHARGE_TABLE_ID_WRITE)})
private int id;
#Column
#Setter(onMethod_ = {#Key(PermissionKey.SURCHARGE_TABLE_SURCHARGE_WRITE)})
private Double surcharge;
#Column
#Getter(onMethod_ = {#Key(PermissionKey.SURCHARGE_TABLE_NAME_READ)})
#Setter(onMethod_ = {#Key(PermissionKey.SURCHARGE_TABLE_NAME_WRITE)})
private String name;
#JoinColumn
#ManyToOne(fetch = FetchType.EAGER)
#Getter(onMethod_ = {#Key(PermissionKey.SURCHARGE_TABLE_SUPPLIER_READ)})
#Setter(onMethod_ = {#Key(PermissionKey.SURCHARGE_TABLE_SUPPLIER_WRITE)})
private SurchargeGroup parent;
public double getSurcharge() {
if (surcharge == null) {
return parent == null
? supplier == null
? Setting.SURCHARGE_DEFAULT.getDoubleValue()
: supplier.getDefaultSurcharge()
: parent.getSurcharge();
} else return surcharge;
}
#JoinColumn
#ManyToOne
#Getter(onMethod_ = {#Key(PermissionKey.SURCHARGE_TABLE_SUPPLIER_READ)})
#Setter(onMethod_ = {#Key(PermissionKey.SURCHARGE_TABLE_SUPPLIER_WRITE)})
private Supplier supplier;
}
My problem is now that if I call the "getSurcharge()" method I get this exception which I cannot explain to myself because I marked the surcharge group to fetch eager
Exception in thread "AWT-EventQueue-0" org.hibernate.LazyInitializationException: could not initialize proxy [kernbeisser.DBEntities.SurchargeGroup#1046] - the owning Session was closed
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:172)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:309)
at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45)
at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
at kernbeisser.DBEntities.SurchargeGroup$HibernateProxy$cdTAuBkS.getSurcharge(Unknown Source)
I asked myself if this could get caused by the #Audited annotation? Any ideas? Thanks a lot!
Note: the #Key annotations have no effect to this scenario.
Here is what the debugger shows (Sorry for the German toString() functions):
Hibernate needs to stop eagerly fetching associations at some point, otherwise it would need to join an infinite number of times the SurchargeGroup entity (since it references itself).
The depth these fetches can be controlled application wide using the hibernate.max_fetch_depth property.
The source of the error was the AuditReader it doesn't fetch all eager properties even if they are annotated as Fetch.EAGER
It looks like the AuditReader only fetches one level of eager relations:
Article -> SurchargeGroup -> SurchargeGroup -> ...
(fetched) (fetched) (not fetched)

Hibernate: add existing child entity to new entity with OneToMany bidirectional relationship and persist it ('detached entity passed to persist')

In order to understand why I have persisted child entities, here is the mapping.
I have Author (id, name, books) and Book (id, title, authors) entities. Their relationship is ManyToMany since any Author may have more than one Book, and any Book may have more than one Author. Also I have BookClient (id, name, rentDate, books) - relationship with Book entity is OneToMany since any Client may rent zero to many books.
Author.java
#Table
public class Author {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false)
private String name;
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "books_authors",
joinColumns = { #JoinColumn(name = "author_id") },
inverseJoinColumns = { #JoinColumn(name = "book_id") }
)
private Set<Book> books = new HashSet<>();
Book.java
#Entity
#Table
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(nullable = false, unique = true)
private Long id;
#Column(nullable = false)
private String title;
#ManyToMany(mappedBy = "books", fetch = FetchType.EAGER)
private Set<Author> authors = new HashSet<>();
#ManyToOne
#JoinColumn(name = "client_id")
private BookClient bookClient;
BookClient.java
#Entity
#Table
public class BookClient {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "client_id")
private Long id;
#Column(nullable = false)
private String name;
#OneToMany(mappedBy = "bookClient", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private Set<Book> books = new HashSet<>();
private LocalDate rentDate;
Some business logic behind the scenes: there're a lot of books written by different authors which are persisted in DB of some, let's say, library. And this library gives books to clients. Any new client may register in the library when he/she takes a book.
Book clients are persisted using Entity Manager:
#Transactional
#Repository("bookClientDao")
public class BookClientDaoImpl implements BookClientDao {
#PersistenceContext
private EntityManager entityManager;
#Override
public void save(BookClient bookClient) {
entityManager.persist(bookClient);
}
#Override
public void saveOrUpdate(BookClient bookClient) {
if(bookClient.getId() == null) {
save(bookClient);
} else {
entityManager.merge(bookClient);
}
}
}
Here is an example of how it may look like in code:
public static void main(String[] args) {
ApplicationContext appContext = new ClassPathXmlApplicationContext("META-INF/context.xml");
AuthorDao authorDao = (AuthorDao) appContext.getBean("authorDao");
BookClientDao bookClientDao = (BookClientDao) appContext.getBean("bookClientDao");
//Create a book and its author
Book book = new Book();
book.setTitle("John Doe Book the 1st");
Author author = new Author();
author.setName("John Doe");
author.getBooks().add(book);
authorDao.save(author);
//Registering new Book Client
BookClient bookClient = new BookClient();
bookClient.setName("First Client");
bookClient.getBooks().add(book);
bookClient.setRentDate(LocalDate.now());
book.setBookClient(bookClient);
//book is expected to be updated by cascade
bookClientDao.saveOrUpdate(bookClient); //'detached entity passed to persist' occurs here
}
After running this code I get detached entity passed to persist exception since Book instance has already been persisted earlier.
Exception in thread "main" javax.persistence.PersistenceException:
org.hibernate.PersistentObjectException: detached entity passed to persist: entity.manager.example.entity.Book
...
Caused by: org.hibernate.PersistentObjectException:
detached entity passed to persist: entity.manager.example.entity.Book
If I persist BookClient berforehand, then connection between BookClient and Book is set correctly since both entities are existed in DB. But it seems to me as some workaround.
Is it possible to create new object, connect already persisted entity to it and persist this object with cascade update of all its children?
In your example, save operation for the author and the bookClient executes in different persistence contexts. So as for the main method, first you should merge books (into saveOrUpdate method) that were detached during saving the author.
Is it possible to create new object, connect already persisted entity to it and persist this object with cascade update of all its children?
It may depend on your application requirements and functionality. In this main() example, it looks like you want to save all these entities transactionally.

Spring JPA: Select specific columns on join with annotation/JPQL

I am trying to get familiar with spring and jpa. For start there is a table anime_details containing details of an anime. As it can have many genres, db has another table named genre. The intermediate table to contain their many to many relationship entries is also there. When I query for any anime by id, it should return the details of the anime along with the genres.
It does return an object with details of the anime and list of Genre objects (which is as expected). But what I want is to restrict the columns that will be fetched from Genre objects. For example only id or just id and name (In case there are more columns other than these).
AnimeDetails
#Getter
#Setter
#Entity
#Table(name = "anime_details")
public class AnimeDetails {
#Id
#SequenceGenerator(name = "animeDetailsSeq", sequenceName =
"anime_details_id_seq",
allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
"animeDetailsSeq")
private Integer id;
private String name;
private Integer episodes;
private Date started;
private Date ended;
private String status;
#ManyToMany
#JoinTable(
name = "anime_genre",
joinColumns = #JoinColumn(name = "details_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "genre_id", referencedColumnName = "id"))
#JsonManagedReference
private List<Genre> genres;
protected AnimeDetails() {
}
}
Genre
#Data
#Entity
#Table(name = "genre")
public class Genre {
#Id
#SequenceGenerator(name = "genreSeq", sequenceName = "genre_id_seq",
allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "genreSeq")
private Integer id;
private String name;
#ManyToMany(mappedBy = "genres")
List<AnimeDetails> animes;
protected Genre() {
}
}
Expected payload
{
"id": 2,
"name": "Your Name",
"episodes": 1,
"started": "2016-08-25T18:00:00.000+0000",
"ended": "2016-08-25T18:00:00.000+0000",
"status": "Completed",
"genres": [
{
"id": 5,
"name": "Drama"
},
{
"id": 10,
"name": "Supernatural"
}
]
}
Right now, I get the result and manually get columns one by one and set those in a DTO. But that is not efficient as the database query is already fetching more data than needed.
Is there any specific annotation/property/jpql to reduce it?
Indeed was looking for a proper solution regarding the same issue , cause as you pointed out it is creating performance issues as there are huge useless data loads between the APP and the DB. Imagine that there could be, not only one query but much more and you need a global optimization solution...
From the first place Spring DATA is not supporting this operation so its leading you at the manual configuration and set up on a DTO reference. The same applies if you were using a custom Object and returning that inside the JPQL with the constructor trick , or else write a native query , get back a List<Object> and again manually map the data back to your actual object , which is the most efficient but not elegant solution
More info in this link , but try to check both answers for the details.
The other thing is that as you are using hibernate underneath , which is providing custom mappers , you could always write up your custom HQL(not jpql) , set up a proper DAO , wire up the EntityManager or directly the SessionFactory (which is breaking the abstract JPA contract , but you can utilize the full goodies that hibernates offers) and then return the same object, but only with the columns you need.
Example for the second point:
import javax.persistence.EntityManager;
import org.hibernate.query.Query;
import org.hibernate.transform.Transformers;
public CustomEntity getEntity(){
Query<CustomEntity> q = (Query<CustomEntity>) entityManager.createQuery("select
e.id,e.name from CustomEntity e where e.name = 'parameter'");
q.setResultTransformer(Transformers.aliasToBean(CustomEntity.class));
CustomEntity entity = (CustomEntity) q.getSingleResult();
return name;
}
Note CustomEntity is a managed Entity Bean / Table in the database , just placing this example to be close on what you might need to achieve.
Tried with
Spring boot 2.0.5.RELEASE
Spring Data 2.0.5.RELEASE
Hibernate-core 5.2.17.Final
I tried a different solution today. Lets look at the code first.
Genre
#Data
#Entity
#Table(name = "genre")
public class Genre {
#Id
#SequenceGenerator(name = "genreSeq", sequenceName = "genre_id_seq",
allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "genreSeq")
private Integer id;
private String name;
#ManyToMany
#JoinTable(
name = "anime_genre",
joinColumns = #JoinColumn(name = "genre_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "details_id", referencedColumnName = "id"))
List<AnimeIdentity> animes;
protected Genre() {
}
}
AnimeIdentity
#Getter
#Setter
#Entity
#Table(name = "anime_details")
public class AnimeIdentity {
#Id
#SequenceGenerator(name = "animeDetailsSeq", sequenceName =
"anime_details_id_seq",
allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
"animeDetailsSeq")
private Integer id;
private String name;
protected AnimeIdentity() {};
}
Queries Hibernate made
Hibernate: select genre0_.id as id1_2_0_, genre0_.name as name2_2_0_ from genre genre0_ where genre0_.id=?<br>
Hibernate: select animes0_.genre_id as genre_id2_1_0_, animes0_.details_id as details_1_1_0_, animeident1_.id as id1_0_1_, animeident1_.name as name2_0_1_ from anime_genre animes0_ inner join anime_details animeident1_ on animes0_.details_id=animeident1_.id where animes0_.genre_id=?
Feel free to show me the pros and cons of this solution. To me its a good solution if my necessity is limited to only this. But in case of different type of queries making more and more entity pojos will be a tiresome task.

HibernateException- identifier of an instance of Domain was altered from X to Y

I am getting an exception when I am updating Parent record in spring data jpa.
This is my code:
ParentEntity
#Entity
#Table(name = "CAMP")
#Getter
#Setter
public class Parent extends AbstractEntity {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tkeygenerator")
#GenericGenerator(name = "tkeygenerator", strategy = "com.custom.TKeyGenerator",
parameters = {#org.hibernate.annotations.Parameter(name = "sequence", value = "TKEY_SEQ")})
#Column(name = "TKEY", nullable = false)
private String id;
#ManyToOne
#JoinColumn(name = "SUB_CAT_TYPE_CODE", referencedColumnName = "SUB_CAT_TYPE_CODE")
private Child child;
#Column(name = "DATE")
#Basic
private LocalDate date;
}
Child Entity
#Entity
#Table(name = "SUB_CAT_TYPE")
#AttributeOverrides({
#AttributeOverride(name = "code",
column = #Column(name = "SUB_CAT_TYPE_CODE", length = 30)),
#AttributeOverride(name = "description",
column = #Column(name = "SUB_CAT_TYPE_DESC", length = 255))})
#EqualsAndHashCode(callSuper = true)
public class Child extends AbstractTypeDesc {}
TestCode
public Parent update(#PathVariable("id") String id, #Valid #RequestBody UpdateDto dto) {
Parent parentObj = parentRepository.findById(id);
mapper.map(dto, parentObj); // Dozer to map incoming dto to domain
childRepository.findByCode(dto.child().getCode())
.map(child -> {
parentObj.setChild(child);
return child;
});
return parentRepository.save(parentObj); //Exception occurs here
}
I am getting an exception while trying to update code variable of child entity in parent entity as fk. It says can't alter code from X to Y.
Any suggestion?
I figured out what went wrong in above code block for update operation.Although not much clear why its happening.Dozer mapping which maps dto to Domain was causing issue it was changing the value of child entity and then again when i was trying to set child entity through setter method It was causing "Id alter exception", Though i thats the same thing i dont know why it was taking it differently. below is the working Working code.
Test code should be like
public Parent update(#PathVariable("id") String id, #Valid #RequestBody UpdateDto dto) {
Parent parentObj = parentRepository.findById(id);
childRepository.findByCode(dto.child().getCode())
.map(child -> {
parentObj.setChild(child);
return child;
});
mapper.map(dto, parentObj); // Dozer dto to domain mapping was causing problem
return parentRepository.save(parentObj);
}

Categories

Resources