Hibernate, projection and eagerly fetching a collection - java

I have two Entities
#Entity
#Table(name = "steps")
public class Step {
#Id
#Column(nullable = false, insertable = true, updatable = true)
private long id;
//more fields
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "result_id", referencedColumnName = "id")
private Result result;
and
#Entity
#Table(name = "results")
public class Result {
#Id
#Column(nullable = false, insertable = true, updatable = true)
private long id;
//more fields
#OneToMany(mappedBy = "result", fetch = FetchType.EAGER)
private List<Step> steps;
Now I need to get all results and their associated steps.
I will want all the steps so an eager fetch seems best, and I know all results will have steps so an inner join will be fine.
I will want to get only some more fields from results, but not all so I can maximize the use of an index, so projection will be needed.
To accomplish this I have tried two methods:
Query query = getSession().createQuery(
"select distinct result from Result as result " +
"inner join result.steps " +
"where result.monitorId = :id " +
"and result.completed between :from and :to ");
query.setLong("id", monitorId);
query.setTimestamp("from", from);
query.setTimestamp("to", to);
for (Result result : query.list()) {
result.getSteps()
//work work
}
Hibernate does a join like I wanted but when I start to iterate over the results, Hibernate logs one more select query per step I'm using.
(I also haven't found a good way for projection if I'm going this route?)
The second approach I have tried seems great so far:
Criteria criteria = getSession().createCriteria(Result.class);
criteria.setProjection(Projections.projectionList()
.add(Projections.property("completed"), "completed")
.add(Projections.property("steps"), "steps"));
criteria.add(Restrictions.eq("someId", someId));
criteria.add(Restrictions.between("completed", from, to));
criteria.setFetchMode("steps", FetchMode.JOIN);
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
criteria.setResultTransformer(Transformers.aliasToBean(Result.class));
The projections work great as long as I don't include steps. I'm guessing that has to do with it not belonging to the actual database table? When I do try with steps I get
java.lang.ArrayIndexOutOfBoundsException: 2
on criteria.list();
So my main question is, how do I best do eager fetching of some but not all columns in results + a collection of steps belonging to each result?
Help please? :)

Related

Java Spring JPA Reference and return only one result from join table

