Hibernate OneToMany Mapping - Where Set of Entities is getting saved? - java

I have two entity class named Customer and Activity.
Customer has customerId,customerName,activities here activities holds set of Activity corresponds to each Customer mapped by #OneToMany(mappedBy = "customer") relationship.
The Customer class has been defined as follows (I have removed other fields and some getter and setter for clarity):
#javax.persistence.Entity
#Table(name = "CUSTOMER")
public class Customer extends Entity {
private String customerId;
private String customerName;
private Set<Activity> activities;
#NaturalId
#Column(name = "CUSTOMER_ID", nullable = false)
public String getCustomerId() {
return customerId;
}
#Column(name = "CUSTOMER_NAME", nullable = false)
public String getCustomerName() {
return customerName;
}
#OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
#Cascade(CascadeType.SAVE_UPDATE)
public Set<Activity> getActivities() {
return activities;
}
}
Activity has activityId,activityName,customer which is mapped by customerId with #ManyToOne relation.
Activity class is defined like:
#javax.persistence.Entity
#Table(name = "ACTIVITY")
public class Activity extends Entity {
private String activityId;
private String activityName;
private Customer customer;
#NaturalId
#Column(name = "ACTIVITY_ID", nullable = false)
public String getActivityId() {
return activityId;
}
#Column(name = "ACTIVITY_NAME", nullable = false)
public String getActivityName() {
return activityName;
}
#ManyToOne
#JoinColumn(name = "CUSTOMER_ID", nullable = false)
public Customer getCustomer() {
return customer;
}
}
Before saving a new Activity, I am adding this Activity to the activities set of Customer.
I want to know:
Where the activities of Customer are getting saved ?
How the update and delete operation of both of the entities will effect each other?
Is it the right way to create #OneToMany and #ManyToOne relationship?
Can I add column name on activities?
I am beginner in hibernate, any pointer would be very helpful to me.
Thanks in advance.

- Where the activities of Customer are getting saved ?
They are not getting saved as "activities of customer" anywhere in the DB.When the activities of a customer are needed, they are loaded together with the customer using a join query on the customer_id column, or they are loaded later (lazy-loading) using a separate query that filters the customer_id column.
- How the update and delete operation of both of the entities will effect each other?
A delete of an activity will not affect the customer, but next time its activities are loaded, the deleted activity will not be part of the collection anymore.
How the delete of a customer activities can be defined using the cascade attribute on the #ManyToOne, for example :
#OneToMany(mappedBy = "customer", cascade = CascadeType.REMOVE)
Basically, the desired behavior depends on if an activity can exist on its own without a customer : if yes, then the customer can be deleted, and the customer_id field can be set to null on the activity table. Otherwise the activities belonging to a customer will be 'cascade-deleted'. Or a customer just can0t be deleted before one has removed all its activities first.
- Is it the right way to create #OneToMany and #ManyToOne relationship?
Yes, it is correct.
- Can I add column name on activities?
No, it wouldn't make any sense. A column could only contain one ID, and activities is a collection.
I hope this helps.

Related

bidirectional onetomany on spring boot and hibernate, best way to save

