How to make JPA use a single join to get columns with conditions for both sides - spring

My problem is the following,
There are two entity classes, let's call them Entity1 and Entity2 with One-to-Many relationship in between, i.e. one Entity1 contains multiple Entity2s, and Entity2 may have only one Entity1:
#Entity
#Table(name = "entity1")
public class Entity1 {
int x;
int y;
...
#LazyCollection(LazyCollectionOption.TRUE)
#OneToMany(mappedBy = "e1", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Entity2> entity2s = new HashSet<>();
}
#Entity
#Table(name = "entity2")
public class Entity2 {
int a;
int b;
...
#ManyToOne
#JoinColumn(name = "entity1_id")
#JsonBackReference
private Entity1 e1;
}
Now I would like to issue a query for retrieving Entity2s with WHERE conditions for both Entity2 and its corresponding Entity1:
#Query("SELECT " +
" e2 " +
"FROM " +
" Entity2 e2 " +
"WHERE " +
" e2.a = '<val1>' AND e2.b = '<val2>' AND e2.e1.x = '<val3>' AND e2.e1.y ='<val4>'")
List<Entity2> findMyEntity2s(
#Param...,
#Param...,
);
So the problem with this approach is that, it indeed gets desired Entity2s by cross joining entity1 and entity2 tables with specified WHERE conditions BUT it fetches e1s for each of those Entity2s in the result with a separate query.
So for example if the result of join is 5 Entity2s, there will be 5 additional queries to entity1 table.
I tried to set #ManyToOne in Entity2 as #ManyToOne(fetch = FetchType.LAZY) but it didn't help. I guess that's expected because LAZY would simply postpone the retrieval of e1s but wouldn't eliminate it completely.
Next, I read about #EntityGraph, and added it to Entity2:
#Entity
#Table(name = "entity2")
#NamedEntityGraph(name = "graph.entity2.entity1",
attributeNodes = { #NamedAttributeNode("e1") })
public class Entity2 {
int a;
int b;
...
#ManyToOne
#JoinColumn(name = "entity1_id")
#JsonBackReference
private Entity1 e1;
}
and in the repository, I added it as:
#EntityGraph(value = "graph.entity2.entity1")
#Query("SELECT " +
" e2 " +
"FROM " +
" Entity2 e2 " +
"WHERE " +
" e2.a = '<val1>' AND e2.b = '<val2>' AND e2.e1.x = '<val3>' AND e2.e1.y ='<val4>'")
List<Entity2> findMyEntity2s(
#Param...,
#Param...,
);
In this case, the separate SQL queries disappear, EntityGraph does left join and its result contains columns from both entity1 and entity2, BUT because the conditions for e2.e1 are still in WHERE clause, it adds ONE MORE unnecessary cross join with entity1 table (e2.e1 conditions are checked in that cross join).
I couldn't find a way to get rid of that extra cross join, so now I'm using the following query:
#EntityGraph(value = "graph.entity2.entity1")
#Query("SELECT " +
" e2 " +
"FROM " +
" Entity2 e2 " +
"WHERE " +
" e2.a = '<val1>' AND e2.b = '<val2>'")
List<Entity2> findMyEntity2s(
#Param...,
#Param...,
);
So basically I get Entity2s and in the application I filter out based on conditions of Entity1 (e2.e1.x = '<val3>' AND e2.e1.y ='<val4>').
Is there a way to make it work with a single join only, for both entity's conditions, not only Entity2 conditions? The way I'm doing it now, does not seem correct and efficient to me, and I feel there's a way to do that using repository method only, without involving the app. Would appreciate any help on this
UPD. Read about nativeQuery option (nativeQuery = true) for #Query annotation, which allows specifying a raw query and thus bypassing entity-based query, but the query still fetches many-to-one e1 field, using entity1_id (entity graph was disabled). I tried to enable entity graph but it dropped exception stating that entity graph cannot be used with native query, which is expected