I believe the code below is working correctly but my logic is flawed and so I'm looking for help getting the results I actually want. In addition I think my attempt on the JPA method is overly convoluted and there's likely a cleaner solution. I know I can likely use #Query but I want to try and get the method name solution working as well so I can learn where I went wrong.
For simplicity I've stripped back the DB tables in this example, however I have Table1 which is the top level table and Table2 which sits below it and has a foreign key back to Table1.
Table 1 -> Table 2 is a one to many relationship, I am looking to write a JPA method that will pull back a record from Table 1 when given the Table1 ID and only the current effective record from Table2 (this will be where EffectiveDateTime column is most recent but NOT future dated). Using the example below I want it to only pull back ID 8.
Unfortunately, I believe with the code I have below it recognises one of those records for Table2 is within the date range required and is therefore pulling all the records back that have a relation with the ID from Table 1. I could be wrong on this though and the logic could be flawed in a different way.
Any help would be appreciated.
Table1 Entity Class
#Entity
#Table(name = "TABLE1")
public class Table1 {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID", nullable = false, unique = true)
private Integer id;
#OneToMany(mappedBy = "table1", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#Fetch(value = FetchMode.SUBSELECT)
#JsonManagedReference
private List<Table2> table2;
//Plus getters & setters
Table2 Entity Class
#Entity
#Table(name = "TABLE2")
public class Table2{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID", nullable = false, unique = true)
private Integer id;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "Table1Id")
#JsonBackReference
private Table1 table1;
#Column(name = "EffectiveDateTime", nullable = false)
private LocalDateTime effectiveDateTime;
//Plus getters & setters
Repo Class
#Repository
public interface Repository extends JpaRepository<Table1, Integer>, JpaSpecificationExecutor<Table1> {
public Optional<Table1> findTopByIdAndTable2EffectiveDateTimeLessThanEqualOrderByTable2EffectiveDateTimeDesc(int id, LocalDateTime now);
}
Current JSON return
{
"id": 5
"Table2": [
{
"id": 6,
"effectiveDateTime": "2021-01-01T00:00:01"
},
{
"id": 8,
"effectiveDateTime": "2022-01-01T00:00:01"
},
{
"id": 9,
"effectiveDateTime": "2023-01-01T00:00:01"
}
]
}
Once you declared a oneToMany relationship, your entity will load all joined entity when calling the getTable method (wich is called by jackson when serializing). I believe if you activate debug mode, you will see 2 request, the first will be the one you describe in your repopsitory, the second the one JPA use to load all related table2 entity.
Their is some possible ways to accomplish what you want:
the first (and most obvious ?) is to manually make the 2 request equivalent to your HQL request (the table 1 find by ID and then the table 2 find by....) and then aggregate the 2 results
the second one would be to use jackson to serialize your data how you want to (by filtering useless table 2 data)
the third one would be to use jackson to serialize a table2 (wich give you the corresponding table1 data) into the appropriate JSON. It's a bit more complicated but probably more efficient if your table1 reference a lot of table2 data.
The last one would be to try using projections it allow you to declare only the data you want to retrieve when requesting data.
hope this help !
In the end we completed this using a JPQL query. An example of the query is below in case it helps any others in the future.
#Query(value = "SELECT table1 FROM Table1 table1 "
+ " JOIN FETCH table1.table2 table2 "
+ " WHERE table1.id = :table1Id"
+ " AND table2.id = "
+ " ( SELECT table2.id FROM table2 "
+ " WHERE table2.table1.id = :table1Id "
+ " AND table2.effectiveDateTime = "
+ " ( SELECT MAX(effectiveDateTime) FROM table2"
+ " WHERE table2.table1.id = :table1Id "
+ " AND table2.effectiveDateTime <= :currentTimestamp "
+ " ) "
+ " ) ")
Optional<Table1> getEffectiveDateTimeTable2(#Param("table1Id") int table1Id, #Param("currentTimestamp") LocalDateTime currentTimestamp);

JPQL, get a list of objects which have a child list, avoid n+1 requests and only choose specific fields

I have the following entity Project:
#Data
#NoArgsConstructor
#Entity
#ToString(exclude = "roles")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Project {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#Column(unique = true)
private String name;
private String description;
private Boolean isArchived;
private LocalDate archivedDate;
private LocalDate creationDate;
#Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT FALSE")
private Boolean invoicingActivated;
#ManyToOne
#NotNull
private Order order;
#OneToOne(cascade = CascadeType.ALL)
private DefaultDailyEntrySettings defaultDailyEntrySettings;
#OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ProjectEmployee> projectEmployees;
}
I want to get all projects. Each project should also have their list of projectEmployees.
Thats the entity ProjectEmployee:
#Data
#Table(uniqueConstraints = {#UniqueConstraint(columnNames = {"employee_id", "project_id"})})
#NoArgsConstructor
#ToString(exclude = "project")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class ProjectEmployee {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToOne
#JsonIgnore
#JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
#NotNull
private Project project;
#ManyToOne
#NotNull
private Employee employee;
#ManyToOne
private ProjectEmployeeRole projectEmployeeRole;
}
To avoid n+1 queries, i wrote the following query:
#Query("SELECT project FROM Project project JOIN FETCH project.order ord JOIN FETCH ord.customer " +
"LEFT JOIN FETCH project.projectEmployees projectEmployee LEFT JOIN FETCH project.defaultDailyEntrySettings " +
"LEFT JOIN FETCH projectEmployee.employee LEFT JOIN FETCH projectEmployee.projectEmployeeRole")
List<Project> findAllProjectsInOneQuery();
This works, but it returns all properties of every single object. For example i´m only interested in the id and the name of ord.customer, i dont need all the other fields of ord.customer in this case. The problem with getting all the fields this way is that theres a lot of data being transmitted which i do not need in this case. To only select the ones i need and reduce the amount of data im sending over the internet i could do something like this:
#Query("SELECT new de.project.Project(project.id, project.name, " +
"project.description, project.isArchived, project.archivedDate, " +
"project.creationDate, project.invoicingActivated, project.order.id, " +
"project.order.name, project.order.customer.id, project.order.customer.name) " +
"FROM Project project")
List<Project> findAllMinimal();
But as you see, i can´t get project.projectEmployees that way, because it´s a list and i do not think im able to pass a list via the constructor this way.
I tried:
#Query("SELECT new de.project.Project(project.id, project.name, " +
"project.description, project.isArchived, project.archivedDate, " +
"project.creationDate, project.invoicingActivated, project.order.id, " +
"project.order.name, project.order.customer.id, project.order.customer.name, " +
"projectEmployee.id) " +
"FROM Project project JOIN project.projectEmployees projectEmployee")
List<Project> findAllMinimal();
But projectEmployee.id is just the id of the first projectEmployee, i do not think im able to pass all projectEmployees this way.
Is there any way to get all projects with their projectEmployees(and the other properties i listed in the query above) and specify which fields i would like to get? It does not have to be one query, a constant number of queries is fine. It should obviosuly just avoid n+1 queries.
Edit:
I came up with a workaround. Im using the following two queries:
#Query("SELECT new de.project.Project(project.id, project.name, " +
"project.description, project.isArchived, project.archivedDate, " +
"project.creationDate, project.invoicingActivated, project.order.id, " +
"project.order.name, project.order.customer.id, project.order.customer.name) " +
"FROM Project project")
List<Project> findAllMinimal();
#Query("SELECT DISTINCT new de.projectemployee.ProjectEmployee(projectEmployee.id, " +
"projectEmployee.employee.id, projectEmployee.employee.email, " +
"projectEmployee.employee.firstName, projectEmployee.employee.lastName, " +
"projectEmployee.employee.address, projectEmployee.employee.weeklyHoursEnabled, " +
"projectEmployee.employee.weeklyHours, projectEmployee.employee.isArchived, " +
"projectEmployee.employee.archivedDate, projectEmployee.project.id, projectEmployeeRole.id, " +
"projectEmployeeRole.name, projectEmployeeRole.hourlyWage) FROM ProjectEmployee projectEmployee " +
"LEFT JOIN projectEmployee.projectEmployeeRole projectEmployeeRole " +
"WHERE projectEmployee.project IN :projects")
List<ProjectEmployee> findByProjects(#Param("projects") List<Project> projects);
To give every project his projectEmployees i need some additional java code:
List<Project> projects = projectRepository.findAllMinimal();
List<ProjectEmployee> projectEmployees = projectEmployeeRepository.findByProjects(projects);
Map<Long, List<ProjectEmployee>> projectIdToProjectEmployeesMap = new HashMap<>();
for (ProjectEmployee projectEmployee : projectEmployees) {
List<ProjectEmployee> projectEmployeesToBeSaved = projectIdToProjectEmployeesMap.getOrDefault(projectEmployee.getProject().getId(), new ArrayList<>());
projectEmployeesToBeSaved.add(projectEmployee);
projectIdToProjectEmployeesMap.put(projectEmployee.getProject().getId(), projectEmployeesToBeSaved);
}
projects.forEach(project -> project.setProjectEmployees(projectIdToProjectEmployeesMap.get(project.getId())));
return projects;
So, ye as you see im able to achieve my goal of getting all projects with their projectEmployees in a constant number of queries(2) and only select the fields i need. The downside is that i have a javacode running with O(n) complexity. But i reduced the size of the data im transmitting by over 90%, so i guess its worth.
It´s hard to believe that a java code algorithm like the one i used is required to find a solution for my problem, so if anyone finds a better solution(with just sql queries) which is capable of doing the above stated, please share it.
Didn't verify it for your case, but at a first glance you may take advantage of the first level cache. After you perform
List<Project> projects = projectRepository.findAllMinimal();
List<ProjectEmployee> projectEmployees = projectEmployeeRepository.findByProjects(projects);
Any access to projectEmployee.getProject() should return its project from the cache avoiding n+1 problem. So if you can leave project.projectEmployees List unpopulated, you can drop your "custom javacode".

Unexpected JavaAssistLazyInitializer even with INNER JOIN FETCH Query

I need to fetch a nested field called tabPremissa into Premissa model, but I can't handle JavaAssistLazyInitializer.
I Already tried
unproxy the lazy field with ((HibernateProxy) entity).getHibernateLazyInitializer().getImplementation()
INNER JOIN FETCH on JpaRepository method
The code is as follows:
Premissa.java
#Entity
#Table(name = "premissa")
public class Premissa implements Draggable {
#Id
#SequenceGenerator(name = SEQ, sequenceName = SEQ, allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQ)
#Column(name = "id")
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "id_subcategoria_premissa", nullable=false)
private SubCategoriaPremissa subCategoriaPremissa;
}
SubCategoriaPremissa.java
#Entity
#Table(name = "subcategoria_premissa")
public class SubCategoriaPremissa implements Draggable {
#Id
#SequenceGenerator(name = SEQ, sequenceName = SEQ, allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQ)
#Column(name = "id")
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "id_tab_premissa", nullable=false)
private TabPremissa tabPremissa;
}
#Query
String QUERY_TAB_ORDER_BY_TAB_AND_ORDER = " SELECT P From Premissa P "
+ " INNER JOIN FETCH P.subCategoriaPremissa SCP "
+ " INNER JOIN FETCH SCP.tabPremissa TP "
+ " WHERE TP in :tabs "
+ " ORDER BY SCP.tabPremissa, P.ordem ";
Hibernate log
select
premissa0_.id as id1_70_0_,
subcategor1_.id as id1_85_1_,
tabpremiss2_.id as id1_86_2_,
premissa0_.campo_1 as campo_2_70_0_,
premissa0_.campo_2 as campo_3_70_0_,
premissa0_.campo_3 as campo_4_70_0_,
premissa0_.campo_4 as campo_5_70_0_,
premissa0_.id_centro_custo as id_cent15_70_0_,
premissa0_.considera_zero as consider6_70_0_,
premissa0_.descricao as descrica7_70_0_,
premissa0_.id_empresa as id_empr16_70_0_,
premissa0_.id_grupo_economico as id_grup17_70_0_,
premissa0_.id_clone as id_clone8_70_0_,
premissa0_.logica_totalizador as logica_t9_70_0_,
premissa0_.nome as nome10_70_0_,
premissa0_.ordem as ordem11_70_0_,
premissa0_.id_pai as id_pai18_70_0_,
premissa0_.style_table as style_t12_70_0_,
premissa0_.id_subcategoria_premissa as id_subc19_70_0_,
premissa0_.tipo_operacao_premissa as tipo_op13_70_0_,
premissa0_.unidade_medida as unidade14_70_0_,
subcategor1_.id_clone as id_clone2_85_1_,
subcategor1_.label_1 as label_3_85_1_,
subcategor1_.label_2 as label_4_85_1_,
subcategor1_.label_3 as label_5_85_1_,
subcategor1_.label_4 as label_6_85_1_,
subcategor1_.nome as nome7_85_1_,
subcategor1_.ordem as ordem8_85_1_,
subcategor1_.id_pai as id_pai9_85_1_,
subcategor1_.id_tab_premissa as id_tab_10_85_1_,
tabpremiss2_.id_categoria_premissa as id_categ8_86_2_,
tabpremiss2_.definicao_json as definica2_86_2_,
tabpremiss2_.enum_link_id as enum_lin3_86_2_,
tabpremiss2_.hexa_bg_color as hexa_bg_4_86_2_,
tabpremiss2_.nome as nome5_86_2_,
tabpremiss2_.ordem as ordem6_86_2_,
tabpremiss2_.id_pai as id_pai9_86_2_,
tabpremiss2_.status_edit as status_e7_86_2_
from
premissa premissa0_
inner join
subcategoria_premissa subcategor1_
on premissa0_.id_subcategoria_premissa=subcategor1_.id
inner join
tab_premissa tabpremiss2_
on subcategor1_.id_tab_premissa=tabpremiss2_.id
where
tabpremiss2_.id in (
? , ?
)
order by
subcategor1_.id_tab_premissa,
premissa0_.ordem
EDIT
I searched one example directly on database where Premissa p has a SubcategoriaPremissa s and ran the following commands:
s = subCategoriaPremissaRepository.findOne(1883L);
p = premissaRepository.findOne(9019L);
In this case, every data is loaded properly and s is located in p.
However, if the order of execution is changed, s is considered JavaAssistLazyInitializer while debugging
The problem you are most likely having is that Hibernate is unable to merge fetched state into existing objects already present in your first level cache a.k.a. your EntityManager/Session. So if you somehow get an entity proxy into your EntityManager/Session, it doesn't matter if subsequent queries fetch that entity, the result will always be the proxy. Hibernate must adhere to the JPA specification that roughly says, managed entities with the same primary key must only exist once object-identity-wise in an EntityManager/Session.
This is IMO one of the biggest reasons to avoid the OSIV(Open-Session-In-View) anti-pattern, but is rarely talked about. Hibernate might at some point fix this, but for now, this is how it behaves.
So one thing you can do right now is to invoke EntityManager.clear() to clear the whole first level cache or EntityManager.detach() with the problematic entity as argument, to remove just that from the first level cache.