I have 2 entities:
#Data
#Entity
#EqualsAndHashCode(onlyExplicitlyIncluded = true)
#Table(name = "source_company")
public class SourceCompany {
#Id
#EqualsAndHashCode.Include
private UUID id;
private String name;
#OneToMany( mappedBy = "company")
private final Set<SourceUser> users = new HashSet<>();
#Column(name = "version")
#Version
private Long version;
}
#Data
#Entity
#EqualsAndHashCode(onlyExplicitlyIncluded = true)
#Table(name = "source_user")
public class SourceUser {
#Id
#EqualsAndHashCode.Include
private UUID id;
private String name;
#Column(name = "version")
#Version
private Long version;
//ref
#ManyToOne
#JoinColumn(name = "fk_source_company")
private SourceCompany company;
}
Is it correct to save in this way (only 2 save)?
#Test
public void testSourceUserSave() {
SourceCompany sourceCompany= new SourceCompany();
sourceCompany.setName("xxx");
sourceCompany.setId(UUID.fromString("2bf05cbc-d530-11eb-b8bc-0242ac130003"));
SourceUser sourceUser= new SourceUser();
sourceUser.setName("dev-team");
sourceUser.setId(UUID.fromString("4bede7a0-d530-11eb-b8bc-0242ac130003"));
sourceUser.setCompany(sourceCompany);
sourceCompany.getUsers().add(sourceUser);
sourceCompanyRepository.save(sourceCompany);
sourceUserRepository.save(sourceUser);
assertNotNull(sourceUser);
assertEquals(sourceUser.getCompany().getId(), sourceCompany.getId());
assertEquals(sourceCompany.getUsers().stream().findFirst().get().getId(), sourceUser.getId());
}
or I need to save the user (without company) and the company (without user) and after that to update the user with a save and the company (without save because is not the owner) like this (3 save):
#Test
public void testSourceUserSave() {
SourceCompany sourceCompany= new SourceCompany();
sourceCompany.setName("xxx");
sourceCompany.setId(UUID.fromString("2bf05cbc-d530-11eb-b8bc-0242ac130003"));
SourceUser sourceUser= new SourceUser();
sourceUser.setName("dev-team");
sourceUser.setId(UUID.fromString("4bede7a0-d530-11eb-b8bc-0242ac130003"));
sourceUserRepository.save(sourceUser);
sourceCompanyRepository.save(sourceCompany);
sourceUser.setCompany(sourceCompany);
sourceCompany.getUsers().add(sourceUser);
sourceUserRepository.save(sourceUser);
assertNotNull(sourceUser);
assertEquals(sourceUser.getCompany().getId(), sourceCompany.getId());
assertEquals(sourceCompany.getUsers().stream().findFirst().get().getId(), sourceUser.getId());
}
It seems, looking in the db, that the first way works, so in future can I update only the owner side (I mean update and save) and so can I update the not-owner side only in the object without save it again?
Thanks in advance
You usually tend to save only one of the objects. This can be done adding the
#ManyToOne(cascade = CascadeType.PERSIST)
to the mapping annotation. This makes sure that the nested entities get persisted too
You would need to do just:
SourceCompany sourceCompany= new SourceCompany();
sourceCompany.setName("xxx");
sourceCompany.setId(UUID.fromString("2bf05cbc-d530-11eb-b8bc-0242ac130003"));
SourceUser sourceUser= new SourceUser();
sourceUser.setName("dev-team");
sourceUser.setId(UUID.fromString("4bede7a0-d530-11eb-b8bc-0242ac130003"));
sourceUser.setCompany(sourceCompany);
sourceUserRepository.save(sourceUser);
One more thing to note is that the .save method actually returns an entity itself. That entity is the persisted entity just created. Basically if you manage everything within a single transactional method any modification to the persisted entity within that method (transaction) will be applied without calling any save, merge or update method
I suggest reading about the #Transactional annotation

How to correctly save associated entities?