This is the classic n + 1 query problem.
You can read the detail here: https://vladmihalcea.com/n-plus-1-query-problem/
In your query, append:
LEFT JOIN FETCH e2.e1 e2e1
This will fetch e1 with e2 in the first and single query.
Don't forget; always use FetchType.LAZY and fetch your entities with JOIN FETCH. Otherwise, you will get into a big mass while the scope of the project enlarges.
In addition, why do you use Jaxson annotations in your Entity classes? Use entities for only DAO access and map them to another DTOs to use elsewhere.

Related

search collection by index in JPA #query

I have 2 entities with OneToMany Relation as below,
public class Visit{
#OneToMany(mappedBy = "visit", fetch = FetchType.LAZY)
#OrderBy("updated_on DESC")
private List<StatusChange> statusHistory;
}
public class StatusChange{
#ManyToOne
#JoinColumn(name = "ms_visit_id")
private Visit visit;
#Column(name = "to_status")
#Enumerated(EnumType.STRING)
private VisitStatus toStatus;
}
Visits can have multiple status like created, canceled, deleted etc.. whenever the status change there will be a new row added to the StatusChange table with a entry associated to that visit (toStatus will become canceled) . Now I want a query to filter the visits for which the latest status is canceled and with the matching user id.
I am using the #query of JPA. I already have got the result with the following query.
#Query(value = "select vs1.visit from StatusChange vs1 where vs1.id in (" +
" select max(vs2.id) from StatusChange vs2 " +
" where vs2.visit.user.id = :userId" +
" group by vs2.visit.id)" +
" and vs1.toStatus in :status")
public List<MSVisit> findByUserAndStatus(#Param("userId") Long userId, #Param("status") List<Visit.VisitStatus> status);
But I feel the query can be improved or is there any way to query some thing like,
"from Visit visit" +
" where visit.statusHistory.get(0).toStatus in :status" +
" and visit.user.id = :userId
Thanks for your help.
Usually, these queries are best modeled with lateral joins, but Hibernate does not support that yet. Note that unless you meant something different, your query is not correct, as the max-aggregation is based on the value of the id rather than the updated_on value. Not sure if that is on purpose, but I would suggest the following query in case you really want the latest visit.
#Query(value = "from Visit vs1 where vs1.user.id = :userId and (select max(h.updatedOn) from vs1.statusHistory h) >= ALL (" +
" select max(h.updatedOn) from vs1.statusHistory h " +
" and vs1.toStatus in :status")
public List<MSVisit> findByUserAndStatus(#Param("userId") Long userId, #Param("status") List<Visit.VisitStatus> status);

The #SqlResultSetMapping usage cause the schema validation problem

I have to extract data for statistic purpose. I've created a native query and used #SqlResultSetMapping to map the resultset to an object.
Hibernate needs to declare this class (Elaboration) as #Entity BUT IS NOT A TABLE, and I don't want a table because I have only to extract data on the fly when needed.
The code works fine but the gitlab pipeline fails during validation with
schemaManagementException: Schema-validation: missing table [elaboration].
Here my code so far:
SqlResultSetMapping(name="ValueMapping",
classes={
#ConstructorResult(
targetClass=Elaboration.class,
columns={
#ColumnResult(name="areadesc", type=String.class),
#ColumnResult(name="subsectordesc", type=String.class),
#ColumnResult(name="eurovalue", type=BigDecimal.class),
#ColumnResult(name="eurotch", type=BigDecimal.class),
}
)
})
#Entity
public class Elaboration{
#Id
private Long id;
private String areadesc;
private String subsectordesc;
private Integer dossiercount;
private BigDecimal eurovalue;
private BigDecimal eurotch;
....
and the custom query:
String statisticValueQuery = "select a.mdescr as areadesc, s.mdescr as subsectordesc, sum(euro_value) as eurovalue,
sum(euro_value_tch) as eurotch " +
"from dossier d " +
"join dossier_document dd on d.id = dd.dossier_id " +
"join dossier_country dc on d.id = dc.dossier_id " +
"join country c on dc.country_id = c.id " +
"join area a on c.area_id = a.id " +
"join dossier_subsector ds on d.id = ds.dossier_id " +
"join subsector s on ds.subsector_id = s.id " +
"where dd.document_id = :document " +
"and d.submission_date >= :startdate and d.submission_date <= :enddate " +
"group by s.id, a.id;";
public List<Elaboration> getValueElaboration(ElaborationRequestDTO elaborationRequestDTO){
Query resultMapping = em.createNativeQuery(statisticValueQuery, "ValueMapping");
resultMapping.setParameter("startdate", elaborationRequestDTO.getElaborateFromEquals());
resultMapping.setParameter("enddate", elaborationRequestDTO.getElaborateToEquals());
resultMapping.setParameter("document", elaborationRequestDTO.getDocumentIdEquals());
return resultMapping.getResultList();
Is there a way to pass the validation test?
Thanks
This is wrong statement.
Hibernate needs to declare this class (Elaboration) as #Entity
You should just put your #SqlResultSetMapping declaration above some #Entity but it can be some other entity not related to the Elaboration.
#SqlResultSetMapping(name="ValueMapping",
classes={
#ConstructorResult(
targetClass=Elaboration.class,
columns={
#ColumnResult(name="areadesc", type=String.class),
#ColumnResult(name="subsectordesc", type=String.class),
#ColumnResult(name="eurovalue", type=BigDecimal.class),
#ColumnResult(name="eurotch", type=BigDecimal.class),
}
)
})
#Entity
public class SomeEntity {
}
And if Elaboration is not an entity you should not annotate it as such.

