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.
Related
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);
I have an issue join fetching in case of OneToOne relation in the same class. Example follows:
class Data {
...
#Id
#Column(name = "DATA_ID")
Long id;
#Column(name = "DATA_OWNER_ID")
#ForeignKey(entityClass = Owner.class)
Long ownerId;
#Column(name = "DATA_RELATED_ID")
#ForeignKey(entityClass = Data.class)
Long relatedDataId;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "DATA_RELATED_ID", referencedColumnName = "DATA_ID", insertable = false, updatable = false)
Data relatedData;
}
I want to select data based on some conditions, while also fetching/initialising the "relatedData", all in one JPQL query:
SELECT owner.something1, data
FROM Data data
JOIN Owner owner on data.ownerId = owner.id
JOIN FETCH data.relatedData
WHERE data.something2 = :expectedSomething2
Executing that JPQL query throws an exception:
Query: ReadObjectQuery(name="relatedData" referenceClass=Data)|Exception:
javax.persistence.PersistenceException: Exception [EclipseLink-6044] (Eclipse Persistence Services - 2.6.2.v20151217-774c696): org.eclipse.persistence.exceptions.QueryException
Exception Description: The primary key read from the row [DatabaseRecord(
DATA_X => something
DATA_Y => something2
...
)] during the execution of the query was detected to be null. Primary keys must not contain null.
Which is somewhat true, as there is no DATA_ID column listed. Changing JOIN FETCH to LEFT JOIN FETCH returns both owner.something1 and data, but the relatedData object is null (relatedDataId is not null).
I can see, that the id for relatedData is returned from DB, but eclipselink trims it in valueFromRowInternalWithJoin and trimRowForJoin methods.
The Id column name attribute value is the reason of this exception. Same issue found in eclipselink version 2.3.2 but it works fine in version 2.0.0
Try with this entry :
eclipselink.jpa.uppercase-column-names=true
OR Try with upper and lower case one by one which on will work for you.
#Id
#Column(name = "UUID") // UUID - uppercase/lowercase one by one
Long id;
I've somehow resolved this issue, but haven't had the time to correctly identify the cause. Final (working) version differences are:
I could've forgotten to add get/set for relatedData
I have specified targetEntity = Data.class in #OneToOne
Fetch is now a LEFT JOIN FETCH and appears before JOIN Owner owner
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".
I am trying to do a bulk delete.
Query queryDeleteEntries = entityManager.createQuery("DELETE from
Entry r where (r.person.emailAddress like :name) and (r.otherData is null)");
int deletedEntriesCount = queryDeleteEntries.setParameter("name",
"tester." + emailAddressSuffix).executeUpdate();
I am getting an error:
Caused by: java.sql.SQLException: ORA-00933: SQL command not properly ended
at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:112) ~[DatabaseError.class:Oracle JDBC Driver version - "10.2.0.2.0"]
Without "and r.otherData is null" it works, so I don't see what the problem is with that?
otherData is a list and it is defined in the Entry class that way:
#OneToMany(fetch = FetchType.EAGER)
#JoinColumn(name = "ENTRY_ID", referencedColumnName = "ID")
private List<OtherData> otherData;
OtherData (name changed) class:
#Entity
#Table(name = "OTHERDATA")
#SequenceGenerator(name = "SEQ", allocationSize = 1, sequenceName = "SEQ_OTHERDATA_ID")
public class OtherData {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ")
private Long id;
...
#Column(name = "ENTRY_ID")
private Long entryId;
Can I not use "is null" on lists?
EDIT
I tried it with "size(r.otherData) = 0" and with "r.otherData IS EMPTY", but both led to the same exception.
What DOES work is this:
Query queryDeleteEntries = entityManager.createQuery("DELETE from Entry r where id in "
+ "(select id from Entry r2 where r2.person.emailAddress like :name"
+ " and not exists (select 1 from OtherData a where a.entryId = r2.id))");
int deletedEntriesCount = queryDeleteEntries.setParameter("name", "tester."+emailAddressSuffix).executeUpdate();
But I think it shouldn't be that complicated! Isn't there an easier way to do that?
Thanks in advance for any hints!
I would recommend taking the generated query and see how hibernate is translating to native Oracle query, by logging the hibernate packages or simply make add this to hibernate.cfg.xml :
<property name="show_sql">true</property>
also try to add .otherData.idOtherData just to see the behaviour.
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? :)