JPA many to many relationship : zero or more - java

I'm new to JPA so I have a question about a many to many relationship (with a zero to more implementation) :
If you have a relationship like :
Like stated a product can excist without a order (normally they will be added as new products arrive) later on it can be used on 1 or more orders. An order must contain at least 1 or more products.
you must state the relationship
#Entity(name = "ORDERS")
public class Order {
#Id
#Column(name = "ORDER_ID", nullable = false)
#GeneratedValue(strategy = GenerationType.AUTO)
private long orderId;
#Column(name = "CUST_ID")
private long custId;
#Column(name = "TOTAL_PRICE", precision = 2)
private double totPrice;
#ManyToMany(fetch=FetchType.EAGER)
#JoinTable(name="ORDER_DETAIL",
joinColumns=
#JoinColumn(name="ORDER_ID", referencedColumnName="ORDER_ID"),
inverseJoinColumns=
#JoinColumn(name="PROD_ID", referencedColumnName="PROD_ID")
)
private List<Product> productList;
...............
The other attributes and getters and setters goes here
}
and
#Entity(name = "PRODUCT")
public class Product {
#Id
#Column(name = "PROD_ID", nullable = false)
#GeneratedValue(strategy = GenerationType.AUTO)
private long prodId;
#Column(name = "PROD_NAME", nullable = false,length = 50)
private String prodName;
#Column(name = "PROD_DESC", length = 200)
private String prodDescription;
#Column(name = "REGULAR_PRICE", precision = 2)
private String price;
#Column(name = "LAST_UPDATED_TIME")
private Date updatedTime;
#ManyToMany(mappedBy="productList",fetch=FetchType.EAGER)
private List<Order> orderList;
...............
The other attributes and getters and setters goes here
}
I wonder the zero to many relation is it still possible to just persist products who aren't linked (at the moment) to a order?
But when a order uses a product the orderlist in product should be updated and the productlist in orde also. How do I enforce this or does JPA this for me?

You can still persist a product without order.
You have to update the other side of the relationship by hand. JPA won't do that for you. (of course if you save one order and then refetch the product your collection of order will be updated)
EDIT
To explain second point:
Product persitentProduct = ... //some product
Order newOrder = new Order();
newOrder.getProducts().add(persitentProduct);
//at this point : persistentProduct.getOrders().contains(newOrder)==false
entityManager.persist(newOrder);
//at this point nothing has changed on the other side of the relationship:
// i.e. : persistentProduct.getOrders().contains(newOrder)==false

You would then get something like :
public class Order {
private List products;
...
public void addProduct(Product product) {
this.products.add(product);
if !(product.getOrders().contains(product) {
product.getOrders().add(this);
}
}
...
}
public class Product {
private List orders;
...
...
}
right? Or do I see this wrong

Related

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

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.

How to prevent duplicate entries Spring Jpa

I'm learning how Spring framework works and as an example I'm trying to save cities and countries which users can log using the API endpoints. However, I can't figure out how to prevent duplicate entries.
For example I'm adding 2 cities in a country using the endpoint (photo below) but in the Country table I get duplicate values. How can I prevent duplicate values ? Thanks in advance.
#Getter
#Setter
#Entity
#Table(name = "COUNTRY")
public class CntCountry {
#Id
#SequenceGenerator(name = "CntCountry", sequenceName = "CNT_COUNTRY_ID_SEQ")
#GeneratedValue(generator = "CntCountry")
private Long id;
#Column(name = "COUNTRY_NAME", length = 30, nullable = false)
private String countryName;
#Column(name = "COUNTRY_CODE", length = 30, nullable = false)
private String countryCode;
}
#Getter
#Setter
#Table(name = "CITY")
#Entity
public class CtyCity {
#Id
#SequenceGenerator(name = "CtyCity", sequenceName = "CTY_CITY_ID_SEQ")
#GeneratedValue(generator = "CtyCity")
private Long id;
#Column(name = "CITY_NAME", length = 30, nullable = false)
private String cityName;
#Column(name = "PLATE_NUMBER", length = 30, nullable = false)
private Long plateNumber;
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "FK_COUNTRY")
private CntCountry country;
}
EDIT:
#PostMapping("/city")
public ResponseEntity<CtyCityDto> save(#RequestBody CtyCitySaveRequestDto ctyCitySaveRequestDto){
CtyCityDto ctyCityDto = ctyCityService.save(ctyCitySaveRequestDto);
return ResponseEntity.ok(ctyCityDto);
}
#Service
#AllArgsConstructor
public class CtyCityService {
private CtyCityDao ctyCityDao;
public CtyCityDto save(CtyCitySaveRequestDto ctyCitySaveRequestDto){
CtyCity ctyCity = CtyCityMapper.INSTANCE.convertToCtyCity(ctyCitySaveRequestDto);
ctyCity = ctyCityDao.save(ctyCity);
CtyCityDto ctyCityDto = CtyCityMapper.INSTANCE.convertToCtyCityDto(ctyCity);
return ctyCityDto;
}
}
public interface CtyCityDao extends JpaRepository<CtyCity,Long> {
}
#Data
public class CtyCityDto {
private Long id;
private String cityName;
private Long plateNumber;
private CntCountry country;
}
I'm not really following your naming conventions, and I think your DTO classes are just complicating things for you at this point... But in general terms, because the entities you're sending have no id value associated with them, JPA assumes they are different objects and adds them to the database with new id's because it hasn't been told anywhere that similar items might in fact be the same object, it needs to be told.
I can think of 2 ways to prevent entity duplication in your database.
1. The easiest way would be to set your Country and City names (or other attributes) to be "unique", you can do this in your entity classes simply by adding unique = true to the column data on the item you wish to be unique.
//In Country.java
#Column(name = "COUNTRY_NAME", length = 30, nullable = false, unique = true)
private String countryName;
//In City.java
#Column(name = "CITY_NAME", length = 30, nullable = false, unique = true)
private String cityName;
Although, you will then need to handle exceptions thrown if a duplicate is provided, in Spring Boot the best way to handle this is with a #ControllerAdvice but that's another subject.
2. Check if the entity exists by name or some other value. A common approach might be something like the following:
//In your service
public Object saveCountry(Country country){
Country existingCountry = countryRepository.findByName(country.getName()).orElse(null);
if(existingCountry == null){
//Country does not already exist so save the new Country
return countryRepository.save(country);
}
//The Country was found by name, so don't add a duplicate
else return "A Country with that name already exists";
}
//In your Country repository
Optional<Country> findByName(countryName);
In case my answer doesn't make sense, I have thrown together an example following my first suggestion (using the unique column attribute and a controller advice) which you can view/clone from here

