List Results Mapping Spring JPA - java

I use PostgreSQL and I have these tables, product and product_media with relation OneToMany on product with product_media. I want to retrieve a list with product which each of them contains a list of product_media.
And I have two options in my mind in order to retrieve them from DB.
First solution is initially retrieve the list of product and then iterate the retrieved list and execute query in order to retrieve the list of product_media.
Query1:
select * from product as p where p.status=1;
Retrieve List and then iterate this list and execute this query:
select * from product_media as pm where pm.product_id=?
Second is to implement join in query and retrieve all data from my DB.
Query:
select * from product as p Join product_media as pm on (p.id=pm.product_id)
Retrieve a complex list with all data.
The problem of second option is to do not know an elegant way to map this list into an object which has the format below. Do you know how can map automatically the results into this format?
product:[
{
id:1,
name:'Pro1',
medias:[
{
id:1,
uuid:'asdfi-asdf-rg-fgsdf-do'
},
{
id:2,
uuid:'asdfi-asdf-rg-fgsdf-do'
}
]
},
{
id:2,
name:'Pro2',
medias:[
{
id:5,
uuid:'asdfi-asdf-rg-fgsdf-do'
},
{
id:7,
uuid:'asdfi-asdf-rg-fgsdf-do'
}
]
}
]

I think the second variant is the better option. After fetching the object tree from the database you can do something like the following to achieve what you are posted above:
Assuming your entities are defined as follows:
Product.java
public class Product {
private long id;
private String name;
private List<ProductMedia> mediaList;
public Product() {
mediaList = new ArrayList<ProductMedia>();
}
public Product(long id, String name) {
this.id = id;
this.name = name;
mediaList = new ArrayList<ProductMedia>();
}
// getters + setters
}
ProductMedia.java
public class ProductMedia {
private long id;
private String uuid;
public ProductMedia() { }
public ProductMedia(long id, String uuid) {
this.uuid = uuid;
}
// getters + setters
}
Using the Jackson library you can generate output as follows:
public class JsonTest {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
Product prod = new Product(1, "p1");
ProductMedia pm = new ProductMedia(1, "uuid1");
ProductMedia pm2 = new ProductMedia(2, "uuid2");
prod.getMediaList().add(pm);
prod.getMediaList().add(pm2);
Product prod1 = new Product(2, "p2");
ProductMedia pm3 = new ProductMedia(3, "uuid3");
ProductMedia pm4 = new ProductMedia(4, "uuid4");
prod1.getMediaList().add(pm3);
prod1.getMediaList().add(pm4);
Product[] pList = {prod, prod1};
mapper.writeValue(System.out, pList);
}
}
In this example, I am writing the output onto the console. But you are not restricted to it; you can write to a file passing in a FileOutputStream.
To be able to run this example you need to add the dependency; if you use Maven you can add the following into your POM:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.7.4</version>
</dependency>
Otherwise add the jar of the dependency into your project build path.

If your response is not in json format you can try below
There is a many-to-many relationship between Product and Media.
Product_Media is a helper table to maintain many-to-many relationship between Product and Media entities.
Product entity:
#Entity(name = "product")
public class Product {
#Id
#GeneratedValue
private Long product_id;
#Column
private String name;
#ManyToMany(cascade = { CascadeType.MERGE }, fetch = FetchType.EAGER)
#JoinTable(name = "product_media", joinColumns = {
#JoinColumn(name = "product_id", table = "product") }, inverseJoinColumns = {
#JoinColumn(name = "media_id", table = "media") })
List<Media> medias;
}
Media entity
#Entity(name = "media")
public class Media {
#Id
#GeneratedValue
private Long media_id;
#Column
private String name;
}
SQL generated by Hibernate
select
product0_.product_id as product_1_1_0_,
product0_.name as name2_1_0_,
medias1_.product_id as product_1_1_1_,
media2_.media_id as media_id2_2_1_,
media2_.media_id as media_id1_0_2_,
media2_.name as name2_0_2_
from
product product0_
left outer join
product_media medias1_
on product0_.product_id=medias1_.product_id
left outer join
media media2_
on medias1_.media_id=media2_.media_id
where
product0_.product_id=?
If the relationship is one-to-many, change entities like below
Media Entity
#Entity(name = "media")
public class Media {
#Id
#GeneratedValue
private Long id;
#Column
private String name;
#ManyToOne
#JoinColumn(name = "product_id", referencedColumnName = "id", nullable = false, updatable = false)
private Product product;
public Media() {
}
}
Product Entity
#Entity(name = "product")
public class Product {
#Id
#GeneratedValue
private Long id;
#Column
private String name;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "product")
List<Media> medias;
}
Hibernate generated SQL
select
product0_.id as id1_2_0_,
product0_.name as name2_2_0_,
medias1_.product_id as product_3_2_1_,
medias1_.id as id1_0_1_,
medias1_.id as id1_0_2_,
medias1_.name as name2_0_2_,
medias1_.product_id as product_3_0_2_
from
product product0_
left outer join
media medias1_
on product0_.id=medias1_.product_id
where
product0_.id=?