JPA Property Expression - Match all children in OneToManyRelationship

Requirement
I have a simple OneToMany Relationship between two entities. I want to use JPA property expression to find Parent Entity with a condition which matches all children entities.
Parent
#Entity
public class PcSigningStatus {
#OneToMany(cascade = CascadeType.ALL, mappedBy = "signingStatus")
private List<PcSigningProcessEvent> signingProcessEvents = new ArrayList<>();
Child
#Entity
public class PcSigningProcessEvent {
#Enumerated(EnumType.STRING)
private ProcessEventType phase;
#ManyToOne
#JoinColumn(name = "SIGNING_STATUS_ID")
private PcSigningStatus signingStatus;
}
Parent Repository
public interface SigningStatusRepo extends CrudRepository<PcSigningStatus, Long> {
PcSigningStatus[] findBySigningProcessEvents_PhaseNot(ProcessEventType phase);
}
Issue
Suppose I only have 1 parent entity in database with 2 child entities containing different value of phase. The above query returns the same parent multiple times depending on the number of children it does not match the value of phase in. I need help in finding a way that Parent entity is returned only if the phase is not present in all the children entities
Maybe you should use #Query for this?
Hard to understand your question :) But you can try add distinct flag using Distinct mentioned in https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation
Edited: or you can use Query something like
SELECT * FROM PcSigningProcessEvent WHERE PcSigningProcessEvent.id NOT IN (SELECT SIGNING_STATUS_ID FROM PcSigningProcessEvent WHERE phase = ?)
So I found no way of doing this with property expression, and had to use Native Query instead.
Soltution
#Query(value = "SELECT ss.* FROM PARENT_TABLE ss\n" +
"left join CHILD_TABLE spe\n" +
"ON ss.ID= spe.SIGNING_STATUS_ID\n" +
"and spe.phase IN ('Value1', 'Value2')\n" +
"where spe.ID is null", nativeQuery = true)
PARENT_ENTITY[] customQuery();
This will return all parent entities whose children's property phase don't have any one of the value 'Value1' or 'Value2' or both.

Map new column from Spring Native query to entity