Hibernate n+1 issue with 3 entities

I have 3 entities. Category, Subcategory and Product
#Entity
#JsonInclude(value = JsonInclude.Include.NON_EMPTY)
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String title;
private String picture;
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL)
#JsonIgnore
private List<Subcategory> subcategories;
//getters and setters...
#JsonProperty("productCount")
private int getProductCount() {
//Counting products
int productCount = 0;
//!!!!!
//My problem starts here!
for (final Subcategory subcategory : subcategories) {
productCount += subcategory.getProducts().size();
}
return productCount;
}
}
#Entity
#JsonInclude(JsonInclude.Include.NON_EMPTY)
public class Subcategory {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String title;
#ManyToOne
#JoinColumn(name = "parent_category_id", nullable = false)
#JsonIgnore
private Category category;
#OneToMany(mappedBy = "subcategory", cascade = CascadeType.ALL)
private List<Product> products;
//getters and setters
}
#Entity
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String title;
private String unit;
private String icon;
#ManyToOne
#JoinColumn(name = "subcategory_id", nullable = false)
#JsonIgnore
private Subcategory subcategory;
//getters and setters
}
They are related to each other, but when I want to count the number of products in each category, about 120 queries are executed (depending on the number of subcategories and products)
I was able to reduce it to around 60 queries by adding #EntityGraph to my category repository:
#EntityGraph(type = EntityGraph.EntityGraphType.FETCH, attributePaths = {"subcategories"})
However 60 query is still too much. I can't add subcategories.products to this entity graph annotation because that causes org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags
I can suppress this exception by changing the data type of products to Set from List but that creates a Cartesian product and makes the performance worse (it returns around 18,000 rows).
How can I fix this issue without creating a Cartesian product?

Orders Products and Order_Line Hibernate Relationship with Composite Keys

