I am having some catalogs, under these catalogs there are some categories some catalogs doesn't have any categories at all and finally there are products under various categories or catalogs.
for example catalog Household will have a category Kitchen which then will have products like oven, plates, dishwasher etc while product bed-sheets will directly belong to catalog households and not to any category
same way catalog chemicals will not have any category and will only have products.
below things are certain
i) All the products belongs to some catalogs
ii) some catalogs will have categories and then categories will have products
iii) some products will only belong to catalogs and never associate with any category
I have come up with below Model classes structure but I am not sure that How they will handle the above case , can you please suggest something ?
#Entity
public class Catalogs {
#OneToMany(CascadeType.All, orphanRemoval = true)
#JoinColumn(name = "catalogs_id")
private Set<Categories> categories;
/* rest of the code will go here */
}
my categories class will be like this
#Entity
public class Categories {
#OneToMany(CascadeType.All, orphanRemoval = true)
#JoinColumn(name = "categories_id")
private Set<Products> products;
}
finally my Products class will go like this
#Entity
public class Products {
/* Products related codes */
}
or should I also have a field for products in my catalog class ?
like below
#Entity
public class Catalogs {
#OneToMany(CascadeType.All, orphanRemoval = true)
#JoinColumn(name = "catalog_id")
private Set<Categories> categories;
#OneToMany(CascadeType.All, orphanRemoval = true)
#JoinColumn(name = "catalog_id")
private Set<Products> products;
/* rest of the code will go here */
}
According to your requirement, I got an approach in my mind. If I start from Catalog, I would consider that class as root. Then I would design that class as
#Entity
public class Catalogs {
// just catalog name or other activity. not mapping
}
After that one, I would design category class like this, where every category would be under a catalog.
#Entity
public class Categories {
// other attribute...
#ManyToOne
#JoinColumn(name = "catalog_id")
private Catalogs catalogs;
}
After these, I would design my product class like this. As you said one product may be under catalog or maybe under a category. So I made some attribute nullable.
#Entity
public class Products {
//other attributes....
#ManyToOne
#JoinColumn(name = "catalog_id")
#Nullable
private Catalogs catalogs;
#ManyToOne
#JoinColumn(name = "category_id")
#Nullable
private Categories categories;
}
Check this approach. And response about your opinion.
Related
I am getting this A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance error with my oneToMany relationship when trying to update my child element (report). Although I see this question asked a few times here, I haven't been able to make my code to work with them and I now feel it may be an issue with me using Lombok perhaps, since most of the answers here mention about changes on the hashcode and equals methods, which are abstracted away by Lombok? I tried to remove Lombok to try without it but then I got a bit confused on what to do next. If I could get some guidance on how to fix this issue within my original Lombok implementation please.
#Entity
#Table(name = "category")
#AllArgsConstructor
#NoArgsConstructor
#Data
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
#Column(name = "category_title", nullable = false)
private String title;
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> report;
public Category(UUID id, String title) {
this.id = id;
this.title = title;
}
}
#NoArgsConstructor
#AllArgsConstructor
#Entity
#Table(name = "report")
#Data
public class Report {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
#Column(name = "report_title", nullable = false)
private String reportTitle;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.MERGE)
#JoinColumn(name = "category_id", nullable = false)
private Category category;
public Report(UUID id) {
this.id = id;
}
}
#Override
public ReportUpdateDto updateReport(UUID id, ReportUpdateDto reportUpdateDto) {
if (reportRepository.findById(id).isPresent()) {
Report existingReport = reportRepository.findById(id).get();
existingReport.setReportTitle(reportUpdateDto.getTitle());
Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();
Category category = new Category(existingCategory.getId(), existingCategory.getTitle());
existingReport.setCategory(category); // This is needed to remove hibernate interceptor to be set together with the other category properties
Report updatedReport = reportRepository.save(existingReport);
updatedReport.setCategory(category); // This is needed to remove hibernate interceptor to be set together with the other category properties
ReportUpdateDto newReportUpdateDto = new ReportUpdateDto(updatedReport.getId(),
updatedReport.getReportTitle(), updatedReport.getCategory());
return newReportUpdateDto;
} else {
return null;
}
}
Thank you very much.
Fast solution (but not recommended)
The error of collection [...] no longer referenced arrises in your code beacuse the synchronization between both sides of the bidiretional mapping category-report was just partially done.
It's important to note that binding the category to the report and vice-versa is not done by Hibernate. We must do this ouserselves, in the code, in order to sync both sides of the relationship, otherwise we may break the Domain Model relationship consistency.
In your code you have done half of the synchronization (binding the category to the report):
existingReport.setCategory(category);
What is missing is the binding of the report to the category:
category.addReport(existingReport);
where the Category.addReport() may be like that:
public void addReport(Report r){
if (this.report == null){
this.report = new ArrayList<>();
}
this.report.add(r);
}
Recommended Solution - Best practice for synchronizing both sides of the mapping
The suggested code above works, but it is error prone as the programmer may forget to call one of the lines when updating the relationship.
A better approach is to encapsulate that sychronization logic in a method in the owning side of the relationship. And that side is the Category as stated here: mappedBy = "category".
So what we do is to encapsulate in the Category.addReport(...) all the logic of cross-reference between Category and Report.
Considering the above version of addReport() method, what is missing is adding r.setCategory(this).
public class Category {
public void addReport(Report r){
if (this.reports == null){
this.reports = new ArrayList<>();
}
r.setCategory(this);
this.reports.add(r);
}
}
Now, in the updateReport() it is enough to call the addReport() and the commented line bellow can be deleted:
//existingReport.setCategory(category); //That line can be removed
category.addReport(existingReport);
It is a good practice including in Category a removeReport() method as well:
public void removeReport(Report r){
if (this.reports != null){
r.setCategory = null;
this.reports.remove(r);
}
}
That is the code of Category.java after the two methods were added:
public class Category {
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> reports;
//Code ommited for brevity
public void addReport(Report r){
if (this.reports == null){
this.reports = new ArrayList<>();
}
r.setCategory(this);
this.reports.add(r);
}
public void removeReport(Report r){
if (this.reports != null){
r.setCategory = null;
this.reports.remove(r);
}
}
}
And the code for updating a report category now is this:
public ReportUpdateDto updateReport(UUID id, ReportUpdateDto reportUpdateDto) {
if (reportRepository.findById(id).isPresent()) {
Report existingReport = reportRepository.findById(id).get();
existingReport.setReportTitle(reportUpdateDto.getTitle());
Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();
existingCategory.addReport(existingReport);
reportRepository.save(existingReport);
return new ReportUpdateDto(existingReport.getId(),
existingReport.getReportTitle(), existingReport.getCategory());
} else {
return null;
}
}
A good resource to see a practical example of synchronization in bidirectional associations: https://vladmihalcea.com/jpa-hibernate-synchronize-bidirectional-entity-associations/
Lombok and Hibernate - not the best of the combinations
Though we can not blame Lombok for the error described in your question, many problems may arrise when using Lombok alongside with Hibernate:
Properties being loaded even if marked for lazy loading...
When generating hashcode(), equals() or toString() using Lombok, the getters of fields marked as lazy are very likelly to be called. So the programmer's initial intention of postponing some properties loading will no be respected as they will be retrieved from the database when one of hascode(), equals() or toString() is invoked.
In the best case scenario, if a session is open, this will cause additional queries and slow down your application.
In the worst case scenarios, when no session is available, a LazyInitializationException will be thrown.
Lombok's hashcode()/equals() affecting the bevahior of collections
Hibernate uses hascode() and equals() logic to check if a object is order to avoid inserting the same object twice. The same applies to removing from a list.
The way Lombok generates the methods hashcode() and equals() may affect hibernate and create inconsistent properties (especially Collections).
See this article for more info on this subject: https://thorben-janssen.com/lombok-hibernate-how-to-avoid-common-pitfalls/
Lombok/Hibernate integration in a nutshell
Don't use Lombok for entity classes. Lombok annotations you need to avoid are #Data, #ToString, and #EqualsAndHashCode.
Off-topic - Beware of delete-orphan
In Category, the #OneToMany mapping is defined with orphanRemoval=true as bellow:
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> reports;
The orphanRemoval=true means that when deleting a category, all the reports in that category will be deleted as well.
It is important to assess if that is the desired behavior in your application.
See an example of the SQLs hibernate will execute when calling categoryRepository.delete(category):
//Retrieving all the reports associated to the category
select
report0_.category_id as category3_1_0_,
report0_.id as id1_1_0_,
report0_.id as id1_1_1_,
report0_.category_id as category3_1_1_,
report0_.report_title as report_t2_1_1_
from
report report0_
where
report0_.category_id=?
//Deleting all the report associated to the category (retrieved in previous select)
delete from
report
where
id=?
//Deleting the category
delete from
category
where
id=?
Just an update based on the accepted answer to avoid a StackOverflow and circular loop that came up after the changes.
I had to create a new Category object to remove the reports inside it within my return dto, otherwise as the category contains that same report, that again contains that category and so on, the infinite loop could be seen on my response.
#Override
public ReportUpdateDto updateReport(UUID id, ReportUpdateDto reportUpdateDto) {
if (reportRepository.findById(id).isPresent()) {
Report existingReport = reportRepository.findById(id).get();
existingReport.setReportTitle(reportUpdateDto.getTitle());
Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();
Category category = new Category(existingCategory.getId(), existingCategory.getTitle());
existingCategory.addReport(existingReport);
reportRepository.save(existingReport);
return new ReportUpdateDto(existingReport.getId(),
existingReport.getReportTitle(), existingReport.getRun_date(),
existingReport.getCreated_date(), category);
} else {
return null;
}
}
So added this part:
Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();
Category category = new Category(existingCategory.getId(), existingCategory.getTitle());
existingCategory.addReport(existingReport);
As if I have something like
Category category = new Category(existingCategory.getId(), existingCategory.getTitle(), existingCategory.getReports);
I can see the issue once again, which is what the existingCategory object itself contains.
And here my final entities
#NoArgsConstructor
#AllArgsConstructor
#Entity
#Table(name = "report")
#Data
public class Report {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
#Column(name = "report_title", nullable = false)
private String reportTitle;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.MERGE)
#JoinColumn(name = "category_id", nullable = false)
private Category category;
#Entity
#Table(name = "category")
#AllArgsConstructor
#NoArgsConstructor
#Data
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
#Column(name = "category_title", nullable = false)
private String title;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> reports;
public Category(UUID id, String title) {
this.id = id;
this.title = title;
}
public void addReport(Report r) {
if (this.reports == null) {
this.reports = new ArrayList<>();
}
r.setCategory(this);
this.reports.add(r);
}
public void removeReport(Report r) {
if (this.reports != null) {
r.setCategory(null);
this.reports.remove(r);
}
}
}
This is my Product entity class:
public class Product extends BaseEntity {
#Column
#ManyToMany()
private List<Customer> customers = new ArrayList<>();
#ManyToOne
private Supplier supplier;
}
And this is my Customer entity class:
public class Customer extends BaseEntity {
//Enum type to String type in database '_'
#Enumerated(EnumType.STRING)
#Column
private Type type;
#Column
#ManyToMany(targetEntity = Product.class)
private List<Product> products = new ArrayList<>();
}
When I run my Spring boot project, it creates 2 separate tables in my database(Mysql): product_customer and customer_product but I need only one. What can I do to solve this?
Update your classes as follows:
public class Product {
#ManyToMany
#JoinTable(name="product_customer"
joinColumns=#JoinColumn(name="product_id"),
inverseJoinColumns=#JoinColumn(name="customer_id")
)
private List<Customer> customers = new ArrayList<>();
...
}
public class Customer extends BaseEntity {
#ManyToMany
#JoinTable(name="product_customer"
joinColumns=#JoinColumn(name="customer_id"),
inverseJoinColumns=#JoinColumn(name="product_id")
)
private List<Product> products = new ArrayList<>();
...
}
Take a look to the following link to know how to map a ManyToMany relation in a suitable way. But basically, you can do:
public class Product {
...
#ManyToMany(cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
#JoinTable(name="product_customer"
joinColumns=#JoinColumn(name="product_id"),
inverseJoinColumns=#JoinColumn(name="customer_id")
)
private Set<Customer> customers = new LinkedHashSet<>();
...
}
And:
public class Customer extends BaseEntity {
...
#ManyToMany(mappedBy = "customers")
private Set<Product> products = new LinkedHashSet<>();
...
}
As #Kavithakaran mentioned in a comment of his answer, you can use #ManyToMany(mappedBy = ... once you identify the "owner of the relation".
If you mean that you don't want to create the third table then you can read the following link below:-
Hibernate Many to Many without third table
Otherwise, you can do this with #jointable annotation.
I am trying to use Hibernate Search 6 and elastic search
A simple example of what I am trying to build is as follows.
I have a Book entity, which has information like title, authorName, genre, price
I have a Shop entity which has information like shopName, phone, email, location
I have a "joining table" which does a many to many mapping between nooks and shops. ( A book can be at many shops, and a shop can have many books)
I am trying to do a search by name and location, ideally to find a book at a location nearest to the input. The standard book-author example in the documentation requires a IndexedEmbedded annotation, which is not really possible in my case because I am using a joining table.
Is there an alternative approach to solve this problem
My entities
#Indexed
public class Book extends PanacheEntity{
public String title;
public String authorName;
#OneToMany(mappedBy = "book", fetch = FetchType.LAZY, cascade = { CascadeType.REMOVE })
public List<BookShopRelation> bookShopRelation = new ArrayList<>();
}
#Indexed
public class Shop extends PanacheEntity{
public String name;
public String city;
#OneToMany(mappedBy = "shop", fetch = FetchType.LAZY, cascade = { CascadeType.REMOVE })
private List<BookShopRelation> bookShopRelation = new ArrayList<>();
}
#Indexed
public class BookShopRelation extends PanacheEntity{
#JoinColumn(name = "shop_id")
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#IndexedEmbedded
private Shop shop;
#JoinColumn(name = "offer_id")
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#IndexedEmbedded
private Book book;
}
For me what was key was to understand, was that the relation table could be indexed and used as the basis for the search
List<BookShopRelation> result = Search.session(entityManager)
.search(BookShopRelation.class) .predicate(f ->
pattern == null || pattern.trim().isEmpty() ?
f.matchAll() :
f.simpleQueryString()
.fields("book.title").matching(pattern)
)
.fetchHits(size.orElse(20));
Product and ProductTag form a one-to-many relationship, as shown below.
#Entity
public class Product {
#Id
Long id;
#OneToMan(mappedBy = "product")
List<ProductTag> productTags;
}
#Entity
public class ProductTag {
#Id
Long id;
String content;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "product_id")
Product product;
}
Now I have an API that searches products, then returns them with their tags. Every time I call product.getProductTags(), Hibernate will fire an SQL query. Since the MySQL server is far away from the app server, I would like to cache product.getProductTags() call. How do I achieve that?
Use a specific query to fetch the tags and store them in a cache:
public TagRepository extends JpaRepository<Tag, Long> {
#Cacheable("tagsByProducts")
#Query("Select t from ProductTag t where t.product = ?product")
List<Tag> findByProduct(Product product);
}
somewhere you need some method to evict the cache: annotated by#CacheEvict("tagsByProducts")
But to be honest: I doubt that is a good idea to store JPA Entities in a cache! I think this would lead to many many strange problems. So better query just for the tag names (or content) instead of the tag-entities.
public TagRepository extends JpaRepository<Tag, Long> {
#Cacheable("tagsByProducts")
#Query("Select t.content from ProductTag t where t.product = ?product")
List<String> findTagContentByProduct(Product product);
}
#Entity
public class Product {
#Id
Long product_id;
#OneToMany(casacade=CascadeType.ALL,
fetch=FetchType.Eager,mappedBy = "product")
#JsonManagedReference(value="product-tag")
List<ProductTag> productTags;
}
#Entity
public class ProductTag {
#Id
Long id;
String content;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "product_id")
#JsonBackReference(value="product-tag")
Product product;
}
I am trying to use entity graph for triggering lazy collections to load but unfortunately entity graph also triggers all nested collections. I am using spring-data-jpa-entity-graph library for creating entity graphs at runtime.
#Entity
public class Brand implements Serializable {
#OneToMany(mappedBy = "brand", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set<Vehicle> vehicles;
}
#Entity
public class Vehicle implements Serializable {
#ManyToOne
#JoinColumn(name = "brand_id")
private Brand brand;
#OneToMany(mappedBy = "vehicle", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set<VehiclePart> parts;
}
#Entity
public class VehiclePart implements Serializable {
#ManyToOne
#JoinColumn(name = "vehicle_id")
private Vehicle vehicle;
}
Spring service with JPA repository:
public interface BrandsRepository extends EntityGraphJpaRepository<Brand, Long> {
Page<Brand> findAll(Pagable pagable, EntityGraph entityGraph);
}
#Service
public class BrandsService {
public List<Brand> find() {
return repository.findAll(PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "id")), EntityGraphUtils.fromAttributePaths("vehicles")).getContent();
}
}
In this case service also return parts collection for each vehicle but I would like to fetch only list of brands with vehicles collection for each brand.
How can we trigger to load lazy collections just on the first level (only brand's vehicles -- without vehicle's parts)?
I had the same problem. In my case: Spring and hibernate acted correctly, but I can see, that unused (lazy) fields are queried from sql.
When you use the fields, then they will be loaded over sql.
Iam using lombok and #EqualsAndHashCode.Exclude and #ToString.Exclude helps to prevent that.
In your case: Add a DTO-layer. Do not return the entities themself.
Or use #JsonIgnore annotation to ignore fields.