Let's say we have the following three domain model entities: Company, Departament, and Employee.
#Getter #Setter #NoArgsConstrutor
public class Employee {
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "department_id", nullable = false, insertable = false, updatable = false)
private Department department;
#JoinColumn(name = "department_id", nullable = false)
private int department_id;
}
#Getter #Setter #NoArgsConstrutor
public class Department {
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "company_id", nullable = false, insertable = false, updatable = false)
private Company company;
#JoinColumn(name = "company_id", nullable = false)
private int company_id;
#OneToMany(mappedBy = "department")
private List<Employee> employees;
}
#Getter #Setter #NoArgsConstrutor
private class Company {
private String name;
#OneToMany(mappedBy = "company")
private List<Department> departments;
}
For each entity, we have Repositories which extend JpaRepository, Services, and Controllers. In each Service we #Autowire the respective Repository, and in each entity Controller we call methods from the entity Service.
My issue is the following: I cannot save an entire Company, because the Departments require a Company ID, and Employees a Deparment ID. So, firstly, in my CompanyService I save and then clear the departments list, do a saveAndFlush which assigns an ID to my company. I assign the received ID to every company_id in each entity of the previously saved departments list, then attach the list back to the company and do another saveAndFlush, and I do this one more time for the employee list.
#RestController
public class CompanyController {
#Autowire
private CompanyService companyService;
#PostMapping("/companies")
public Company createCompany(#RequestBody Company newCompany) {
return companyService.createCompany(newCompany);
}
}
#Service
public class CompanyService {
#Autowire
private CompanyRepository companyRepository;
public Company createCompany(Company company) {
List<Department> departments = new ArrayList<>(company.getDepartments());
company.getDepartments().clear();
companyRepository.saveAndFlush(company);
int company_id = company.getId();
departments.forEach (department ->
department.setCompany_id(company_id);
);
//here I save a copy of the previously saved departments, because I still need the employees
company.getDepartments().addAll(departments.stream().map(department -> department.clone(department)).collect(Collectors.toList()));
company.getDepartments().forEach(department -> department.getEmployees().clear());
companyRepository.saveAndFlush(company);
//here I assign each employee it's corresponding department ID
for (int i = 0; i < company.getDepartments().size(); i++) {
Department departmentInSavedCompany = company.getDepartments().get(i);
Department departmentWhichStillHasEmployees = departments.get(i);
departmentWhichStillHasEmployees.setId(departmentInSavedCompany.getId());
departmentWhichStillHasEmployees.getEmployees().forEach(employee -> employee.setDepartment_id(departmentInSavedCompany.getId()));
}
company.getDepartments.clear();
company.getDepartments.addAll(departments);
return companyRepository.saveAndFlush(company);
}
}
#Repository
public interface CompanyRepository extends JpaRepository<Company, Integer> {
}
I currenty do not like this implementation neither do I find it good. Which is the correct approach for this situation?
When working with JPA, do not work with IDs, work with object references.
In your case, this means removing the id attributes that duplicate the references.
In order to obtain the proper entities for IDs use JpaRepository.getOne. It will return either the entity if it is already in the 1st level cache or a proxy just wrapping the id, so it won't hit the database.
This allows you to assemble your object graph and persist it in one pass starting with the entity having no references to other entities.
You might also consider configuring cascading, if you consider entities to be part of the same Aggregate, i.e. they should be loaded and persisted together.

Hibernate #OneToMany Mapping - issues with deleting records

In my application I would like to put all images in the application in one table. I posted a question last time and someone recommended that I use unidirectional #OneToMany.
I have the following entities which are associated with Image entity
#Entity
#Table(name="promotion")
public class Promotion {
#Id
#Column(name="id")
protected String id;
#OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#JoinColumn(name="itemId")
protected List<Image> images = new ArrayList<>();
}
#Entity
#Table(name="product")
public class Product {
#Id
#Column(name="id")
protected String id;
#OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#JoinColumn(name="itemId")
protected List<Image> images = new ArrayList<>();
}
#Entity
#Table(name="image")
public class Image{
#Id
#Column(name="id")
private String id = IdGenerator.createId();
#Column(name="itemId")
private String itemId;
#Column(name="title", nullable = false)
protected String title;
#Column(name="filename", nullable = false)
protected String filename;
#Column(name="path", unique = true)
private String path;
#Column(nullable = true)
protected int width;
#Column(nullable = true)
protected int height;
}
Issues am facing now are:
A)
When I use cascade={CascadeType.PERSIST, CascadeType.MERGE} on the images ArrayList attributes I get this exception:
org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
So I replaced it with cascade=CascadeType.ALL and when I save Product or Promotion the associated Image(s) are saved as well which is cool and that is what I want
B)
The main problem I have now is when I delete a Product or Promotion, its associated images never get deleted. Their associated images stays in the image table.
By using cascade=CascadeType.ALL I expect that when I delete a Product or a Promotion, its images should also be deleted automatically. I tried to delete an image from the database if it will trigger its associated Product or Promotion to be deleted but it didn't since I think it is unidirectional which makes sense. But how come when I delete a Product or a Promotion its associated images don't get deleted
add orphanRemoval = true in both relationships:
#OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.EAGER,
orphanRemoval = true)
This will
apply the remove operation to entities that have been removed from the
relationship and to cascade the remove operation to those entities.

