Hibernate performs update and delete on custom JPQL - spring

I am trying to update the fields of an entity that has a ManyToMany relationship, however, as I just want to update the table fields and ignore the ManyToMany relationship. The relationship is between the Company and UserSystem entities, it was defined in the relationship that company_user_system is the union table of the entities. The problem is that when executing my update in Company, always before my update, Hibernate makes an update in company and the relationship delete in user_system_company and this erases the relationship between Company and UserSystem and I don't understand why these two queries occur if I don't execut.
These are the queries, the first and second are not executed by my code:
Hibernate: update company set active=?, email=?, identification_code=?, trading_name=?, update_on=? where id=?
Hibernate: delete from company_user_system where company_id=?
Hibernate: update company set email=?, phone=?, corporate_name=?, trading_name=?, identification_code=?, email=?, phone2=? where id=?
Hibernate: select company0_.id as id1_0_, company0_.active as active2_0_, company0_.corporate_name as corporat3_0_, company0_.created_on as created_4_0_, company0_.email as email5_0_, company0_.email2 as email6_0_, company0_.identification_code as identifi7_0_, company0_.phone as phone8_0_, company0_.phone2 as phone9_0_, company0_.trading_name as trading10_0_, company0_.update_on as update_11_0_ from company company0_ where company0_.id=?
Following is the update implementation code:
public class CompanyRepositoryImpl implements CompanyRepositoryCustom {
#PersistenceContext
private EntityManager entityManager;
public Company updateCompanyFields(Company company) {
// ... fieldSql implementation omitted
String sql = "UPDATE Company SET "+ fieldsSql +" WHERE id = :id ";
Query query = entityManager.createQuery(sql);
// set the values for the fields
for (Method method : getMethods) {
query.setParameter(lowercaseFirstCharInString(cutGetInMethods(method.getName())), method.invoke(company));
}
// set id
query.setParameter("id", company.getId());
// execute update and search the database to return the updated object
if (query.executeUpdate() == 1) {
query = entityManager.createQuery("SELECT c FROM Company c WHERE c.id = :id");
query.setParameter("id", company.getId());
Company getCompany = (Company) query.getResultList().get(0);
return getCompany;
}
return null;
}
// ... Other methods omitted
}
Repository Code:
#Repository
public interface CompanyRepository extends JpaRepository<Company, Long>, JpaSpecificationExecutor<Company> , CompanyRepositoryCustom {
#Modifying
Company updateCompanyFields(Company company);
}
Company entity code, I just added the attributes that I think may contain something useful to try to solve the problem:
#Entity
#DynamicUpdate
#Table(name = "company")
public class Company implements Serializable {
#CreationTimestamp
#Column(name = "created_on", nullable = false)
private Instant createdOn;
#UpdateTimestamp
#Column(name = "update_on")
private Instant updateOn;
#ManyToMany
#JoinTable(
name = "company_user_system",
joinColumns = #JoinColumn(
name = "company_id", referencedColumnName = "id"
),
inverseJoinColumns = #JoinColumn(
name = "user_system_id", referencedColumnName = "id"
)
)
private Set<UserSystem> userSystems = new HashSet<>();
}
The UserSystem class defines the relationship as follows:
#ManyToMany(mappedBy = "userSystems")
private Set<Company> companies = new HashSet<>();
What may be causing this update and delete before my update?

This happens because you changed somewhere the value(s) of your relationship. EntityManager tracks such changes and marks the entity as dirty. When you execute a custom SQL query Hibernate will perform all the pending queries (submit any dirty entities).
You may prevent it by calling EntityManager.clear().

Related

Is there a way to have custom SQL query on top of JPA repository to have BULK UPSERTS?

