Why do we need bidirectional synchronized methods? - java

As stated in the topic. Why do we need bidirectional synchronized methods? What real world use case does it solve? What happens if I don't use them?
In Hibernate's User Guide:
Whenever a bidirectional association is formed, the application developer must make sure both sides are in-sync at all times.
The addPhone() and removePhone() are utility methods that synchronize both ends whenever a child element is added or removed.
Source - Hibernate User Guide
In one of Vlad's blog posts:
However, we still need to have both sides in sync as otherwise, we break the Domain Model relationship consistency, and the entity state transitions are not guaranteed to work unless both sides are properly synchronized.
Source - Vlad Mihalcea Blog
Lastly, in Vlad's book - High Performance Java Persistance, page 216:
For a bidirectional #ManyToMany association, the helper methods must be added to the entity that is more likely to interact with. In our case, the root entity is the Post, so the helper methods are added to the Post entity
However, if I use simple generated setters, Hibernate seems to work just fine as well. Furthermore, synchronized methods might lead to performance degredation.
Synchronized methods:
public void joinProject(ProjectEntity project) {
project.getEmployees().add(this);
this.projects.add(project);
}
Generates this:
Hibernate:
select
employeeen0_.id as id1_0_0_,
projectent2_.id as id1_2_1_,
teamentity3_.id as id1_3_2_,
employeeen0_.first_name as first_na2_0_0_,
employeeen0_.job_title as job_titl3_0_0_,
employeeen0_.last_name as last_nam4_0_0_,
employeeen0_.team_id as team_id5_0_0_,
projectent2_.budget as budget2_2_1_,
projectent2_.name as name3_2_1_,
projects1_.employee_id as employee1_1_0__,
projects1_.project_id as project_2_1_0__,
teamentity3_.name as name2_3_2_
from
employees.employee employeeen0_
inner join
employees.employee_project projects1_
on employeeen0_.id=projects1_.employee_id
inner join
employees.project projectent2_
on projects1_.project_id=projectent2_.id
inner join
employees.team teamentity3_
on employeeen0_.team_id=teamentity3_.id
where
employeeen0_.id=?
Hibernate:
select
projectent0_.id as id1_2_,
projectent0_.budget as budget2_2_,
projectent0_.name as name3_2_
from
employees.project projectent0_
where
projectent0_.id=?
Hibernate:
select
employees0_.project_id as project_2_1_0_,
employees0_.employee_id as employee1_1_0_,
employeeen1_.id as id1_0_1_,
employeeen1_.first_name as first_na2_0_1_,
employeeen1_.job_title as job_titl3_0_1_,
employeeen1_.last_name as last_nam4_0_1_,
employeeen1_.team_id as team_id5_0_1_
from
employees.employee_project employees0_
inner join
employees.employee employeeen1_
on employees0_.employee_id=employeeen1_.id
where
employees0_.project_id=?
Hibernate:
insert
into
employees.employee_project
(employee_id, project_id)
values
(?, ?)
Notice additional select for Employee right after Projects were fetched. If I use simply employeeEntity.getProjects().add(projectEntity);, it generates:
Hibernate:
select
employeeen0_.id as id1_0_0_,
projectent2_.id as id1_2_1_,
teamentity3_.id as id1_3_2_,
employeeen0_.first_name as first_na2_0_0_,
employeeen0_.job_title as job_titl3_0_0_,
employeeen0_.last_name as last_nam4_0_0_,
employeeen0_.team_id as team_id5_0_0_,
projectent2_.budget as budget2_2_1_,
projectent2_.name as name3_2_1_,
projects1_.employee_id as employee1_1_0__,
projects1_.project_id as project_2_1_0__,
teamentity3_.name as name2_3_2_
from
employees.employee employeeen0_
inner join
employees.employee_project projects1_
on employeeen0_.id=projects1_.employee_id
inner join
employees.project projectent2_
on projects1_.project_id=projectent2_.id
inner join
employees.team teamentity3_
on employeeen0_.team_id=teamentity3_.id
where
employeeen0_.id=?
Hibernate:
select
projectent0_.id as id1_2_,
projectent0_.budget as budget2_2_,
projectent0_.name as name3_2_
from
employees.project projectent0_
where
projectent0_.id=?
Hibernate:
insert
into
employees.employee_project
(employee_id, project_id)
values
(?, ?)
No more fetching of employee.
Full code.
Controller.
#RestController
#RequestMapping(path = "${application.endpoints.projects}", produces = MediaType.APPLICATION_JSON_VALUE)
#Validated
public class ProjectsEndPoint {
#PostMapping("add-employee")
#ApiOperation("Add employee to project")
public void addEmployeeToProject(#RequestBody #Valid EmployeeProjectRequest request) {
LOGGER.info("Add employee to project. Request: {}", request);
this.projectsService.addEmployeeToProject(request);
}
}
EmployeeProjectRequest.
#JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public record EmployeeProjectRequest(
#NotNull #Min(0) Long employeeId,
#NotNull #Min(0) Long projectId) {
}
ProjectService.
#Service
public class ProjectsService {
private final ProjectRepo projectRepo;
private final EmployeeRepo repo;
public ProjectsService(ProjectRepo projectRepo, EmployeeRepo repo) {
this.projectRepo = projectRepo;
this.repo = repo;
}
#Transactional
public void addEmployeeToProject(EmployeeProjectRequest request) {
var employeeEntity = this.repo.getEmployee(request.employeeId())
.orElseThrow(() -> new NotFoundException("Employee with id: %d does not exist".formatted(request.employeeId())));
var projectEntity = this.projectRepo.getProject(request.projectId())
.orElseThrow(() -> new NotFoundException("Project with id: %d does not exists".formatted(request.projectId())));
//This line can be changed with employeeEntity.joinProject(projectEntity);
employeeEntity.getProjects().add(projectEntity);
}
}
ProjectRepo.
#Repository
public class ProjectRepo {
private final EntityManager em;
public ProjectRepo(EntityManager em) {
this.em = em;
}
public Optional<ProjectEntity> getProject(Long id) {
var result = this.em.createQuery("SELECT p FROM ProjectEntity p where p.id = :id", ProjectEntity.class)
.setParameter("id", id)
.getResultList();
return RepoUtils.fromResultListToOptional(result);
}
}
EmployeeRepo.
#Repository
public class EmployeeRepo {
private final EntityManager em;
public EmployeeRepo(EntityManager em) {
this.em = em;
}
public Optional<EmployeeEntity> getEmployee(Long id) {
var employees = this.em.createQuery("""
SELECT e FROM EmployeeEntity e
JOIN FETCH e.projects p
JOIN FETCH e.team t
WHERE e.id = :id""", EmployeeEntity.class)
.setParameter("id", id)
.getResultList();
return Optional.ofNullable(employees.isEmpty() ? null : employees.get(0));
}
}
EmployeeEntity.
#Entity
#Table(name = "employee", schema = "employees")
public class EmployeeEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
#Enumerated(EnumType.STRING)
private JobTitle jobTitle;
#ManyToOne(fetch = FetchType.LAZY)
private TeamEntity team;
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
#JoinTable(schema = "employees", name = "employee_project",
joinColumns = #JoinColumn(name = "employee_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "project_id", referencedColumnName = "id"))
private Set<ProjectEntity> projects = new HashSet<>();
public EmployeeEntity() {
}
public void joinProject(ProjectEntity project) {
project.getEmployees().add(this);
this.projects.add(project);
}
public void leaveProject(ProjectEntity project) {
project.getEmployees().remove(this);
this.projects.remove(project);
}
... Getters and Setters ...
}
ProjectEntity.
Entity
#Table(name = "project", schema = "employees")
public class ProjectEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal budget;
#ManyToMany(fetch = FetchType.LAZY, mappedBy = "projects")
private Set<EmployeeEntity> employees = new HashSet<>();
public ProjectEntity() {
}
... Getters and Setters ...
}