Get if a user is following element in a query with JPA / Hibernate

In a spring boot + JPA applition, I have a user table and a item table with a ManyToMany relationship. The users can "follow" any Item element, in a twitter/facebook style to get the posts related to that item, get notificacions when an item change, etc.
The classes are like this, very simple way:
UserEntity.java
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Size(max = 50)
#Column(name = "username", length = 50)
private String username;
#ManyToMany
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#JoinTable(name = "user_follow_item",
joinColumns = #JoinColumn(name="user_id", referencedColumnName="id"),
inverseJoinColumns = #JoinColumn(name="follow_item_id", referencedColumnName="id"))
private Set<Items> followItems = new HashSet<>();
ItemEntity.java
#ManyToMany(mappedBy = "followItems")
private Set<User> followUsers = new HashSet<>();
The query is made with JpaRepository repository classes and exposed in spring boot throught a #RestController class and all works just fine.
The question is, in a query looking for items by id, it is possible to get if the current user (the logged user) is following the item? All the data in one single query. With this query I get the item info, but I don't know how to get if the user if following the item:
#Query("select item from Item item left join item.followUsers userFollow on userFollow.login = ?#{principal.username} where item.id = :id ")
Item itemWithCurrentUserFollows(#Param("id") Long id);
The query in native SQL is something like this (when i get data in "follow" part I know that the user is following the item) but I don't know how to realice this in a JPA query:
SELECT * FROM item left join (user user join user_follow_item follow) on user.id = follow.user_id and follow.follow_item_id = item.id and user.username = 'mockUser' where item.id = 1;
Any ideas are welcome (#Formula, a extra boolean value with this info, etc...)
Many Thanks!

NamedNativeQuery in Hibernate generates many select statements? How get referenced entities in a batch-way?

I thought I understood hibernate's fetching strategies, but it seems I was wrong.
So, I have an namedNativeQuery:
#NamedNativeQueries({
#NamedNativeQuery(
name = "getTest",
resultClass = ArticleOnDate.class,
query = "SELECT `a`.`id` AS `article_id`, `a`.`name` AS `name`, `b`.`price` AS `price` FROM article a LEFT JOIN price b ON (a.id = b.article_id) WHERE a.date <= :date"
)
})
#Entity()
#Immutable
public class ArtikelOnDate implements Serializable {
#Id
#OneToOne
#JoinColumn(name = "article_id")
private Article article;
...
}
Then I call it:
Query query = session.getNamedQuery("getTest").setDate("date", date);
List<ArticleOnDate> list = (List<ArticleOnDate>) query.list();
The query returns thousand of entities... Well, ok, but after that query hibernate queries thousand other queries:
Hibernate:
select
article0_.id as id1_0_0_,
article0_.bereich as name2_0_0_,
price1_.price as price1_14_1_
from
article artikel0_
where
artikel0_.id=?
Ok, that's logic, because the #OneToOne relation is fetched eagerly. I don't want to fetch it lazy, so I want a batch fetching strategy.
I tried to annotate the Article property but it didn't work:
#Id
#OneToOne
#JoinColumn(name = "article_id")
#BatchSize(size=100)
private Article article;
So what can I do to fetch the relation in a batch?

Categories

Resources