How to retrieve data from parent-child tables using Spring Data JPA? - java

In my Spring Boot app, I use Hibernate and applied the necessary relations to the following entities properly.
#Entity
public class Recipe {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable=false, length=50)
private String title;
#OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL)
private List<RecipeIngredient> recipeIngredients = new ArrayList<>();
}
#Entity
public class RecipeIngredient {
#EmbeddedId
private RecipeIngredientId recipeIngredientId = new RecipeIngredientId();
#ManyToOne(optional = true, fetch = FetchType.LAZY)
#MapsId("recipeId")
#JoinColumn(name = "recipe_id", referencedColumnName = "id")
private Recipe recipe;
#ManyToOne(optional = true, fetch = FetchType.LAZY)
#MapsId("ingredientId")
#JoinColumn(name = "ingredient_id", referencedColumnName = "id")
private Ingredient ingredient;
}
#Entity
public class Ingredient
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(unique=true, nullable=false, length=50)
#EqualsAndHashCode.Include
private String name;
#OneToMany(mappedBy = "ingredient", cascade = CascadeType.ALL)
private Set<RecipeIngredient> recipeIngredients = new HashSet<>();
}
Now I am trying to retrieve data by merging related entities. For example, when retrieving a Recipe, I also need to retrieve all Ingredients belonging to this Recipe.
As far as I know, I can use Projection and maybe it is better to only use Hibernate features and retrieve related table data via Java Stream. I have no idea how should I retrieve data via Hibernate.
Suppose that I just need an Optional<Recipe> that has List<Ingredient>. Then, I probably need a DTO class something like that:
#Data
public class ResponseDTO {
private Long id;
private String title;
List<RecipeIngredient> ingredients;
// getter, setter, constructor
}
So, how should I populate this DTO with the requested Recipe and corresponding Ingredient data (getting Ingredient names besides id values) using Java Stream?
Or if you suggest Projection way, I tried it but the data is multiplied by the ingredient count belonging to the searched recipe.
Update:
#Getter
#Setter
#NoArgsConstructor
public class ResponseDTO {
private Long id;
private String title;
List<IngredientDTO> ingredientDTOList;
public ResponseDTO(Recipe recipe) {
this.id = recipe.getId();
this.title = recipe.getTitle();
this.ingredientDTOList = recipe.getRecipeIngredients().stream()
.map(ri -> new IngredientDTO(ri.getIngredient().getName()))
.toList();
}
}
#Getter
#Setter
public class IngredientDTO {
private Long id;
private String name;
public IngredientDTO(String name) {
this.name = name;
}
}

First, in the ResponseDTO you will need you change the type of ingredients from List<RecipeIngredient> to List<Ingredient>.
To manually perform the mapping, you should use (to map from a suppose Recipe recipe to a RespondeDTO response):
ResponseDTO recipeToResponseDTO(Recipe recipe) {
ResponseDTO response = new ResponseDTO();
response.setId(recipe.getId());
response.setTitle(recipe.getTitle());
response.setIngredients(recipe.recipeIngredients.stream()
.map(RecipeIngredient::getIngredient()
.collect(Collectors.toList());
return response;
}
On the other hand, to model a n-n relation, I encourage you to use the approach proposed by E-Riz in the comment.

Related

Spring-boot: Join table with #OneToMany annotation return empty list

I recently started learning Spring Boot and I have a problem. I have two tables (Categories, subcategories) with a one to many relationship. I am having trouble creating this relationship. When requesting the output of all categories, the list of subcategories is empty, but in the database tables are created correctly.
Please tell me what can be wrong.
#Entity
#Table(name ="subcategory")
#Data
public class SubCategoryEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable=false)
private String name;
#ManyToOne( targetEntity = CategoryEntity.class)
#JoinColumn(name = "category_id")
private CategoryEntity category;
}
#Entity
#Table(name = "category")
#Data
public class CategoryEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable=false)
private String name;
#Column(nullable=false)
private String img;
#OneToMany(mappedBy = "category", fetch = FetchType.EAGER,
cascade = CascadeType.ALL, targetEntity = SubCategoryEntity.class)
private Set<SubCategoryEntity> subcategories;
}
#GetMapping("/categories")
public List<CategoryEntity> getCategories() {
return categoryRepository.findAll();
}
Attempt to get data:
Here I tried to get a list of categories and subcategories, but the list of subcategories is empty, although it is not.

How to get object from OneToMany collection of objects?