Related

set a relationship in jpa

Yes, i know there's lots of this questions about jpa and foreign keys
But i dont found any answer for my case.
I need to import json data in a mysql database. The data looks like this example
order -(1:n)> item
{
type: "order",
name: "abcd",
business_key: "2020_0001",
}
[
{
type: "item",
desc: "xxxx",
business_key: "2020_0001",
date: "2020-01-01",
},
{
type:"item",
desc: "aaaaa",
business_key: "2020_0001",
date: "2020-01-01",
}
]
The business_key is in the old datasource the foreign key.
What i tried was to create a java entity class for order with a oneToMany items.
public class Order {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "business_key")
private String businessKey;
#OneToMany(mappedBy = "order")
private List<Item> items;
}
public class Item {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "business_key")
private String businessKey;
#ManyToOne
#JoinColumn(name = "order_id", referencedColumnName = "id")
private Order order;
}
And here is the part to insert the data in the database
private void processObjectMapping(String className, File file) throws IOException {
if (className.matches("^item.json$")) {
List<Item> list = mapper.readValue(file, new TypeReference<List<Item>>() {
});
itemRepository.saveAll(list);
} else if (className.matches("^order.json$")) {
Order order = mapper.readValue(file, new TypeReference<Order>() {
});
orderRepository.save(order);
}
Of course the JPA cant insert a value in the Foreign_key column all objects were created independently.
Question: is there a way to build a relationship between the entities in jpa with the existing business_key.
Or what i thought is to run after the import is done a process which set the releationships?
Could help me here a JPA Entity Graph?
Or maybe other recommendations?

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

JPA Hibernate Lazy load collection in a self relationship

I'm trying to lazily load the ingredients of a product in a self relationship. A product can have zero or more ingredients. The relationship is stored in the ProductComposition entity.
These are my entities:
Product
#Entity(name = Product.TABLE_NAME)
//#NamedEntityGraph(name = "graph.Product.ingredients", attributeNodes = //#NamedAttributeNode("ingredients"))
public class Product {
public static final String TABLE_NAME = "Product";
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long idProduct;
private String name;
#OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE}, mappedBy = "product")
private List<OrderDetail> orders;
#OneToMany(fetch = FetchType.LAZY,cascade = CascadeType.ALL, mappedBy = "ingredient", orphanRemoval=true)
private List<ProductComposition> ingredients;
ProductComposition
#Entity(name = ProductComposition.TABLE_NAME)
#IdClass(ProductCompositionId.class)
public class ProductComposition {
public static final String TABLE_NAME = "ProductComposition";
#Id
#ManyToOne //(fetch = FetchType.LAZY)
#PrimaryKeyJoinColumn(name = "PrincipalProductID")
private Product principalProduct;
#Id
#ManyToOne //(fetch = FetchType.LAZY)
#PrimaryKeyJoinColumn(name = "IngredientID")
private Product ingredient;
private int quantity;
ProductCompositionId
class ProductCompositionId implements Serializable{
private long principalProduct;
private long ingredient;
In the method get of my Dao I've tried different things:
Fetching with a CriteriaQuery the ingredients and then set them to the product
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<ProductComposition> q = cb.createQuery(ProductComposition.class);
Root<ProductComposition> product = q.from(ProductComposition.class);
product.fetch("principalProduct", JoinType.LEFT);
q.select(product).where(cb.equal(product.get("principalProduct"), id));
List<ProductComposition> ingredients = entityManager.createQuery(q).getResultList();
Product p = entityManager.find(Product.class, id);
p.setIngredients(ingredients);
Using an Entity Graph
EntityGraph<Product> graph = (EntityGraph<Product>) entityManager.getEntityGraph("graph.Product.ingredients");
Map<String, Object> ingredients = new HashMap<>();
ingredients.put("javax.persistence.fetchgraph", graph);
Product p = entityManager.find(entityClass, id, ingredients);
Calling the method initialize
p = productDao.get(p.getIdProduct()); //the get here just calls entityManager.find(Product.class, id)
Hibernate.initialize(p.getIngredients());
System.out.println("Ingredients size: "+p.getIngredients().size()); //gives 0
After calling those two above lines I get the following two logs, but p still has no ingredients after:
Hibernate: select product0_.idProduct as idProduc1_4_0_, product0_.name as name2_4_0_, orders1_.product_idProduct as product_3_3_1_, orders1_.foodOrder_idFoodOrder as foodOrde2_3_1_, orders1_.foodOrder_idFoodOrder as foodOrde2_3_2_, orders1_.product_idProduct as product_3_3_2_, orders1_.quantity as quantity1_3_2_, foodorder2_.idFoodOrder as idFoodOr1_2_3_, foodorder2_.CustomerID as Customer2_2_3_, foodorder2_.DeliverymanID as Delivery3_2_3_, foodorder2_.RestaurantID as Restaura4_2_3_ from Product product0_ left outer join OrderDetail orders1_ on product0_.idProduct=orders1_.product_idProduct left outer join FoodOrder foodorder2_ on orders1_.foodOrder_idFoodOrder=foodorder2_.idFoodOrder where product0_.idProduct=?
Hibernate: select ingredient0_.ingredient_idProduct as ingredie2_5_0_, ingredient0_.principalProduct_idProduct as principa3_5_0_, ingredient0_.ingredient_idProduct as ingredie2_5_1_, ingredient0_.principalProduct_idProduct as principa3_5_1_, ingredient0_.quantity as quantity1_5_1_, product1_.idProduct as idProduc1_4_2_, product1_.name as name2_4_2_ from ProductComposition ingredient0_ inner join Product product1_ on ingredient0_.principalProduct_idProduct=product1_.idProduct where ingredient0_.ingredient_idProduct=?
However, all tries can't load the ingredients. They just return an empty list.
What am I doing wrong in this methods?
I would prefer to keep the relationship as lazy. Also because otherwise hibernate will return cannot simultaneously fetch multiple bags referring to Product.orders and Product.ingredients

Join external column to Hibernate entity using native SQL

I have a (simplified) table structure that looks something like that:
customer table:
id name
-------------------
1 customer1
alias table:
customer_id alias
-------------------------------
1 customer one
1 customer uno
When I run the following query I easily get the list of aliases per customer:
select * from customer_alias where customer_id=1;
I would like to use this query in my hibernate to populate a list of type String. I tried using #Formula as follows:
#Entity
#Table(name = "customer")
public class Customer {
#Id
#Column(name = "id")
#GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
#Column(name="name")
private String name;
#Formula("(select alias from customer_alias where customer_id = id)")
private List<String> aliases;
// Getters, setters, etc...
}
It didn't work and I got this exception:
Could not determine type for: java.util.List, at table: customer, for columns: [org.hibernate.mapping.Formula( (select alias from customer_alias where customer_id = id) )]
Is there anyway to achieve this? Doesn't have to be with #Formula of course. Any reasonable way would be great.
Here is an SQLFiddle of my example
You could use #ElementCollection for having a List of related aliases without the need to map the whole entity:
#ElementCollection
#CollectionTable(name = "customer_alias", joinColumns = #JoinColumn(name = "customer_id") )
#Column(name = "alias")
private List<String> aliases;
See also:
Difference between #OneToMany and #ElementCollection?
I think you don't want to use OneToMany annotation as the second table is just a list of strings you want to find something more elegant that would not require me to create another entity.
You can use #ElementCollection as below:
#Entity
#Table(name="college")
class College implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name="college_id")
#GeneratedValue(strategy=GenerationType.IDENTITY)
private int collegeId;
#Column(name="name")
private String collegeName;
#ElementCollection
#CollectionTable(name="student", joinColumns=#JoinColumn(name="college_id"))
#Column(name="student_name")
private Set<String> students;
public College() {
}
public Set<String> getStudents() {
return students;
}
public void setStudents(Set<String> students) {
this.students = students;
}
public int getCollegeId() {
return collegeId;
}
public void setCollegeId(int collegeId) {
this.collegeId = collegeId;
}
public String getCollegeName() {
return collegeName;
}
public void setCollegeName(String collegeName) {
this.collegeName = collegeName;
}
#Override
public String toString() {
return "College [collegeId=" + collegeId + ", collegeName=" + collegeName + ", students=" + students + "]";
}
}
I don't think #Formula annotation supports collection it can only be applied for single-valued properties. Can't say if if there exists any tweak.

Java JPA: adding further objects (products) to a already saved entity (store)

I have a many-to-many relationship between Stores and Products, represented by the following code (mostly based in this answer):
#Entity
#Table(name = "Store")
public class Store {
private long idStore;
// ...
private Collection<StoreHasProduct> storeHasProducts = new ArrayList<>();
#OneToMany(mappedBy = "store", cascade = {CascadeType.MERGE, CascadeType.PERSIST}, fetch = FetchType.EAGER)
public Collection<StoreHasProduct> getStoreHasProducts() {
return storeHasProducts;
}
public void setStoreHasProducts(Collection<StoreHasProduct> storeHasProducts) {
this.storeHasProducts = storeHasProducts;
}
}
#Entity
#Table(name="Product")
public class Product {
private long idProduct;
// ...
private Collection<StoreHasProduct> storeHasProducts = new ArrayList<>();
#OneToMany(mappedBy = "product", fetch = FetchType.LAZY)
public Collection<StoreHasProduct> getStoreHasProducts() {
return storeHasProducts;
}
public void setStoreHasProducts(Collection<StoreHasProduct> storeHasProducts) {
this.storeHasProducts = storeHasProducts;
}
}
#Entity
#Table(name = "Store_has_Product")
#IdClass(StoreHasProductPK.class)
public class StoreHasProduct implements java.io.Serializable {
#Id
#ManyToOne
#JoinColumn(name = "Store_idStore",updatable = true)
private Store store;
#Id
#ManyToOne
#JoinColumn(name = "Product_idProduct", updatable = true)
private Product product;
}
public class StoreHasProductPK implements java.io.Serializable {
private Long store;
private Long product;
}
All basic insertion are working fine. However, when I try to add new Products to a existing Store I'm having a PersistentObjectException: detached entity passed to persist exception. This happens, for example, in the following test:
#Test
public void testAssignProductToAnExistingStore() throws Exception {
//Create a store
Store store = getStore();
//Create and save a product
Product product = getProduct();
StoreHasProduct storeHasProduct = getStoreHasProduct(store, product);
store.getStoreHasProducts().add(storeHasProduct);
storeRepository.save(store);
//Create and save a second product
Product productTwo = getProduct();
Store s = storeRepository.findOne(store.getIdStore());
product.getStoreHasProducts().add(getStoreHasProduct(s, productTwo));
productRepository.save(product);
// s.getStoreHasProducts().add(getStoreHasProduct(s, productTwo));
// storeRepository.save(s);
}
If I try to persist the product, I get detached entity passed to persist: Product. If instead I try to persist the store (commented code) I get the same exception but for store.
What should I do? I'm trying to use the CASCADE.DETACH, but I'm not sure if this is the appropriate path to follow.
Thanks
it's all about configuring Entity manager and/or Transaction manager
take a look
How to save a new entity that refers existing entity in Spring JPA?

Categories

Resources