I have a case statement in my Native query where I am attempting to override a field in my entity.
SELECT i.id, i.ONE_TO_ONE_ID, i.ANOTHER, CASE(WHEN condition THEN 'YES' WHEN another_condition THEN 'NO' ELSE 'MAYBE' END) as word ....
I am using this with JpaRepository as a native query, with pagination.
When I run the native query against my db directly, the result set looks as though I expect.
| id_value | MAPPED_ENTITY_ID_value | another value | word_value (YES) |
When I run the native query from my JpaRepository, everything works there, except word is always null. I cant' seem to figure out how to map the additional String word result to a field in my Entity.
Is there a way to get this to map? Or will I have to create an entire #SqlResultSetMapping() for all of my fields coupled with a native query? (hoping not)
UPDATE: 1
I was generalizing above. Here is my Query.
#Query(
name = "listPagedMapping",
value = "SELECT DISTINCT i.ID, i.INSTANCE_ID, i.REGION, i.CNAME_STACK_ID, i.INSTANCE_STATE, i.IP_ADDRESS, i.EC2_ROLE_NAME, i.INSTANCE_OWNER, i.IS_MASTER, i.EC2_MASTER_ID, i.CNAME, i.EC2_START_TIMESTAMP, i.PRIVATE_DNS, i.INSTANCE_NAME, i.AUTO_TERMINATE, i.AUTO_TERMINATE_DATE, i.TERMINATION_ZONE, i.ADMIN_GROUP_AD_LDAP_ID, i.USER_GROUP_AD_LDAP_ID, (CASE WHEN i.INSTANCE_OWNER=:username THEN 'OWNER' WHEN i.ADMIN_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) THEN 'ADMIN' WHEN i.USER_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) THEN 'USER' END) as PERMISSION FROM USER u, USER_ACCESS_GROUPS g, EC2_PROVISIONING i WHERE i.INSTANCE_OWNER=:username and i.INSTANCE_STATE in (:instanceStates) or u.username=:username and i.INSTANCE_STATE in (:instanceStates) and g.USER_ID=u.USER_ID and (i.ADMIN_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) or i.USER_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID))",
countQuery = "SELECT count(*) FROM (SELECT DISTINCT i.* FROM USER u, USER_ACCESS_GROUPS g, EC2_PROVISIONING i WHERE i.INSTANCE_OWNER=:username and i.INSTANCE_STATE in (:instanceStates) or u.username=:username and i.INSTANCE_STATE in (:instanceStates) and g.USER_ID=u.USER_ID and (i.ADMIN_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) or i.USER_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID))) as ug",
nativeQuery = true)
Page<Ec2Instance> findAllByPermissionUserAdminOrOwnerAndInstanceStateIn(
#Param("username")final String username,
#Param("instanceStates") final Set<String> instanceStates,
final Pageable pageable);
}
Obviously a bit more complex.
I can get it to map to the entity field with using a named query, but then I loose all the default mappings:
#JsonInclude(JsonInclude.Include.NON_NULL)
#SuppressWarnings("unused")
#Data
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode(exclude={"masterNode", "workers", "associatedBuckets"})
#Entity
#Table(name = "EC2_PROVISIONING")
#SqlResultSetMapping(
name="listPagedMapping",
columns = {
#ColumnResult(name = "permission", type = String.class)
}
)
#NamedNativeQuery(
name = "listAccessibleInstances",
query = ACCESSIBLE_QUERY,
resultSetMapping = "listPagedMapping"
)
public class Ec2Instance {
....
private String permission;
#column(name = "INSTANCE_ID")
private String instanceId;
#ManyToOne
#JoinColumn(name = "EC2_MASTER_ID")
private Ec2Instance masterNode;
#Setter(AccessLevel.NONE)
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "WORKER_EC2_NODES", joinColumns = { #JoinColumn(name = "EC2_MASTER_ID") }, inverseJoinColumns = {
#JoinColumn(name = "ID") })
private Set<Ec2Instance> workers = new HashSet<>();
... More fields ..
}
I guess, I am hoping there is a way to provide a single mapping on-top of the default mapping that is done by ORM. The above code results in only a pageable of Content PERMISSION, rather than the whole entity + permission.
UPDATE: 2
Ok, so I am getting closer... Seems by removing the #ColumnResult I do get the default mapping, plus the PERMISSION field mapped over! Looks like this:
#SqlResultSetMapping(
name="listPagedMapping"
)
The last issue is it does not accept my CountQuery, and causes my tests to fail whenever a Pagination Query results with multiple pages. Looks like Spring try's to come up with its own CountQuery, which is not correct.
UPDATE: 3
To finish this off, looks like I can provide the Count Query as described here: Spring Data - Why it's not possible to have paging with native query
I will give this a go and update back.
I never got this to work quite how I wanted. I am sure I could by mapping my entire entity, but, that would have been painstaking. I ended up solving this by using NamedNativeQueries, with mapping for the additional Column as a result of my Case statement. My entity class is now annotated like:
#JsonInclude(JsonInclude.Include.NON_NULL)
#SuppressWarnings("unused")
#Data
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode(callSuper = false)
#Entity
#Table(name = "EC2_PROVISIONING")
#SqlResultSetMappings({
#SqlResultSetMapping(
name = "listPagedMapping",
entities = {
#EntityResult(
entityClass = Ec2Instance.class
)
},
columns = {#ColumnResult(name = "permission", type = String.class)}
),
#SqlResultSetMapping(name = "listPagedMapping.count", columns = #ColumnResult(name = "cnt"))
})
#NamedNativeQueries({
#NamedNativeQuery(
name = "Ec2Instance.listAccessibleInstances",
query = ACCESSIBLE_QUERY,
resultClass = Ec2Instance.class,
resultSetMapping = "listPagedMapping"
),
#NamedNativeQuery(
name = "Ec2Instance.listAccessibleInstances.count",
resultSetMapping = "listPagedMapping.count",
query = ACCESSIBLE_QUERY_COUNT
)
})
We also dont need the permission field in this entity anymore. I removed that.
Then in my Repository:
Page<Object[]> listAccessibleInstances(
#Param("username")final String username,
#Param("instanceStates") final Set<String> instanceStates,
final Pageable pageable);
Thats it! Now the result of my case statement is returned with each entity.
Object[0] = original, default mapped entity.
Object[1] = permission