I have an Order entity and OrderProduct. I want to show order details on frontend and of course order products in it. So how to fetch product object in OrderProduct JSON. I'm missing product object in products array. I don't need order object one more time and i think it going to be a infinite recursion stuff with it. :)
My Order entity:
#Entity
#Getter
#Setter
#Table(name ="orders")
public class Order{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
private BigDecimal totalPrice;
#OneToMany(mappedBy = "order", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JsonManagedReference(value="orders")
private List<OrderProduct> products = new ArrayList<>();
private int userId;
#DateTimeFormat(pattern="dd/MM/yyyy")
private Date date = new Date();
#DateTimeFormat(pattern="dd/MM/yyyy")
private Date deliveryDate;
#Enumerated(EnumType.STRING)
private OrderType orderType;
}
My OrderProduct entity:
#Entity
#Setter
#Getter
public class OrderProduct {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne(fetch = FetchType.EAGER)
#JsonBackReference(value="product")
#JoinColumn(name = "product_id")
private Product product;
#ManyToOne
#JsonBackReference(value="orders")
#JoinColumn(name = "order_id")
private Order order;
private Integer quantity;
}
Product entity:
#Entity
#Getter
#Setter
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(unique = true)
private String name;
private double price;
#OneToMany(mappedBy = "product", cascade = CascadeType.ALL)
#JsonManagedReference(value="ingredients")
private List<Ingredient> ingredients = new ArrayList<>();
#OneToMany(mappedBy = "product",fetch = FetchType.EAGER)
#JsonManagedReference(value="product")
private List<OrderProduct> products = new ArrayList<>();
private String fileName;
}
This can help annotate one of your entity clases with
#JsonIdentityInfo(
property = "id",
generator = ObjectIdGenerators.PropertyGenerator.class
)
Every time when JSON serialization go in circles object data will be replaced with object id or orher field of entity for your choose.
You can use #JsonViewannotation to define the fields that you need to serialize to JSON
How it works:
You need define class with interfaces. For example:
public class SomeView {
public interface id {}
public interface CoreData extends id {}
public interface FullData extends CoreData {}
}
Mark entity fields with #JsonView(<some interface.class>)
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView(SomeView.id.class)
private Long id;
#Column(nullable = false)
#JsonView(SomeView.CoreData.class)
private String username;
#Column(nullable = false)
#JsonView(SomeView.FullData.class)
private String email;
}
Annotate endpoint with #JsonView(<some interface.class>)
#GetMapping()
#JsonView(<some interface.class>)
public User getUser() {
return <get user entity somwhere>
}
In case #JsonView(SomeView.id.class) you will get this JSON:
{
id: <some id>
}
In case #JsonView(SomeView.CoreData.class):
{
id: <some id>,
username: <some username>
}
In case #JsonView(SomeView.FullData.class):
{
id: <some id>,
username: <some username>,
email: <some email>
}
#JsonView also works with embeded objects and you can annotate one field with multiply views classes - #JsonView({SomeView.FullData.class, SomeOtherView.OtherData.class})
In your case i think you should annotate all the fields you need except:
#OneToMany(mappedBy = "product",fetch = FetchType.EAGER)
#JsonManagedReference(value="product")
private List<OrderProduct> products = new ArrayList<>();
in Product
to avoid circular serialization
Or as alternative you can just use DTO classes or seralize oject to JSON manualy (https://thepracticaldeveloper.com/java-and-json-jackson-serialization-with-objectmapper/)
This can be done by my library beanknife
// This configure generate a class named ProductInfo which has the same shape with Product without property "products"
#ViewOf(value = Product.class, genName="ProductInfo", includePattern = ".*", excludes = {"products"})
class ProductInfoConfigure {}
// This configure generate a class named OrderProductRelation with the same shape of OrderProduct.
// But it has not order property and the type of its product property is change to ProductInfo generated above.
#ViewOf(value = OrderProduct.class, genName="OrderProductRelation", includePattern = ".*", excludes = {"order"})
class OrderProductRelationConfigure {
#OverrideViewProperty("product")
private ProductInfo product;
}
// This configure generate a class named OrderDetail with the same shape of Order.
// But the type of its products property is change to List<OrderProductRelation>
#ViewOf(value = Order.class, genName="OrderDetail", includePattern = ".*")
class OrderDetailConfigure {
#OverrideViewProperty("products")
private List<OrderProductRelation> products;
}
will generate these classes:
class ProductInfo {
private Long id;
private String name;
private double price;
private List<Ingredient> ingredients; // it is not processed because you have not provide the class Ingredient
private String fileName;
}
public class OrderProductRelation {
private Long id;
private ProductInfo product;
private Integer quantity;
}
public class OrderDetail {
public Long id;
private BigDecimal totalPrice;
private List<OrderProductRelation> products;
private int userId;
private Date date = new Date();
private Date deliveryDate;
private OrderType orderType;
}
Then
Order order = ...
OrderDetail orderDetail = OrderDetail.read(order);
// serialize the otherDetail instead of order.
List<Order> orders = ...
List<OrderDetail> orderDetails = OrderDetail.read(orders);
// serialize the orderDetails instead of orders.
Possible problems:
I doesn't use Lombok, so Lombok may need to be adapted because it change the byte code on the fly. But it is not a big problem, I will try to adapt it if someone commit the issue and provide enough use cases.
The generated class does not inherit the annotation on the original class. In next release I will provide a sulotion. At this moment, as a workaround, we can use custom method to convert the property manually. such as
#ViewOf(value = Order.class, genName="OrderDetail", includePattern = ".*")
class OrderDetailConfigure {
#OverrideViewProperty("products")
private List<OrderProductRelation> products;
#OverrideViewProperty("orderType")
public static String orderType(Order source) {
return source.getOrder().name();
}
}
The generated class will be changed to
public class OrderDetail {
public Long id;
private BigDecimal totalPrice;
private List<OrderProductRelation> products;
private int userId;
private Date date = new Date();
private Date deliveryDate;
private String orderType;
}
Update
Version 1.2.0 released. Add support of annotation inheritance.
#ViewOf(value = Order.class, genName="OrderDetail", includePattern = ".*")
#UseAnnotation({DateTimeFormat.class, Enumerated.class, JsonProperty.class})
class OrderDetailConfigure {
#OverrideViewProperty("products")
private List<OrderProductRelation> products;
}
generate
public class OrderDetail {
public Long id;
private BigDecimal totalPrice;
private List<OrderProductRelation> products;
private int userId;
#DateTimeFormat(pattern="dd/MM/yyyy")
private Date date;
#DateTimeFormat(pattern="dd/MM/yyyy")
private Date deliveryDate;
#Enumerated(EnumType.STRING)
private OrderType orderType;
}

#GetMapping used to retrive item gives a responce of infinite loop of foreignkey object

i am new in spring boot and i could not find solution for this for a day now.
#GetMapping used to retrive item gives a responce of infinite loop of foreignkey object "user".
why am i getting this infinite loop?
how to fix it?
user object in infinite loop(the problem)
result that i want
item entity
#Entity
public class Item{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long ItemId;
#ManyToOne
#JoinColumn(name = "owner_id")
private User user;
private String ItemName;
// #Column(columnDefinition="text")
private String Description;
private double Price;
private int AvailableQuantity;
private double shippingWeight;
// #Transient
// private MultipartFile Picture;
#Enumerated(value = EnumType.STRING)
private Category category;
#OneToMany(mappedBy = "item")
#JsonIgnore
private List<CartItem> CartItemList;
}
user entity
#Entity
#Table(name = "Utilisateur")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idU;
private String username;
private String password;
private String firstname;
private String lastname;
private String gender;
private Long phone;
private String adress;
#Temporal(TemporalType.DATE)
private Date dateofbirth;
private int rating;
private String email;
public Role role;
private Integer status;
#OneToMany(mappedBy = "user")
private List<Item> ItemList;
}
item service
#Service
public class ItemService implements ItemServiceInterface{
#Autowired
ItemRepository itemrepository;
public Optional<Item> getItemById(long id){
return itemrepository.findById(id);
}
}
item controller
#RestController
public class ItemControl {
#Autowired
ItemServiceInterface itemservice;
#GetMapping("/getitem/{id}")
public Optional<Item> getitembyid(#PathVariable Long id) {
return itemservice.getItemById(id);
}
}
You can use combination of #JsonManagedReference and #JsonBackReference to discourage Jackson from infinite serialization.
#Entity
#Table(name = "Utilisateur")
public class User {
// omitted
#JsonManagedReference
#OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Item> ItemList;
}
#Entity
public class Item{
// omitted
#JsonBackReference
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "owner_id")
private User user;
}
More details could be found here https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion
You can make use of lazy loading to cut the dependency loop between user and item. However, following that approach might potentially affect other parts of your projects because other codes might use the entity with an assumption that item list in user entity is already eager fetched.
A better way is not return the entity object directly to the REST response. You can define a data model for the rest response and convert the entity to that model in your service class. This way, you can completely control what to return and not to.
Another approach if you still want to use the entity as response: https://www.baeldung.com/spring-data-jpa-named-entity-graphs. This way, you can define when to use the lazy load with each specific query.