If there are really many elements on the Many side, then you probably should not use OneToMany at all. Fetching large collections implies using some kind of pagination\filtering, but OneToMany loads the whole set.
First of all, you need to update an owning entity(where FK resides) to store it in the DB. And what Vlad and Hibernate guide mean about consistency, refers to updating entity objects inside current session. Those objects have transitions during lifecycle, and when you have bidirectional association, if you don't set inverse side, then that inverse side entity won't have the field updated, and would be inconsistent with an owning side entity(and probably with the DB ultimately, after TX commits) in the current session.
Let me illustrate on OneToMany example.
If we get 2 managed entities Company and Employee:
set employee.company = X -> persist(employee) -> managed List<Employee> company.employees gets inconsistent with db
And there might be different types of inconsistencies, like getting from company.employees field after and arising side-effects(guess it was not empty, but just without employee you just added), and if there is Cascade.ALL, you might miss or falsely remove\update\add entities through broken relationships, because your entities are in a ambigious state, and hibernate deals with it in a defensive but sometimes unpredictable way:
Delete Not Working with JpaRepository
Also, you might find interesting this answer: https://stackoverflow.com/a/5361587/2924122

Related

Why are lazy fields on related entities loaded

In my REST API project (Java 8, Spring Boot 2.3.1) I have a problem with some queries triggering massive query chains by loading lazy relations, even though the related objects are never accessed.
I have a UserEntity and a polymorphic CompanyEntity that are related with a ManyToMany relationship. I have an endpoint that returns all users and I include the IDs of the related companies in the JSON. I excpect a query to the user table and a query to the company table, however all related entities of one sub-entity of CompanyEntity are always loaded for each of those sub-entities resulting in large query chains.
Here are snippets of my classes:
User entity
#Entity(name = "USERS")
public class UserEntity {
#Id
#GeneratedValue
private UUID id;
#EqualsAndHashCode.Exclude
#Fetch(FetchMode.SUBSELECT)
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(
name = "users_company",
joinColumns = #JoinColumn(name = "USER_ID"),
inverseJoinColumns = #JoinColumn(name = "COMPANY_ID")
)
private Set<CompanyEntity> companies = new HashSet<>();
public List<UUID> getCompanyIds() {
return companies.stream()
.map(CompanyEntity::getId)
.collect(Collectors.toList());
}
}
Polymorphic company entity
#Entity(name = "COMPANY")
#Inheritance(strategy = InheritanceType.JOINED)
public abstract class CompanyEntity {
#Id
#GeneratedValue
private UUID id;
#Fetch(FetchMode.SUBSELECT)
#ManyToMany(mappedBy = "companies", fetch = FetchType.LAZY)
private Set<UserEntity> users = new HashSet<>();
}
Concrete company subclass that triggers the problem
#Entity(name = "CUSTOMER")
public class CustomerEntity extends CompanyEntity {
#NotNull
#OneToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
private ContactPersonEntity contactPerson;
#Fetch(FetchMode.SUBSELECT)
#OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY, mappedBy = "customer")
private Set<TransactionEntity> transactions = new HashSet<>();
public Set<UUID> getTransactionIds() {
return this.transactions.stream()
.map(TransactionEntity::getId)
.collect(Collectors.toSet());
}
}
In the REST controller I return the following mapping:
#GetMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE)
public List<UserReadModel> getUsers() {
return userRepository.findAll().stream()
.map(userEntity -> new UserReadModel(userEntity))
.collect(Collectors.toList());
}
Where the UserReadModel is a DTO:
#Data
public class UserReadModel {
private UUID id;
private List<UUID> companyIds;
}
Logging the database queries results in the following output:
// Expected
Hibernate: select userentity0_.id as id1_47_, ... from users userentity0_
Hibernate: select companies0_.user_id ... case when companyent1_1_.id is not null then 1 when companyent1_2_.id is not null then 2 when companyent1_.id is not null then 0 end as clazz_0_ from users_company companies0_ inner join company companyent1_ on companies0_.company_id=companyent1_.id left outer join customer companyent1_1_ on companyent1_.id=companyent1_1_.id left outer join external_editor companyent1_2_ on companyent1_.id=companyent1_2_.id where companies0_.user_id in (select userentity0_.id from users userentity0_)
// Unexpected as they are marked lazy and never accessed
Hibernate: select contactper0_.id ... from contact_person contactper0_ where contactper0_.id=?
Hibernate: select transactio0_.customer_id ... from transactions transactio0_ where transactio0_.customer_id=?
Hibernate: select contactper0_.id ... from contact_person contactper0_ where contactper0_.id=?
Hibernate: select transactio0_.customer_id ... from transactions transactio0_ where transactio0_.customer_id=?
...
I've read through loads of articles on entity mapping and lazy loading but I can't seem to find a reason why this behavior persists. Did anyone have this problem before?
You are accessing the collection, so Hibernate has to load the collection. Since you only need the ids and already have a DTO, I think this is a perfect use case for Blaze-Persistence Entity Views.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(UserEntity.class)
public interface UserReadModel {
#IdMapping
UUID getId();
#Mapping("companies.id")
Set<UUID> getCompanyIds();
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
UserReadModel a = entityViewManager.find(entityManager, UserReadModel.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
Page<UserReadModel> findAll(Pageable pageable);
The best part is, it will only fetch the state that is actually necessary! In your case, a query like the following will be generated:
select u.id, uc.company_id
from users u
left join users_company uc on uc.user_id = u.id
left join company c on c.id = uc.company_id
Depending on the Hibernate version, the join for the company might even be omitted.
I eventually figured out the solution and want to post it here, in case anyone stumbles upon this question. This was purely a mistake on my side and is not reproducible from the examples I posted.
I used lombok annotations to generate equals and hashcode methods on the customer entity (and all other entities for that matter) and forgot to annotate the contactPerson and transactions fields with #EqualsAndHashcode.Exclude. As the equals method was called somewhere along the execution, it triggered the lazy loading of those fields. Implementing equals and hashcode manually and using the guidelines from this article for that solved the problem.

Fetch join to attribute from another object using Spring Specification

I have the following entities:
#Entity
#Table(name = "user_data")
public class UserData {
...
#ManyToOne
private User user;
...
}
#Entity
#Table(name = "user_cars")
public class UserCar {
...
private Integer userId;
...
}
#Entity
#Table(name = "users")
public class User {
...
#OneToMany(mappedBy = "userId", cascade = CascadeType.ALL)
private List<UserCar> userCars;
...
}
As you can see, userCars are loaded lazily (and I am not going to change it). And now I use Specifications in order to fetch UserData:
public Page<UserData> getUserData(final SpecificationParameters parameters) {
return userDataRepository.findAll(createSpecification(parameters), parameters.pageable);
}
private Specification<UserData> createSpecification(final SpecificationParameters parameters) {
final var clazz = UserData.class;
return Specification.where(buildUser(parameters.getUserId()));
}
private Specification<UserData> buildUser(final Integer userId) {
return (root, criteriaQuery, criteriaBuilder) -> {
if (Objects.nonNull(userId)) {
final Join<UserData, User> joinParent = root.join("user");
return criteriaBuilder.equal(joinParent.get("id"), userId);
} else {
return criteriaBuilder.isTrue(criteriaBuilder.literal(true));
}
};
}
But I have no idea how to add there a fetch join clause in order to fetch user cars. I tried to add it in different place and I got either LazyInitializationException (so it didn't work) or some other exceptions...
Slightly different approach from the prior answer, but I think the idea jcc mentioned is on point, i.e. "Hibernate is complaining because it it unable to find the owner, user in this case, of the userCars relationship."
To that end, I'm wondering if the Object-Relational engine is getting confused because you have linked directly to a userId (a primitive) instead of a User (the entity). I'm not sure if it can assume that "userId" the primitive necessarily implies a connection to the User entity.
Can you try to re-arrange the mapping so that it's not using an integer UserId in the join table and instead using the object itself, and then see if it allows the entity manager to understand your query better?
So the mapping might look something like this:
#Entity
#Table(name = "user_cars")
public class UserCar {
...
#ManyToOne
#JoinColumn(name="user_id", nullable=false) // Assuming it's called user_id in this table
private User user;
...
}
#Entity
#Table(name = "users")
public class User {
...
#OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<UserCar> userCars;
...
}
It would also be more in line with
https://www.baeldung.com/hibernate-one-to-many
In addition of the suggestion #crizzis provided in the question comments, please, try to join fetch the user relationship as well; in the error you reported:
org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list
Hibernate is complaining because it it unable to find the owner, user in this case, of the userCars relationship.
It is strange in a certain way because the #ManyToOne relationship will fetch eagerly the user entity and it will be projected as well while obtaining userData but probably Hibernate is performing the query analysis prior to the actual fetch phase. It would be great if somebody could provide some additional insight about this point.
Having said that, please, consider to set the fetch strategy explicitly to FetchType.LAZY in your #ManyToOne relationship:
#Entity
#Table(name = "user_data")
public class UserData {
...
#ManyToOne(fetch= FetchType.LAZY)
private User user;
...
}
Your Specification code can look like the following:
public Page<UserData> getUserData(final SpecificationParameters parameters) {
return userDataRepository.findAll(createSpecification(parameters), parameters.pageable);
}
private Specification<UserData> createSpecification(final SpecificationParameters parameters) {
final var clazz = UserData.class;
return Specification.where(buildUser(parameters.getUserId()));
}
private Specification<UserData> buildUser(final Integer userId) {
return (root, criteriaQuery, criteriaBuilder) -> {
// Fetch user and associated userCars
final Join<UserData, User> joinParent = (Join<UserData, User>)root.fetch("user");
joinParent.fetch("userCars");
// Apply filter, when provided
if (Objects.nonNull(userId)) {
return criteriaBuilder.equal(joinParent.get("id"), userId);
} else {
return criteriaBuilder.isTrue(criteriaBuilder.literal(true));
}
};
}
I did not pay attention to the entity relations themself previously, but Atmas give you a good advice indeed, it will be the more performant way to handle the data in that relationship.
At least, it would be appropriate to define the relationship between User and UserCars using a #JoinColumn annotation instead of mapping through a non entity field in order to prevent errors or an incorrect behavior of your entities. Consider for instance:
#Entity
#Table(name = "users")
public class User {
...
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "user_id")
private List<UserCar> userCars;
...
}

One to many relationship supporting reads & deletes but not inserts

I would like to extend the requirements mentioned in the earlier post to support deletes. We have two data model object - Organization & Department sharing a one-to-many relationship. With the below mapping I am able to read the list of departments from the organization object. I have not added the cascade ALL property to restrict adding a department when creating an organization.
How should I modify the #OneToMany annotation (and possibly #ManyToOne) to restrict inserts of department but cascade the delete operation such that all associated departments are deleted when deleting an organization object?
#Entity
#Table(name="ORGANIZATIONS")
public class Organization{
#Id
#GeneratedValue
Private long id;
#Column(unique=true)
Private String name;
#OneToMany(mappedBy = "organization", fetch = FetchType.EAGER)
private List<Department> departments;
}
#Entity
#Table(name="DEPARTMENTS")
Public class Department{
#Id
#GeneratedValue
Private long id;
#Column(unique=true)
Private String name;
#ManyToOne(fetch = FetchType.EAGER)
private Organization organization;
}
The code to delete the organization is just a line
organizationRepository.deleteById(orgId);
The test case to validate this is as below
#RunWith(SpringJUnit4ClassRunner.class)
#DataJpaTest
#Transactional
public class OrganizationRepositoryTests {
#Autowired
private OrganizationRepository organizationRepository;
#Autowired
private DepartmentRepository departmentRepository;
#Test
public void testDeleteOrganization() {
final organization organization = organizationRepository.findByName(organizationName).get(); //precondition
Department d1 = new Department();
d1.setName("d1");
d1.setorganization(organization);
Department d2 = new Department();
d2.setName("d2");
d2.setorganization(organization);
departmentRepository.save(d1);
departmentRepository.save(d2);
// assertEquals(2, organizationRepository.getOne(organization.getId()).getDepartments().size()); //this assert is failing. For some reason organizations does not have a list of departments
organizationRepository.deleteById(organization.getId());
assertFalse(organizationRepository.findByName(organizationName).isPresent());
assertEquals(0, departmentRepository.findAll().size()); //no departments should be found
}
}
See code comments on why it fails:
#RunWith(SpringJUnit4ClassRunner.class)
#DataJpaTest
#Transactional
public class OrganizationRepositoryTests {
#Autowired
private OrganizationRepository organizationRepository;
#Autowired
private DepartmentRepository departmentRepository;
#PersistenceContext
private Entitymanager em;
#Test
public void testDeleteOrganization() {
Organization organization =
organizationRepository.findByName(organizationName).get();
Department d1 = new Department();
d1.setName("d1");
d1.setOrganization(organization);
Department d2 = new Department();
d2.setName("d2");
d2.setOrganization(organization);
departmentRepository.save(d1);
departmentRepository.save(d2);
// this fails because there is no trip to the database as Organization
// (the one loaded in the first line)
// already exists in the current entityManager - and you have not
// updated its list of departments.
// uncommenting the following line will trigger a reload and prove
// this to be the case: however it is not a fix for the issue.
// em.clear();
assertEquals(2,
organizationRepository.getOne(
organization.getId()).getDepartments().size());
//similary this will execute without error with the em.clear()
//statement uncommented
//however without that Hibernate knows nothing about the cascacding
//delete as there are no departments
//associated with organisation as you have not added them to the list.
organizationRepository.deleteById(organization.getId());
assertFalse(organizationRepository.findByName(organizationName).isPresent());
assertEquals(0, departmentRepository.findAll().size());
}
}
The correct fix is to ensure that the in-memory model is always maintained correctly by encapsulating add/remove/set operations and preventing
direct access to collections.
e.g.
public class Department(){
public void setOrganisation(Organisation organisation){
this.organisation = organisation;
if(! organisation.getDepartments().contains(department)){
organisation.addDepartment(department);
}
}
}
public class Organisation(){
public List<Department> getDepartments(){
return Collections.unmodifiableList(departments);
}
public void addDepartment(Department departmenmt){
departments.add(department);
if(department.getOrganisation() != this){
department.setOrganisation(this);
}
}
}
Try this code,
#OneToMany( fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "organisation_id", referencedColumnName = "id")
private List<Department> departments;
#ManyToOne(fetch = FetchType.EAGER,ascade = CascadeType.REFRESH,mappedBy = "departments")
private Organization organization;
if any issue inform
You can try to add to limit the cascade to delete operations only from Organization to department:
#OneToMany(mappedBy = "organization", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Department> departments;
Please note that if you have dependents/foreign key constraints on the department entity, then you would need to cascade the delete operations to these dependent entities as well.
You can read this guide, it explains the cascade operations nicely:
https://vladmihalcea.com/a-beginners-guide-to-jpa-and-hibernate-cascade-types/

How to delete relation between two tables with one-to-many unidirectional

I have two entities bound with one-to-many relationship. But one entity can exist without the other. So the relationship is uni-directional. As this;
#Entity
public class TransportationOrderProduct {
#OneToMany(fetch = FetchType.EAGER,cascade = CascadeType.ALL)
private List<WarehousePackage> selectedWarehousePackages;
}
#Entity
public class WarehousePackage{
}
And hibernate created these tables;
TransportationOrderProduct
id
TransportationOrderProductSelectedWarehousePackages
transportationOrderProductId
selectedWarehousePackageId
WarehousePackage
id
When fetching the collection (selectedWarehousePackages) everything works fine.
But when I clear the TransportationOrderProduct.selectedWarehousePackages list, and add new ones, Hibernate throws DuplicateEntry exception. Saying that transportationOrderProductId in the TransportationOrderProductSelectedWarehousePackages table cannot be inserted twice.
I think this is because Hibernate doesn't delete the relation in the TransportationOrderProductSelectedWarehousePackages table when I call;
TransportationOrderProduct.selectedWarehousePackages.clear()
And add some entities after ;
TransportationOrderProduct.selectedWarehousePackages.add(newPackage)
TransportationOrderProduct.selectedWarehousePackages.add(newPackage)
.
.
Can somebody help?
Its sounds the relation is one-Many as I understand
Don’t use unidirectional one-to-many associations
avoid unidirectional one-to-many associations in your domain model. Otherwise, Hibernate might create unexpected tables and execute more SQL statements than you expected and this certainly described why hibernate create 3 entities while your implementation should be 2 entities with relation one-2-many.
The definition of uni-directional sounds not an issue in our case if we use directional it's OKAY it will serve our purpose you just need attribute to map our association
using annotation
#Entity
public class TransportationOrderProduct {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private long id;
#OneToMany(fetch = FetchType.EAGER,cascade = CascadeType.ALL)
private Set<WarehousePackage> packages= new HashSet<>();
public Set<WarehousePackage> getPackages() {
return packages;
}
public void setPackages(Set<WarehousePackage> packages) {
this.packages = packages;
}
public void addPackages (WarehousePackage value) {
this.packages.add(value);
}
public void clearPackages (WarehousePackage value) {
this.packages.clear();
}
...
}
#Entity
public class WarehousePackage{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private long id;
}
You can save transportation order with association you need
TransportationOrderProduct transportation = new TransportationOrderProduct ();
Set<WarehousePackage> packages = new HashSet <> () ;
WarehousePackage package1 = new WarehousePackage () ;
WarehousePackage package2 = new WarehousePackage () ;
packages.add(package1);
packages.add(package2);
transportation.setPackages(packages) ;
session.save(transportation);

Hibernate persist entity without fetching association object. just by id

I have an simple association between 2 entities:
public class Car {
...
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id")
private User user;
...
}
and
public class User {
#Id
#GeneratedValue
#Column(name = "user_id")
private long userId;
...
#OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
private Set<Car> cars;
...
}
Then I get some user id from client. For example, userId == 5;
To save car with user I need to do next:
User user = ... .findOne(userId);
Car car = new Car();
car.setUser(user);
... .save(car);
My question is: Can I persist car record without fetching user?
Similarly like I would do by using native SQL query: just insert userId like string(long) in Car table.
With 2nd lvl cache it will be faster but in my opinion I don't need to do extra movements. The main reason that I don't want to use native Query is because I have much more difficult associations in my project and I need to .save(car) multiple times. Also i don't want to manually control order of query executions.
If I use session.createSQLQuery("insert into .....values()") will the Hibernate's batch insert work fine?
Correct me if I'm wrong.
Thanks in advance!
UPDATE:
Actually the mapping is similar to:
There is #ManyToMany association between User and Car. But cross table is also an entity which is named, for example, Passanger. So the mapping is next:
public class User{
#OneToMany(fetch = FetchType.LAZY, mappedBy = "user", targetEntity = Passenger.class)
private Set<Passenger> passengers;
}
Cross entity
#IdClass(value = PassengerPK.class)
public class Passenger {
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id")
private User user;
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "car_id")
private Car car;
... other fields ...
}
Car entity:
public class Car {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "car", targetEntity = Passenger.class, cascade = CascadeType.ALL)
private Set<Passenger> passengers;
}
And the code:
List<User> users = ... .findInUserIds(userIds); // find user records where userId is IN userIds - collection with user Ids
Car car = new Car(); //initialization of car's fields is omitted
if (users != null) {
car.setPassengers(new HashSet<>(users.size()));
users.forEach((user) -> car.getPassengers().add(new Passenger(user, car)));
}
... .save(car);
"Can I persist car record without fetching user?"
Yes, that's one of the good sides of Hibernate proxies:
User user = entityManager.getReference(User.class, userId); // session.load() for native Session API
Car car = new Car();
car.setUser(user);
The key point here is to use EntityManager.getReference:
Get an instance, whose state may be lazily fetched.
Hibernate will just create the proxy based on the provided id, without fetching the entity from the database.
"If I use session.createSQLQuery("insert into .....values()") will the Hibernate's batch insert work fine?"
No, it will not. Queries are executed immediately.
If someone is using Spring Data JPA: The same can be achieved in Spring Data JPA can be done using the method
JpaRepository.getReferenceById(ID id)
This replaced the former
getOne(ID)
Hibernate users can implement this method:
public <T extends Object> T getReferenceObject(Class<T> clazz, Serializable id) {
return getCurrentSession().get(clazz, id);
}
And call like:
MyEntity myEntity = getRefererenceObject(MyEntity.class, 1);
You can change id type to Integer or Long as per your entity model.
Or T can be inherited from your BaseEntity if you have one base class for all entities.
The following approach works for me:
User user = new User();
user.setId(userId);
car.setUser(user);

Categories