I have a snowflake database and it doesn't support unique constraint enforcement (https://docs.snowflake.com/en/sql-reference/constraints-overview.html).
I'm planning to have a method on JPA repository with a custom SQL query to check for duplicates before inserting to the table.
Entity
#Entity
#Table(name = "STUDENTS")
public class Students {
#Id
#Column(name = "ID", columnDefinition = "serial")
#GenericGenerator(name = "id_generator", strategy = "increment")
#GeneratedValue(generator = "id_generator")
private Long id;
#Column(name = "NAME")
private String studentName;
}
Snowflake create table query
CREATE table STUDENTS(
id int identity(1,1) primary key,
name VARCHAR NOT NULL,
UNIQUE(name)
);
Repository
public interface StudentRepository extends JpaRepository<Students, Long> {
//
#Query(value = "???", nativeQuery = true)
List<Student> bulkUpsertStudents(List<Student> students);
}
You can use a SELECT query to check for duplicate values in the name column before inserting a new record into the table. For example:
#Query(value = "SELECT * FROM STUDENTS WHERE name = :name", nativeQuery = true)
List<Student> findByName(#Param("name") String name);
This method will return a list of Student records with the specified name value. If the list is empty, it means that there are no records with that name value, and you can safely insert a new record with that name value.
List<Student> studentList = new ArrayList<>();
for (Student student : students) {
List<Student> existingStudents = studentRepository.findByName(student.getName());
if (existingStudents.isEmpty()) {
studentsToInsert.add(student);
}
}
studentRepository.bulkUpsertStudents(studentList)
EDIT
If the above solution doesn't work. You can use the MERGE statement to update existing records in the table if the data has changed. For example, if you want to update the name of a Student if it has changed, you can use the following MERGE statement:
#Query(value = "MERGE INTO students t USING (SELECT :name AS name, :newName AS newName) s
ON t.name = s.name
WHEN MATCHED AND t.name <> s.newName THEN UPDATE SET t.name = s.newName
WHEN NOT MATCHED THEN INSERT (name) VALUES (s.name)", nativeQuery = true)
List<Student> bulkUpsertStudents(List<Student> students);
This query will update the name of each Student in the students list if it has changed, and if a conflict occurs, it will not insert a new record. This will ensure that only unique name values are inserted into the table, without having to perform a separate query for each record.
I was able to overcome this using the below approach but need to verify the performance of the queries.
Repository saveAll() method to save all the entities.
Using the custom nativeQuery as below
INSERT OVERWRITE INTO STUDENTS
WITH CTE AS(SELECT ROW_NUMBER() OVER (PARTITION BY NAME ORDER BY ID) AS RNO, ID, NAME FROM STUDENTS)
SELECT ID, NAME FROM CTE WHERE RNO = 1;
Example code :
import static io.vavr.collection.List.ofAll;
import static io.vavr.control.Option.of;
import static java.util.function.Predicate.not;
public Validation<ValidationError, List<Students>> saveAll(List<String> students) {
return of(students)
.filter(not(List::isEmpty))
.map(this::mapToEntities) // maps the list to list of database entities
.map(repository::saveAll) // save all
.toValidation(ERROR_SAVING_STUDENTS) // vavr validation in case of error
.peek(x -> repository.purgeStudents()) // purging to remove duplicates
.toValidation(ERROR_PURGING_STUDENTS);
}
This issue is only due to snowflake's incapability to check uniqueness.

Spring JPA delete entity not working when iterating

I am trying to delete an entity using its remove method of its repository from another service class, but it is not getting deleted. Below code works when I hard code the Id:
long id = 1234;
Optional<Employee> employeeOptional = employeeRepository.findById(id);
Employee employee = employeeOptional.get();
employeeRepository.delete(employee);
Above code is working fine, but if I try with below code, deletion is not happening.
for (Employee employee : department.getEmployees()) {
if (employee.getRole().equals("Manager")) {
employeeRepository.delete(employee);
}
}
I am trying the code from DepartmentServiceImpl class, but above is not working, but same when id is hardcoded it works.
Inside Department I have relationship like below,
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "deal")
private Set<Employee> employees= new HashSet<>();
And inside Employee I have like below,
#ManyToOne
#JoinColumn(name = "department_id", referencedColumnName = "department_id")
private Department department;
How can I fix this issue?
You are attempting to delete Employees, but your entities still have references to each other.
A better way to delete an employee is to use orphan removal and remove the employee from the collection which will trigger a delete.
Also mappedBy = "deal" should be the name of the attribute on the owning side of the relationship so this should be mappedBy = "department"
#OneToMany(
cascade = CascadeType.ALL,
mappedBy = "department",
orphanRemoval = true
)
private Set<Employee> employees= new HashSet<>();
add a method to Department to remove the Employee and keep the bidirectional relationship in sync by also removing Department from Employee
public void removeEmployee(Employee employee) {
employees.removeEmployee(employee);
employee.setDepartment(null);
}
you can then remove the 'Managers' from your Employees collection which should trigger the delete statements
List<Employee> managers = department.getEmployees.stream()
.filter(e -> e.getRole().equals("Manager"))
.collect(Collectors.toList());
for (Employee manager : managers) {
department.removeEmployee(manager);
}
Not tested but should work fine:
Try tweaking your code a little like this:
Set<Employee>employees= new HashSet<>();
for (Employee employee : department.getEmployees()) {
if (employee.getRole().equals("Manager")) {
employees.add(employee);
}
}
department.setEmployees(employees);//I assume you have a setter
departmentRepository.save(department); //I hope you understand what departmentRepository means
Here you are reassigning the valid employees list.
You could follow another method, instead of deleting each entity separately, you could call a bulk-save using saveAll(...) method on the valid list.