Save object with OneToMany from JSON to database

I'm trying to send the following JSON to a REST API and persist on database, but only the Product is created, the Image it is not.
{"name":"pen",
"description":"red pen",
"images":[{"type":"jpeg"}]
}
#Controller
#POST
#Path("/product/add")
#Consumes("application/json")
public Response addProduct(Product product) {
service.createProduct(product);
}
#Service
#Autowired
private ProductDAO productDAO;
#Autowired
private ImageDAO imageDAO;
public void createProduct(Product product) {
productDAO.save(product);
}
#Product
#Entity
#Table
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer productId;
#Column(name = "NAME")
private String name;
#Column(name = "DESCRIPTION")
private String description;
#OneToMany(cascade=CascadeType.ALL, fetch = FetchType.EAGER, mappedBy="product")
private Set<Image> images;
#OneToMany(cascade=CascadeType.ALL, fetch = FetchType.EAGER, mappedBy="parent")
private Set<Product> children;
#ManyToOne
#JoinColumn(name = "PARENT_PRODUCT_ID")
private Product parent;
#Image
#Entity
#Table
public class Image implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer imageId;
#Column(name = "TYPE")
private String type;
#ManyToOne
#JoinColumn(name = "PRODUCT_ID", nullable = false)
private Product product;
At the #POST method, when print the Product object received, this is what returns:
Product [productId=null, name=pen, description=red pen, images=[Image [id=null, type=jpeg, product=null]], children=null, parent=null]
The correct way is to first persist the Product, and then persist the Image or the Hibernate can automatically persist the Image when I persist the Product?
Hibernate takes care of persisting your child entities if your bidirectional mapping is correctly implemented and you have set proper relationships between your entity objects.
You have a Product entity that has a collection of Image. Product entity is the parent entity here. You can simply set proper relations between Product and Image entities and persist only Product. Hibernate will persist your parent as well as your child entities.
What you need to do
Product product = new Product();
product.setName("PRODUCT_NAME");
Set<Image> productImages = new HashSet<>();
Image productProfileImage = new Image();
productProfileImage.setType("PROFILE");
productProfileImage.setProduct(product);
//..set other fields
productImages.add(productProfileImage);
Image productCoverImage = new Image();
productCoverImage.setType("COVER");
productCoverImage.setProduct(product);
//..set other fields
productImages.add(productCoverImage);
product.setImages(productImages);
productRepository.save(product); //Persist only your product entity and the mapped child entities will be persisted
Check out this similar answer.
PS: I have not tested the code but this should work.