JPA Unique Entries

I want to assign a category to a recipe. If I assign a second category with the same name to the recipe it does another insert to the database that aborts (Abort due to constraint violation (UNIQUE constraint failed: category.name) - this is actually fine). I want to reuse this entry and attach it to the recipe. Is there a JPA way to do this "automatically" or do I have to handle this? Should I search for a category with the same name in the setCategory method and use this one if present? Is there a Pattern?
#Entity
public class Recipe {
private Integer id;
private Category category;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
#ManyToOne(cascade = CascadeType.PERSIST)
#JoinColumn(name = "category_id", referencedColumnName = "id")
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
}
#Entity
public class Category {
private Integer id;
private String name;
private List<Recipe> recipes;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
#Basic
#Column(name = "name")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#OneToMany(mappedBy = "category", cascade = {CascadeType.ALL})
public List<Recipe> getRecipes() {
return recipes;
}
public void setRecipes(List<Recipe> recipes) {
this.recipes = recipes;
}
}
Example:
Category category = new Category();
category.setName("Test Category");
cookbookDAO.add(cookbook);
Recipe recipe = new Recipe();
recipe.setTitle("Test Recipe");
recipe.setCategory( category );
recipeDAO.add(recipe);
Executing this twice results in the UNIQUE constraint failed: category.name. This is fine since I don't want multiple categories with the same name. The database enforced this but I'm looking for the soltuion to enforce this on the java language level too.
The DDL:
CREATE TABLE "recipe"
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER,
FOREIGN KEY (category_id) REFERENCES category(id)
);
CREATE TABLE "category"
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
`name` VARCHAR,
UNIQUE(`name`) ON CONFLICT ABORT
);
Hello the behavior you are describing is a result of the mapping
#ManyToOne(cascade = CascadeType.PERSIST)
#JoinColumn(name = "category_id", referencedColumnName = "id")
public Category getCategory() {
return category;
}
If we translate this mapping is simple language. You are saying:
Everytime I attempt to save a Recipe the corresponding Category should
be persisted as well.
The problem here comes from the fact that you already have an existing Category so the #Cascade.PERSIST here is not appropriate.
The semantics here is the opposite . A Recipie creates a Category only if a Category does not exist yet. This mean that the creation of the Category is more of an exception than a general rule.
This mean that you have two options here.
Option 1 is to remove Cascade.PERSIST.
Option 2 is to replace it with Cascade.MERGE.
Option 3 is to go the other way. Instead of annotating the #ManyToOne relationship in Recipe to Category with Cascade.PERSIST to annotate the #OneToMany relationship from the Category to Recipe.
The advantage of such approach is very simple. While you not always want to create a new category when adding a new recipe. It is 100% all the time you want to add a new Category you also want to add all the attached Recipies.
Also I will recommend you to favor Bidirectional relationships over unidirectional ones and read this article about merge and persist JPA EntityManager: Why use persist() over merge()?
The problem is you are creating a new category with the following statement:
Category category = new Category();
Because this instance of the Category entity is new and accordingly does not have a persistent identity the persistence provider will try to create a new record in the database. As the name field of the category table is unique you get constraint violation exception from the database.
The solution is first fetching the category and assign it to recipe. So what you have to do is the following:
String queryString = "SELECT c FROM Category c WHERE c.name = :catName";
TypedQuery<Category> query = em.createQuery(queryString, Category.class);
em.setParameter("catName", "Test Category");
Category category = query.getSingleResult();
This fetched instance is a managed entity and the persistence provider will not try to save. Then assign this instance to the recipe as you already did:
recipe.setCategory( category );
In this case the cascading will just ignore saving the category instance when recipe is saved. This is stated in the JPA 2.0 specification in section 3.2.2 Persisting an Entity Instance as follows:
If X is a preexisting managed entity, it is ignored by the persist operation.

