In my spring boot application, I have two entities: ScheduledLegEntity and BookingLegEntity.
#Entity
#Table(name="SCHEDULED_LEG")
public class ScheduledLegEntity {
// ...
#OneToMany(mappedBy = "getOn", cascade = CascadeType.MERGE)
private List<BookingLegEntity> enteringBookingLegs;
#OneToMany(mappedBy = "getOff", cascade = CascadeType.MERGE)
private List<BookingLegEntity> exitingBookingLegs;
// ...
}
#Entity
#Table(name = "BOOKING_LEG")
public class BookingLegEntity {
// ...
#ManyToOne(cascade = CascadeType.MERGE)
#JoinColumn(name = "GET_ON")
private ScheduledLegEntity getOn;
#ManyToOne(cascade = CascadeType.MERGE)
#JoinColumn(name = "GET_OFF")
private ScheduledLegEntity getOff;
// ...
}
Now, I need to query my database for a specific set of scheduled legs and I also need to work with their enteringBookingLegs and exitingBookingLegs. I quickly stumbled accross this exception:
rg.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: de.mopla.server.schedule.entities.ScheduledLegEntity.enteringBookingLegs, could not initialize proxy - no Session
I tried solving this (and the subsequent MultipleBagFetchException) problem by following this guide on baeldung ("8. Ideal Solution: Using Multiple Queries
") like this:
public class CustomScheduledLegRepositoryImpl implements CustomScheduledLegRepository {
#PersistenceContext
private EntityManager entityManager;
#Override
public List<ScheduledLegEntity> customFindMethod(){
List<ScheduledLegEntity> legs = entityManager.createQuery(
"select distinct p from ScheduledLegEntity p left join fetch p.enteringBookingLegs", ScheduledLegEntity.class)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();
legs = entityManager.createQuery("select distinct p from ScheduledLegEntity p left join fetch p.exitingBookingLegs t where p in :legs", ScheduledLegEntity.class)
.setParameter("legs", legs)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();
return legs;
}
}
But even with those two queries, only either enteringBookingLegs or exitingBookingLegs are empty (see screenshots from debugging). How can I solve this problem (efficiently)?
As you can see, only one of the lists can be accessed, first exitingBookingLegs and then enteringBookingLegs, but never both at the same time.
Related
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
I have entity Workflow which has #OneToMany relation with ValidationResults class. It's fetch Lazy but sometimes I would like to get all the Workflows and interate on them accessing the ValidationResults. In that moment I want jpa to get all the data eagerly not query each time I access ValidationResults. I use springDataJpa, How to do it, is there any way to do it with #Query ?
I try to achieve something like that but I don't know how
//here all the workflows has corresponding data eagerly
List<Workflow> workflows = workflowService.getAllWorkflowsWithValidationResultsEagerly();
//here validationResults ref is lazy, when I try to access it it does query
List<Workflow> workflows = workflowService.getAllWorkflowsUsually();
Here are my entities.
#Entity
#Table(name = "workflow")
public class Workflow {
..............
#OneToMany(fetch = FetchType.LAZY, mappedBy = "workflow", cascade = CascadeType.ALL)
private Set<ValidationResults> validationResultsSet = new HashSet<>();
public Set<ValidationResults> getValidationResultsSet(){return this.validationResultsSet;}
...............
}
And ValidationResult class
#Entity
#Table(name = "validation_results")
public class ValidationResults {
...
#ManyToOne
#JoinColumn(name = "workflow_id", insertable = false, updatable = false)
private Workflow workflow;
....
}
The spring boot-ish way of doing this is by using the #EntityGraph as described in the documentation.
You can use fetch join in order to do it on #Query https://www.logicbig.com/tutorials/java-ee-tutorial/jpa/fetch-join.html
#Query("SELECT DISTINCT e FROM Employee e INNER JOIN FETCH e.tasks t")
If you don't want to create another query, just call .size() of your list
I have 2 entities with #Where annotation. First one is Category;
#Where(clause = "DELETED = '0'")
public class Category extends AbstractEntity
and it has the following relation;
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "category")
private Set<SubCategory> subCategories = Sets.newHashSet();
and second entity is SubCategory;
#Where(clause = "DELETED = '0'")
public class SubCategory extends AbstractEntity
and contains corresponding relation;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "CATEGORY_ID")
private Category category;
Whenever I call the following Dao method;
#Query(value = "select distinct category from Category category join fetch category.subCategories subcategories")
public List<Category> findAllCategories();
I got the following sql query;
select
distinct category0_.id as id1_3_0_,
subcategor1_.id as id1_16_1_,
category0_.create_time as create2_3_0_,
category0_.create_user as create3_3_0_,
category0_.create_userip as create4_3_0_,
category0_.deleted as deleted5_3_0_,
category0_.update_time as update6_3_0_,
category0_.update_user as update7_3_0_,
category0_.update_userip as update8_3_0_,
category0_.version as version9_3_0_,
category0_.name as name10_3_0_,
subcategor1_.create_time as create2_16_1_,
subcategor1_.create_user as create3_16_1_,
subcategor1_.create_userip as create4_16_1_,
subcategor1_.deleted as deleted5_16_1_,
subcategor1_.update_time as update6_16_1_,
subcategor1_.update_user as update7_16_1_,
subcategor1_.update_userip as update8_16_1_,
subcategor1_.version as version9_16_1_,
subcategor1_.category_id as categor11_16_1_,
subcategor1_.name as name10_16_1_,
subcategor1_.category_id as categor11_3_0__,
subcategor1_.id as id1_16_0__
from
PUBLIC.t_category category0_
inner join
PUBLIC.t_sub_category subcategor1_
on category0_.id=subcategor1_.category_id
where
(
category0_.DELETED = '0'
)
Could you please tell me why the above query lacks
and subcategor1_.DELETED = '0'
inside its where block?
I have just solved a similar problem in my project.
It is possible to put #Where annotation not only on Entity, but on also on your child collection.
According to the javadoc:
Where clause to add to the element Entity or target entity of a collection
In your case, it would be like :
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "category")
#Where(clause = "DELETED = '0'")
private Set<SubCategory> subCategories = Sets.newHashSet();
Please find a similar issues resolved here
I believe thus solution is not as invasive compared to using Hibernate Filters.These filters are disabled by default and operate on Session level, thus enabling them each time new Session opens is extra work especially when your DAO works through abstractions like Spring Data
This is a quick reply;
#Where(clause = "DELETED = '0'")
public class SubCategory extends AbstractEntity
Where will effect when direct query for SubCategry.
To not get deleted sub categories use Hibernate Filters
as exampled on here
I got a question regarding hibernate.
I have to classes with many to many relationship and try to make a select. The problem is I am getting an exception. Can you help?
#Entity(name = "pracownik")
#Inheritance(strategy = InheritanceType.JOINED)
#Proxy(lazy = false)
public class Pracownik extends Osoba {
#ManyToMany(fetch = FetchType.EAGER, cascade = { CascadeType.ALL })
#JoinTable(name = "grafikpracownika", joinColumns = { #JoinColumn(name = "idosoby") },
inverseJoinColumns = { #JoinColumn(name = "idgrafiku") })
private List<Grafik> grafiki = new ArrayList<>();
}
The second entity
#Entity (name = "grafik")
#Proxy(lazy = false)
public class Grafik {
#ManyToMany (mappedBy = "grafiki",cascade = { CascadeType.ALL })
private List <Pracownik> pracownik = new ArrayList<>();
}
The method I developed is:
public List<Pracownik> getWszyscyPracownicyGrafiku() {
List<Pracownik> pracownicy = new ArrayList<>();
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
Transaction transaction = session.beginTransaction();
Query query = session.createQuery("from pracownik as p join p.grafiki");
pracownicy = query.list();
transaction.commit();
return pracownicy;
}
And the exception is:
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to model.Pracownik
Any idea what is wrong?
I also would like to add a "where" but it should be easy after I get rid of this exception.
I also tried with "normal" sql
SELECT * from pracownik p join grafikpracownika g on p.idosoby = g.idosoby where idgrafiku = 6
but what I am getting is:
org.hibernate.loader.custom.NonUniqueDiscoveredSqlAliasException: Encountered a duplicated sql alias [idosoby] during auto-discovery of a native-sql query
You're selecting from two tables without explicit select clause, therefore Hibernate produces a list of tuples (Pracownik, Grafik) as a result.
If you want only Pracowniks (i.e. join is needed to create condition on p.grafiki in where), use
select distinct p from pracownik as p join p.grafiki
If join is used to instruct Hibernate to fetch associated Grafiks, use join fetch:
from pracownik as p join fetch p.grafiki
I have two entities:
#Entity
#Table(name = "ACCOUNT")
#Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class MyCloudAccount implements Serializable {
...
#OneToMany(mappedBy = "account", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Set<ServerInstance> servers = new HashSet<ServerInstance>();
...
}
#Entity
#Table(name = "SERVER_INSTANCE")
public class ServerInstance implements Serializable {
...
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "ACCOUNT_ID")
private MyCloudAccount account;
...
}
I am getting all accounts by this code:
StringBuilder sql = new StringBuilder();
sql.append("SELECT e FROM ");
sql.append(persistentClass.getName());
sql.append(" e");
return entityManager.createQuery(sql.toString()).getResultList();
And this produces one query for the account and N queries for the servers instead of one with outer join. How to force JPA to make the query in optimal way?
I find it more convenient to use Java Persistence Query Language
you can do:
#NamedQueries{
#NamedQuery(name="myQuery" query="SELECT a FROM MyCloudAccount JOIN FETCH a.servers")
}
public class MyCloudAccount{
...
}
then you can do
TypedQuery<MyCloudAccount> query = em.createNamedQuery("MyCloudAccount.myQuery", MyCloudAccount.class);
List<MyCloudAccount> results = query.getResultList();
EDIT
You are actually already using JPQL. The key thing to your problem is using the JOIN FECTH command.