Ok so today I have spent all day trying to figure out how to map a relationship between Orders, Products, and OrderLine with hibernate using annotations all java config.
I want OrderLine to be a junction table with extra fields, that joins Products and Orders.
Orders has a OneToMany relationsbhip with OrderLine - One order has many order lines.
Each Order has one or more OrderLines, Each OrderLine has one Order, Each OrderLine has one Product, Each Product can have 0 or more OrderLines.
From what I have been following along with tutorials, there are two ways to do it. One being with #Embeddable, #EmbeddedId annotations and the other being #IdClass annotations, I have tried both with no success I will post my code for my shot at #IdClass method.
My orders class
#Entity
#Table(name="orders")
public class Order {
#Id
#Column(name = "order_id")
#OneToMany(fetch = FetchType.EAGER, mappedBy = "order")
private Set<OrderLine> orderLines = new HashSet<>();
...some extra properties relevant to all orders
public Order(){}
...setters/getters
}
My Products class
#Entity
#Table(name="products")
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "product_id")
public Integer product_id; // primary key
#OneToMany(fetch = FetchType.EAGER, mappedBy = "product")
private Set<OrderLine> orderLines = new HashSet<>();
...other properties
public Product() {
}
...setters/getters
}
Here is my OrderLine Class
#Entity
#IdClass(OrderLineId.class)
#Table(name="order_line")
public class OrderLine implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "product_id", insertable= false, updatable= false)
public Integer product_id;
#Id
#Column(name = "order_id", insertable= false, updatable= false)
public Integer order_id;
#ManyToOne
#JoinColumn(name = "product_id", insertable = false, updatable = false)
private Product product;
#ManyToOne
#JoinColumn(name = "order_id", insertable = false, updatable = false)
private Order order;
#Column(name = "product_quantity", unique = false, nullable = false)
private int productQuantity;
...additional fields associated with each OrderLine
And finally my OrderLineId implementation
public class OrderLineId implements Serializable {
private static final long serialVersionUID = 1L;
private Integer order_id;
private Integer product_id;
public OrderLineId(){}
public OrderLineId(Integer order_id, Integer product_id) {
this.order_id = order_id;
this.product_id = product_id;
}
#Column(name = "order_id")
public Integer getOrder() {
return this.order_id;
}
public void setOrder(Integer order_id) {
this.order_id = order_id;
}
#Column(name = "product_id")
public Integer getProduct() {
return this.product_id;
}
public void setProduct(Integer product_id) {
this.product_id = product_id;
}
#Override
public int hashCode() {
return order_id + product_id;
}
#Override
public boolean equals(Object obj) {
if(obj instanceof OrderLineId){
OrderLineId orderLineId = (OrderLineId) obj;
return orderLineId.order_id == order_id && orderLineId.product_id == product_id;
}
return false;
}
}
Here is the exception I am getting
MySQLSyntaxErrorException: Unknown column 'orderlines0_.order' in 'field list'
when I visit the end point below
#RequestMapping(value = "/testorder", method = RequestMethod.GET)
public ModelAndView testOrder(){
Order order = orderService.findOrderById(23);
Product product = productService.findProduct(2);
OrderLine ol = new OrderLine(product, order);
ol.setSize("large");
ol.setProductQuantity(30);
orderService.saveOrderLine(ol);
return null;
}
Please can somebody help me, this is driving me crazy....
Thank you
After going through your question and code, I think you have got your design wrong.
You say
want OrderLine to be a junction table with extra fields, that joins Products and Orders.
However you have only two variables, Order and Product in your OrderLine Class.
If I am not wrong, What you really need is a many-to-many table.
Your table order_line would contain two columns order_id and product_id, having foreign key to table orders and products respectively.
Your Order class would be:
#Entity
#Table(name="orders")
public class Order {
#Id
//id of table Order
#ManyToMany(
targetEntity = Product.class,
cascade = CascadeType.DETACH, fetch = FetchType.LAZY)
#JoinTable(name = "order_line",
joinColumns =
#JoinColumn(name = "order_id", nullable = false, updatable = false),
inverseJoinColumns =
#JoinColumn(name = "product_id", nullable = false, updatable = false))
public Set<Product> products= new HashSet(0);
...some extra properties relevant to all orders
public Order(){}
...setters/getters
}
Your Product Class would look like:
#Entity
#Table(name="products")
public class Product implements Serializable {
#Id
#Column(name = "product_id")
public Integer product_id; // primary key
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "products", targetEntity = Order.class) private Set<Order> order = new HashSet<>();
...other properties
public Product() {
}
...setters/getters
}
As you can see, from Order entity you can get 'products' and from Product entity you can get 'orders'. This should server your purpose. No need to use 'OrderLine' class.
Other major errors in code:
- #Id is used to represent Primary key. You have used #Id multiple times for single class.
- In 'Order' class #Id given to Set, and #Id is used along with #OneToMany, which won't work.
Code provided would help you if, indeed what you need is many-to-many table.
I got it to work on my last try last night by doing this change
in OrderLine I removed the product_id and order_id fields and placed the #Id annotation over both ManyToOne relationships like so
#Entity
#IdClass(OrderLineId.class)
#Table(name="order_line")
public class OrderLine implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#ManyToOne
#JoinColumn(name = "product_id", insertable = false, updatable = false)
private Product product;
#Id
#ManyToOne
#JoinColumn(name = "order_id", insertable = false, updatable = false)
private Order order;
#Column(name = "product_quantity", unique = false, nullable = false)
private int productQuantity;
...additional fields associated with each OrderLine
I did not think this woudl work but correct table with correct values was updated, is this valid?
Thank you
Ok I figured it out - I ended up using embeddable method instead based on this guys excellent tutorial here
He goes through 3 major ways to implement many to many assosiations - a simple junction only table, a junction table (with additional fields) which is the method I needed to implement, and finally a less popular method where the class is mapped as a component.

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