Spring boot hibernate #ManyToMany doesn't commit or returns incomplete data when I execute any method on the junction table from non-owner entity

I'm currently working on a Spring Boot project for an online shop. It's my first project with Spring Boot (and my first post here), so my coding is not the best.
Context for the questions:
My shop (for now) has a lists of products and whishlists of different users (shopping lists), which have a bidirectional #ManyToMany relation (i left here the relevant details for my question(s)):
Product.java entity:
#Entity
public class Product extends RepresentationModel\<Product\>{
#Id
#GeneratedValue
#JsonView(ProductView.DescriptionExcluded.class)
private Integer id;
#ManyToMany()
#JoinTable(
name = "Shopping_Product",
joinColumns = { #JoinColumn(name = "id", referencedColumnName = "id") },
inverseJoinColumns = { #JoinColumn(name = "list_id", referencedColumnName = "list_id") })
#JsonIgnore
private Set<ShoppingList> shoppinglists = new HashSet<>();
// Constructor, getters, setters ....
ShoppingList.java entity:
#Entity
public class ShoppingList {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView(ShoppingListView.ProductsExcluded.class)
private Integer list_id;
#JsonView(ShoppingListView.ProductsIncluded.class)
#ManyToMany(mappedBy = "shoppinglists")
private Set<Product> products = new HashSet<>();
// Constructor, getters, setters ...
I chose Product as the owner because i wanted to delete (tho it would be more fit to show something like "offer expired", but I'll stick to delete for now) the product from all existing lists when the admin takes it down from the shop, which works as expected:
ProductResource.java (controller):
#DeleteMapping("/categs/*/sub/*/products/{id}")
public ResponseEntity<String> deleteProduct(#PathVariable int id) {
Optional<Product> optional = productRepository.findById(id);
if(!optional.isPresent()) throw new NotFoundException("Product id - " + id);
Product prod = optional.get();
productRepository.delete(prod);
return ResponseEntity.ok().body("Product deleted");
}
My problems now are related to the ShoppingList entity, which is not the owner.
Any call I make to the Product resource (controller) works as expected, but anything from the other side either fails or returns incomplete results, like the following:
1.
I call retrieve all products from a list and it returns only the first object (the list has at least 2):
ShoppingListResource.java (controller):
#RestController
public class ShoppingListResource {
#Autowired
private ProductRepository productRepository;
#Autowired
private UserRepository userRepository;
#Autowired
private ShoppingListRepository shoppinglistRepository;
#GetMapping("/user/lists/{id}")
public Set<Product> getShoppinglistProducts(#PathVariable int id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String currentPrincipalName = authentication.getName();
ShoppingList shoppingList = shoppinglistRepository.findById(id).get();
String name = shoppingList.getUser().getUsername();
if(!Objects.equals(currentPrincipalName, name)) throw new IllegalOperation("You can only check your list(s)!");
// All lists are shown for a product
// Product p = productRepository.findById(10111).get();
// Set<ShoppingList> set = p.getShoppinglists();
// set.stream().forEach(e -> log.info(e.toString()));
// Only first product is shown for a list
return shoppingList.getProducts();
This is what hibernate does on the last row (only returns 1/2 products)
Hibernate: select products0_.list_id as list_id2_3_0_,
products0_.id as id1_3_0_,
product1_.id as id1_1_1_,
product1_.description as descript2_1_1_,
product1_.name as name3_1_1_,
product1_.price as price4_1_1_,
product1_.subcat_id as subcat_i5_1_1_ from shopping_product products0_ inner join product product1_ on products0_.id=product1_.id where products0_.list_id=?
As i said above, I can delete a product and it gets removed automatically from all existing lists, but when i try the same from ShoppingList entity does nothing:
Same controller
#DeleteMapping("/user/lists/{id}")
public ResponseEntity<String> deleteShoppinglist(#PathVariable int id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String currentPrincipalName = authentication.getName();
ShoppingList shoppingList = shoppinglistRepository.findById(id).get();
String name = shoppingList.getUser().getUsername();
if(!Objects.equals(currentPrincipalName, name)) throw new IllegalOperation("You can only delete your list(s)!");
shoppinglistRepository.delete(shoppingList);
return ResponseEntity.ok().body("Shopping list deleted");
}
Also, when i try to add/delete product from an existing list, does nothing.
This is my repo with full code, if you'd like to test directly (dev branch is up to date):
https://github.com/dragostreltov/online-store/tree/dev
You can just use admin admin as authentication (on the H2 console too). More details on the readme.
All DB data at app start is inserted from a .sql file.
I checked other similar questions and tried different methods on my ShoppingList entity (on the delete issue), like:
#PreRemove
public void removeListsFromProducts() {
for(Product p : products) {
p.getShoppinglists().remove(this);
}
}
Spring/Hibernate: associating from the non-owner side
And still doesn't work.
UPDATE:
I found out what issues I was having, I'll post an answer with the solution.
For anyone who's got the same/similar problems as I did, this is how I resolved them:
For point 1
(Hibernate only retrieves the first product from a shoppingList (Set))
I made multiple tests on my retrieve method and found out my Set was only containing 1 object, despite calling .add(product) twice.
As you can see, I'm using HashSet for both entities:
In Product (owner):
private Set<ShoppingList> shoppinglists = new HashSet<>();
In ShoppingList (mappedBy):
private Set<Product> products = new HashSet<>();
Thanks to this answer: https://stackoverflow.com/a/16344031/18646899
I learnt:
HashSet (entirely reasonably) assumes reflexivity, and doesn't check for equality when it finds that the exact same object is already in the set, as an optimization. Therefore it will not even call your equals method - it considers that the object is already in the set, so doesn't add a second copy.
In particular, if x.equals(x) is false, then any containment check would also be useless.
Taking this into account, I overwrote the hashCode() and equals() methods in Product.class and now
shoppingList.getProducts()
works as expected.
For point 2
(not being able to delete associations of non-owner entity before deleting the row from it's table)
Added lazy fetch and cascade to Product #ManyToMany:
#ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH})
And added the following methods:
In Product class:
public void addShoppinglist(ShoppingList list) {
this.shoppinglists.add(list);
list.getProducts().add(this);
}
public void removeShoppinglist(ShoppingList list) {
this.shoppinglists.remove(list);
list.getProducts().remove(this);
}
In ShoppingList class:
public void addProduct(Product product) {
this.products.add(product);
product.getShoppinglists().add(this);
}
public void removeProduct(Product product) {
this.products.remove(product);
product.getShoppinglists().remove(this);
}
Added #Transactional and modified the method inside the controller (ShoppingListResource) for deleteShoppingList:
#RestController
public class ShoppingListResource {
...
#Transactional
#DeleteMapping("/user/lists/{id}")
public ResponseEntity<String> deleteShoppinglist(#PathVariable int id) {
...
shoppingList.getProducts().stream().forEach(e -> {
e.removeShoppinglist(shoppingList);
});
shoppinglistRepository.delete(shoppingList);
return ResponseEntity.ok().body("Shopping list deleted");
}
}
And now this is working as expected, the shoppingList's associations are deleted first then the shoppingList itself.

Spring Boot - relationship deleted on save() method

My problem is this: There is a many to many relationship between two tables - Project and Employee. There is an option to update a given employee, but there is a little problem. After updating the employee, hibernate automatically deletes the employee's record from the connected project_employee table.
Hibernate: update employee set email=?, first_name=?, last_name=? where employee_id=?
And this happens right after that
Hibernate: delete from project_employee where employee_id=?
I'm following a course and I've just noticed this error. Source code of the lecturer is here:
https://github.com/imtiazahmad007/spring-framework-course
I've checked your github page:
#ManyToMany(cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.PERSIST},
fetch = FetchType.LAZY)
#JoinTable(name="project_employee",
joinColumns=#JoinColumn(name="employee_id"),
inverseJoinColumns= #JoinColumn(name="project_id")
)
#JsonIgnore
private List<Project> projects;
CascadeType.MERGE + CascadeType.PERSIST mean, that if Employee entity is saved, Project entity references must be saved.
In may-to-many cases it means:
DELETE by foreign key
Bulk insert
In case there's no bulk insert, there's an issue with persisntence context (your are saving an entity with empty collection of projects).
Possible solutions:
Remove CascadeType.MERGE + CascadeType.PERSIST if you do not want to change projects every time your save Employee. You can still save the ccollection via Repository
Make sure collection is attached on save action. That will cause Delete+Insert, but the resut will be ok.
Change Many-To-Many to One-To-Many with EmbeddedId
Please, refer to documentation:
When an entity is removed from the #ManyToMany collection, Hibernate simply deletes the joining record in the link table. Unfortunately, this operation requires removing all entries associated with a given parent and recreating the ones that are listed in the current running persistent context.
https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html#associations-many-to-many
*** Update from dialog below to make cascade clear.
Say, you have two entities A & B (getters and setters omitted). + repos
#Entity
#Table(name = "a")
public class A {
#Id
private Integer id;
private String name;
#ManyToMany(cascade = {CascadeType.ALL})
#JoinTable(name="a_b",
joinColumns=#JoinColumn(name="a_id"),
inverseJoinColumns= #JoinColumn(name="b_id")
)
private List<B> bs;
}
#Entity
#Table(name = "b")
public class B {
#Id
private Integer id;
private String name;
}
You sample test looks like this:
#Test
public void testSave() {
B b = new B();
b.setId(1);
b.setName("b");
b = bRepository.save(b);
A a = new A();
a.setId(1);
a.setName("a");
a.setBs(Collections.singletonList(b));
aRepository.save(a);
a.setName("new");
service.save(a); //watch sevice implementations below
}
Version1:
#Transactional
public void save(A a) {
aRepository.save(a);
}
Hibernate logs are the following:
Hibernate:
update
a
set
name=?
where
id=?
Hibernate:
delete
from
a_b
where
a_id=?
Hibernate:
insert
into
a_b
(a_id, b_id)
values
(?, ?)
delete+bulk insert present (despite the fact, that B-s where not in fact changed)
Version2:
#Transactional
public void save(A a) {
Optional<A> existing = aRepository.findById(a.getId());
if (existing.isPresent()) {
a.setBs(existing.get().getBs());
}
aRepository.save(a);
}
Logs:
update
a
set
name=?
where
id=?
Here b-collection was forcibly re-attached, so hibernate understands, that it's not needed to be cascaded.

Order by #oneToMany field using JPA Specification

Consider the entities below -
#Entity
public class Employee {
#Id
#GeneratedValue
private long id;
private String name;
#OneToMany(mappedBy = "employee", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Phone> phones; //contains both "active" & "inactive" phones
}
#Entity
public class Phone {
#Id
#GeneratedValue
private long id;
private boolean active;
private String number;
#ManyToOne(fetch = FetchType.LAZY)
private Employee employee;
}
I need to pull all the employees and sort them depending on the count of "active" phones they have.
Please note that the employee can have active as well as inactive phones. So the query I am trying to achieve is
ORDER BY (SELECT
COUNT(phone4_.employee_id)
FROM
phone phone4_
WHERE
employee4_.id = phone4_.employee_id
AND phone4_.active = true
) DESC
I am stuck with specification here because of some reason and below is the code I have used -
List<Order> orders = new ArrayList<>();
orders.add(cb.desc(cb.size(employee.get("phones"))));
cq.orderBy(orders);
When I run the code the query that's get generated is
ORDER BY (SELECT
COUNT(phone4_.employee_id)
FROM
phone phone4_
WHERE
employee4_.id = phone4_.employee_id) DESC
I am unable to add an extra AND condition to the logic. Please suggest
As specified in the Persistence API specification:
4.6.16 Subqueries
Subqueries may be used in the WHERE or HAVING clause.
JPA doesn't support subqueries in the order by clause, nor in the select clause.
Hibernate ORM, though, supports them in the SELECT and WHERE clauses.
So you cannot write that query and being JPA compliant.
This HQL should work though and it's covered by Hibernate ORM:
SELECT e1, (SELECT count(p)
FROM Phone p
WHERE p.active = true AND p.employee = e1) as activeCount
FROM Employee e1
ORDER BY activeCount DESC
Surprisingly, writing this query with criteria doesn't work:
CriteriaBuilder builder = ormSession.getCriteriaBuilder();
CriteriaQuery<Object> criteria = builder.createQuery();
Root<Employee> root = criteria.from( Employee.class );
Subquery<Long> activePhonesQuery = criteria.subquery( Long.class );
Root<Phone> phoneRoot = activePhonesQuery.from( Phone.class );
Subquery<Long> phonesCount = activePhonesQuery
.select( builder.count( phoneRoot ) )
.where( builder.and( builder.isTrue( phoneRoot.get( "active" ) ), builder.equal( phoneRoot.get( "employee" ), root ) ) );
criteria.multiselect( root, phonesCount )
.orderBy( builder.desc( phonesCount ) );
The reason is that, Hibernate ORM tries to expand the subquery in the order by clause instead to refer to an alias. And as I mentioned before, this is not supported.
I think the HQL is the easiest option if you don't want to use native queries.

Resources