org.hibernate.hql.internal.ast.QuerySyntaxException: writes is not mapped

I set up a Database Application with Spring an Hibernate and I'm using a Many-To-Many-Relationship.
Here's the code:
Author.java
#ManyToMany(cascade = {CascadeType.ALL})
#JoinTable(name = "writes", joinColumns = {#JoinColumn(name = "authorId")}, inverseJoinColumns = {#JoinColumn(name = "publicationId")})
private Set<Publication> publications = new HashSet<Publication>();
Publication.java
#ManyToMany(mappedBy = "publications")
private Set<Author> authors = new HashSet<Author>();
these lines of code generate a connection-table named writes, but when I try to run a query over all Tables int gives me the error, named above.
this is the method, that schould run the query:
#Transactional
public List<Author> findAuthorByLastname(String lastName) {
String hql = "from Author a, Publication p, writes w where a.id = w.authorId and p.id = w.publicationId and a.lastname = :lastName";
Query q = sessionFactory.getCurrentSession().createQuery(hql);
q.setParameter("lastName", lastName);
List<Author> result = q.list();
return result;
}
You don't need to mention connection table explicitly in HQL queries (moreover, you even cannot mention them - only mapped entities). You need to use join on mapped relationships instead.
Also, it's not quite clear what you want to achieve.
If you want to fetch Authors with eagerly filled collections of their Publications in one query, use join fetch:
select distinct(a) from Author a join fetch a.publications where a.lastName = :lastName
If you want to fetch a list of pairs (Author, Publication), use regular join:
select a, p from Author a join a.publications p where a.lastName = :lastName

Resources