Hibernate triggering constraint violations using orphanRemoval

I'm having trouble with a JPA/Hibernate (3.5.3) setup, where I have an entity, an "Account" class, which has a list of child entities, "Contact" instances. I'm trying to be able to add/remove instances of Contact into a List<Contact> property of Account.
Adding a new instance into the set and calling saveOrUpdate(account) persists everything lovely. If I then choose to remove the contact from the list and again call saveOrUpdate, the SQL Hibernate seems to produce involves setting the account_id column to null, which violates a database constraint.
What am I doing wrong?
The code below is clearly a simplified abstract but I think it covers the problem as I'm seeing the same results in different code, which really is about this simple.
SQL:
CREATE TABLE account ( INT account_id );
CREATE TABLE contact ( INT contact_id, INT account_id REFERENCES account (account_id) );
Java:
#Entity
class Account {
#Id
#Column
public Long id;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "account_id")
public List<Contact> contacts;
}
#Entity
class Contact {
#Id
#Column
public Long id;
#ManyToOne(optional = false)
#JoinColumn(name = "account_id", nullable = false)
public Account account;
}
Account account = new Account();
Contact contact = new Contact();
account.contacts.add(contact);
saveOrUpdate(account);
// some time later, like another servlet request....
account.contacts.remove(contact);
saveOrUpdate(account);
Result:
UPDATE contact SET account_id = null WHERE contact_id = ?
Edit #1:
It might be that this is actually a bug
http://opensource.atlassian.com/projects/hibernate/browse/HHH-5091
Edit #2:
I've got a solution that seems to work, but involves using the Hibernate API
class Account {
#SuppressWarnings("deprecation")
#OneToMany(cascade = CascadeType.ALL, mappedBy = "account")
#Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
#JoinColumn(name = "account_id", nullable = false)
private Set<Contact> contacts = new HashSet<Contact>();
}
class Contact {
#ManyToOne(optional = false)
#JoinColumn(name = "account_id", nullable = false)
private Account account;
}
Since Hibernate CascadeType.DELETE_ORPHAN is deprecated, I'm having to assume that it has been superseded by the JPA2 version, but the implementation is lacking something.
Some remarks:
Since you have a bi-directional association, you need to add a mappedBy attribute to declare the owning side of the association.
Also don't forget that you need to manage both sides of the link when working with bi-directional associations and I suggest to use defensive methods for this (shown below).
And you must implement equals and hashCode on Contact.
So, in Account, modify the mapping like this:
#Entity
public class Account {
#Id #GeneratedValue
public Long id;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true)
public List<Contact> contacts = new ArrayList<Contact>();
public void addToContacts(Contact contact) {
this.contacts.add(contact);
contact.setAccount(this);
}
public void removeFromContacts(Contact contact) {
this.contacts.remove(contact);
contact.setAccount(null);
}
// getters, setters
}
In Contact, the important part is that the #ManyToOne field should have the optional flag set to false:
#Entity
public class Contact {
#Id #GeneratedValue
public Long id;
#ManyToOne(optional = false)
public Account account;
// getters, setters, equals, hashCode
}
With these modifications, the following just works:
Account account = new Account();
Contact contact = new Contact();
account.addToContact(contact);
em.persist(account);
em.flush();
assertNotNull(account.getId());
assertNotNull(account.getContacts().get(0).getId());
assertEquals(1, account.getContacts().size());
account.removeFromContact(contact);
em.merge(account);
em.flush();
assertEquals(0, account.getContacts().size());
And the orphaned Contact gets deleted, as expected. Tested with Hibernate 3.5.3-Final.

Categories

Resources