Persisting a #ManyToOne-referenced object only if it does not exist

I'm fairly new to Spring/JPA so this is somewhat a trivial question.
I have two entities with a many-to-one relationship: Item and ItemType. Basically, ItemType simply represents a unique name for a set of Items. I use a CrudRepository<Item, Long> to store them. The relevant code is as follows (getters/setters/equals()/hashCode() omitted):
#Entity
public class Item {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinColumn(name = "type_id")
private ItemType itemType;
public Item() {}
public Item(ItemType itemType) {
this.itemType = itemType;
}
}
#Entity
public class ItemType {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(unique = true, nullable = false)
private String name;
public ItemType() {}
public ItemType(String name) {
this.name = name;
}
}
#Controller
public class ItemsController {
#Autowired private ItemsRepo itemsRepo;
#RequestMapping(value = "/item", method = RequestMethod.POST)
#ResponseBody
public Item addQuestionSet(#RequestBody Item item) {
return itemsRepo.save(item);
}
}
When I insert a new Item into the database, I want it to get a type_id from either an ItemType with the given name if it already exists, or from a newly persisted ItemType otherwise.
As of now, I naturally get an exception when trying to insert the second item with the same type:
org.hsqldb.HsqlException: integrity constraint violation: unique constraint or index violation
I could probably make a boilerplate check in my controller before saving a new item into repository. But this task is rather generic, I'm pretty sure there must be a convenient solution in JPA.
Thanks.
It seems you are persist() method on the Item object rather than merge() method. I hope it will resolve your query.
I can see that the problem is when you "persist", try with "lazy" type. You could get the data only when you need it and EAGER always.
I can give you an example how i do it
this is my class "CentroEstudio"
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "idCentroEstudio",nullable=false)
private Long idCentroEstudio;
#ManyToOne(fetch = FetchType.EAGER,cascade=CascadeType.ALL)
#JoinColumn(name = "idTipoCentroEstudio", nullable = false)
private TipoCentroEstudio tipoCentroEstudio;
#Column(name="nombre",nullable=false)
private String nombre;
#Column(name="activo",nullable=false)
private boolean activo;
this is my class "TipoCentroEstudio"
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="idTipoCentroEstudio",nullable=false)
private Long idTipoCentroEstudio;
#Column(name="descripcion",nullable=false)
private String descripcion;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "tipoCentroEstudio")
private Set<CentroEstudio> centroEstudio = new HashSet<CentroEstudio>(0);
I'm sorry for the Spanish in the example, but I'm peruvian and I speak Spanish.
I hope this helps you ...

Categories