Named entity graph JOINS results (need distinct option) in Spring Data JPA - java

I'm using #NamedEntityGraph annotation to load a graph from database.
#NamedEntityGraph(
name = "Firma.uredjivanje",
attributeNodes = {
#NamedAttributeNode(value="prevodi", subgraph = "prevodi")
},
subgraphs = {
#NamedSubgraph(
name = "prevodi",
attributeNodes = {
#NamedAttributeNode(value = "jezik", subgraph = "jezik")
}
)
}
)
In the Spring Data JPA repository, I'm using annotation:
#EntityGraph(value="Firma.uredjivanje", type = EntityGraph.EntityGraphType.LOAD)
List<Firma> getByAktivna(boolean aktivna);
Everything works as expected, expect that all relations are joined, and I get duplicate Firma entities (because of JOIN). Instead of a List with entity id's {1,2,3}, I get {1,1,1,2,2,3}.
What is the best way to get distinct entities (if this is not a bug ofcourse).

Found the answer... Since NamedEntityGraph does JOIN in database, it selects all entities without DISTINCT. So the solution is to use Distinct in method name...
#EntityGraph(value="Firma.uredjivanje", type = EntityGraph.EntityGraphType.LOAD)
List<Firma> getDistinctByAktivna(boolean aktivna);

After adding dictinct I have error logs during server starup (maybe because there is no such thing as findDistinctAll() .
For me help a hint in https://jira.spring.io/browse/DATAJPA-680.
I added #Query annotation to the method, so my method looks like below
#EntityGraph(value = "User.detail", type = EntityGraph.EntityGraphType.LOAD)
#Query(value = "SELECT DISTINCT u FROM User u")
List<User> findAll();

Related

Why is this FetchType.LAZY being ignored and all entities are being fetched on the query?

I am using Spring Boot 2 with Hibernate and I have an entity named ItemCarga with this:
#ManyToMany(cascade = { CascadeType.DETACH }, fetch = FetchType.LAZY)
#JoinTable(name = "rel_transp_carga", joinColumns = { #JoinColumn(name = "fk_item_carga") }, inverseJoinColumns = { #JoinColumn(name = "fk_transportadora") })
#LazyCollection(LazyCollectionOption.TRUE)
private Set<Transportadora> transportadoras = new HashSet<Transportadora>();
Whey do a query using that entity on my repository, like this:
#Query("select e from ItemCarga e where e.cnpjCeramica = :cnpjCeramica and (e.dataInserido between :inicio and :fim) ")
List<ItemCarga> listarProdutosPorPeriodo(
#Param("cnpjCeramica") String cnpjCeramica,
#Param("inicio") Date dataInicial,
#Param("fim") Date dataFinal,
Sort sort);
The result is a set of ItemCarga entity with the attribute transportadoras fetched with all its items.
It shouldn't it be null or empty?
Shouldn't be ignored since I didn't mention that attribute on my select?
You did mention the attribute in the query: select e from ItemCarga e. This means you're fetching the whole ItemCarga entity. Since you defined transportadoras as fetch = FetchType.LAZY, a proxy is created (the data is not fetched from the database).
If you're invoking the query withing transaction, you can iterate over the set, then hibernate will fetch the child entities (this often leads to n+1 select problem). If you try to access it outside of transaction, LazyInitializationException will be thrown.
Since it's just a hint for hibernate, you can make sure, the Set won't be fetched in a couple of ways:
by not querying for it, for example:
#Query("select e.field1, e.field2 from ItemCarga e ...")
List<Object[]> listarProdutosPorPeriodo...
The downside is that you have to cast the results,
by using dto and query mapping. I won't describe it in detail, more you can find here,
by using projections - interfaces with getters and setters for the fields you want to fetch. More details here.

HQL. Intersection of two lists

Update: look my answer below on how to check if 2 list intersects (both for #ElementCollection with string/enums and usual entities list mapped like #OneToMany)
I have an entity which contains #ElementCollectionfield with enums.
public enum StatusType {
NEW, PENDING, CLOSED;
}
#Entity
public class MyEntity {
#ElementCollection
#CollectionTable(name = "status_type", joinColumns = {#JoinColumn(name = "my_entity_id")})
#Column(name = "status_type", nullable = false)
private Set<StatusType > statusTypes = new HashSet<StatusType >();
...
}
Now I want to get all entities which contains status NEW or PENDING (or both).
I'm trying to use this query:
SELECT DISTINCT u FROM MyEntity u WHERE u.statusTypes in :statusTypes
But I'm getting exception: org.postgresql.util.PSQLException: No value specified for parameter 9.
How to properly query on collections and filter by intersections?
Problem solved by adding JOIN clause to HQL. Hibernate couldn't implicitly recognize that query needs JOIN clause. May be it will help someone:
SELECT DISTINCT u FROM MyEntity u
LEFT JOIN u.statusTypes statusTypes
WHERE statusTypes in :statusTypes
I set the query params like this:
query.setParameter( "statusTypes", listOfStatusTypesEnums);
It will select rows where at least one element of listOfStatusTypesEnums list is present in entity's statusTypes property (i.e. if 2 list are intersects in some way).
If you have usual list of entities (which are not #ElementCollection, but #OneToMany etc), same rule will work as well. Just use like this: LEFT JOIN u.subEntities subEntities WHERE subEntities.id in :subEntityIds

Using JPA CriteriaBuilder to generate query where attribute is either in a list or is empty

I am trying to use the JPA CriteriaBuilder to generate a query for an entity called "TestContact" that has a many-to-many join with another entity called "SystemGroup" where the attribute for this join called "groups". The objective of the query is to retrieve records from the "TestContact" entity where the "groups" attribute is either in a list or is empty.
The code I'm using is as follows
public List<TestContact> findWithCriteriaQuery(List<SystemGroup> groups) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<TestContact> cq = cb.createQuery(TestContact.class);
Root<TestContact> testContact = cq.from(TestContact.class);
cq.select(testContact);
Path<List<SystemGroup>> groupPath = testContact.get("groups");
// cq.where(groupPath.in(groups));
// cq.where(cb.isEmpty(groupPath));
cq.where(cb.or(cb.isEmpty(groupPath), groupPath.in(groups)));
TypedQuery<TestContact> tq = em.createQuery(cq);
return tq.getResultList();
}
The problem is this query only returns results where group is in the list "groups" but for some reason isn't also returning the results where group is empty (i.e. there is no entry in the join table)
If I change the where clause to cq.where(cb.isEmpty(groupPath)); then the query correctly returns the results where group is empty.
If I change the where clause to cq.where(groupPath.in(groups)); then the query correctly returns the results where the group is in the list "groups".
What I don't understand is why when I try to combine these two predicates using the CriteriaBuilder or method the results don't include the records where the group is either in the list or is empty.
The groups attribute in the "TestContact" entity is declared as follows
#ManyToMany(fetch=FetchType.EAGER)
#JoinTable(name = "TEST_CONTACT_GROUPS", joinColumns = { #JoinColumn(name = "CONTACT_ID", referencedColumnName = "CONTACT_ID") }, inverseJoinColumns = { #JoinColumn(name = "GROUP_ID", referencedColumnName = "GROUP_ID") })
private List<SystemGroup> groups;
The JPA provider is EclipseLink 2.5.0, the Java EE application server is GlassFish 4 and the database is Oracle 11gR2.
Can anyone please point out where I'm going wrong?
Update
I've tried the suggestion from #Chris but Eclipse is returning the following error on Join<List<SystemGroup>> groupPath = testContact.join("groups", JoinType.LEFT)
Incorrect number of arguments for type Join; it cannot be
parameterized with arguments >
Looking at the JavaDoc for Join it says the type parameters are...
Z - the source type of the join, X - the target type of the join
I've tried Join<TestContact, SystemGroup> groupPath = testContact.join("groups", JoinType.LEFT); which then causes Eclipse to return the following error on cb.isEmpty
Bound mismatch: The generic method isEmpty(Expression) of type
CriteriaBuilder is not applicable for the arguments
(Join). The inferred type SystemGroup is not
a valid substitute for the bounded parameter >
The testContact.get("groups"); clause forces an inner join from testContact to groups, which filters out testContacts with no groups. You need to specify a left outer join, and use that in your isEmpty and in clauses.
Root<TestContact> testContact = cq.from(TestContact.class);
cq.select(testContact);
Join<TestContact, SystemGroup> groupPath = testContact.join("groups", JoinType.LEFT);
cq.where(cb.or(cb.isEmpty(testContact.get("groups")), groupPath.in(groups)));
I usually refer to https://en.wikibooks.org/wiki/Java_Persistence/Criteria#Join for examples

Order by count using Spring Data JpaRepository

I am using Spring Data JpaRepository and I find it extremely easy to use. I actually need all those features - paging, sorting, filtering. Unfortunately there is one little nasty thing that seems to force me to fall back to use of plain JPA.
I need to order by a size of associated collection. For instance I have:
#Entity
public class A{
#Id
private long id;
#OneToMany
private List<B> bes;
//boilerplate
}
and I have to sort by bes.size()
Is there a way to somehow customize the ordering still taking the advantage of pagination, filtering and other Spring Data great features?
I've solved the puzzle using hints and inspirations from:
Limiting resultset using #Query anotations by Koitoer
How to order by count() in JPA by MicSim
Exhaustive experiments on my own
The first and most important thing I've not been aware of about spring-data is that even using #Query custom methods one can still create paging queries by simply passing the Pageable object as parameter. This is something that could have been explicitely stated by spring-data documentation as it is definitely not obvious though very powerful feature.
Great, now the second problem - how do I actually sort the results by size of associated collection in JPA? I've managed to come to a following JPQL:
select new package.AwithBCount(count(b.id) as bCount,c) from A a join a.bes b group by a
where AwithBCount is a class that the query results are actually mapped to:
public class AwithBCount{
private Long bCount;
private A a;
public AwithBCount(Long bCount, A a){
this.bCount = bCount;
this.a = a;
}
//getters
}
Excited that I can now simply define my repository like the one below
public interface ARepository extends JpaRepository<A, Long> {
#Query(
value = "select new package.AwithBCount(count(b.id) as bCount,c) from A a join a.bes b group by a",
countQuery = "select count(a) from A a"
)
Page<AwithBCount> findAllWithBCount(Pageable pageable);
}
I hurried to try my solution out. Perfect - the page is returned but when I tried to sort by bCount I got disappointed. It turned out that since this is a ARepository (not AwithBCount repository) spring-data will try to look for a bCount property in A instead of AwithBCount. So finally I ended up with three custom methods:
public interface ARepository extends JpaRepository<A, Long> {
#Query(
value = "select new package.AwithBCount(count(b.id) as bCount,c) from A a join a.bes b group by a",
countQuery = "select count(a) from A a"
)
Page<AwithBCount> findAllWithBCount(Pageable pageable);
#Query(
value = "select new package.AwithBCount(count(b.id) as bCount,c) from A a join a.bes b group by a order by bCount asc",
countQuery = "select count(a) from A a"
)
Page<AwithBCount> findAllWithBCountOrderByCountAsc(Pageable pageable);
#Query(
value = "select new package.AwithBCount(count(b.id) as bCount,c) from A a join a.bes b group by a order by bCount desc",
countQuery = "select count(a) from A a"
)
Page<AwithBCount> findAllWithBCountOrderByCountDesc(Pageable pageable);
}
...and some additional conditional logic on service level (which could be probably encapsulated with an abstract repository implementation). So, although not extremely elegant, that made the trick - this way (having more complex entities) I can sort by other properties, do the filtering and pagination.
One option, which is much simpler than the original solution and which also has additional benefits, is to create a database view of aggregate data and link your Entity to this by means of a #SecondaryTable or #OneToOne.
For example:
create view a_summary_view as
select
a_id as id,
count(*) as b_count,
sum(value) as b_total,
max(some_date) as last_b_date
from b
Using #SecondaryTable
#Entity
#Table
#SecondaryTable(name = "a_summary_view",
pkJoinColumns = {#PrimaryKeyJoinColumn(name = "id", referencedColumnName= "id")})
public class A{
#Column(table = "a_summary_view")
private Integer bCount;
#Column(table = "a_summary_view")
private BigDecimal bTotal;
#Column(table = "a_summary_view")
private Date lastBDate;
}
You can now then sort, filer, query etc purely with reference to entity A.
As an additional advantage you have within your domain model data that may be expensive to compute in-memory e.g. the total value of all orders for a customer without having to load all orders or revert to a separate query.
Thank you #Alan Hay, this solution worked fine for me. I just had to set the foreignKey attribute of the #SecondaryTable annotation and everything worked fine (otherwise Spring Boot tried to add a foreignkey constraint to the id, which raise an error for a sql View).
Result:
#SecondaryTable(name = "product_view",
pkJoinColumns = {#PrimaryKeyJoinColumn(name = "id", referencedColumnName = "id")},
foreignKey = #javax.persistence.ForeignKey(ConstraintMode.NO_CONSTRAINT))
I don't know much about Spring Data but for JPQL, to sort the objects by size of associated collection, you can use the query
Select a from A a order by a.bes.size desc
You can use the name of an attribute found in the select clause as a sort property:
#Query(value = "select a, count(b) as besCount from A a join a.bes b group by a", countQuery = "select count(a) from A a")
Page<Tuple> findAllWithBesCount(Pageable pageable);
You can now sort on property besCount :
findAllWithBesCount(PageRequest.of(1, 10, Sort.Direction.ASC, "besCount"));
I used nativeQuery to arrange sorting by number of records from another table, pagable works.
#Query(value = "SELECT * FROM posts where posts.is_active = 1 and posts.moderation_status = 'ACCEPTED' " +
"group by posts.id order by (SELECT count(post_id) FROM post_comments where post_id = posts.id) desc",
countQuery = "SELECT count(*) FROM posts",
nativeQuery = true)
Page <Post> findPostsWithPagination(Pageable pageable);
For SpringBoot v2.6.6, accepted answer isn't working if you need to use pageable with child's side field especially when using #ManyToOne.
For the accepted answer:
You can return new object with static query method, which have to include order by count(b.id)
And also order by bCount isn't working.
Please use #AlanHay solution, it is working, but you can't use primitive field and change foreign key constraint. For instance, change long with Long. Because:
When saving a new entity Hibernate does think a record has to be written to the secondary table with a value of zero. (if you use primitive type)
Otherwise you will get an exception:
Caused by: org.postgresql.util.PSQLException: ERROR: cannot insert into view "....view"
Here is the example:
#Entity
#Table(name = "...")
#SecondaryTable(name = "a_summary_view,
pkJoinColumns = {#PrimaryKeyJoinColumn(name = "id",
referencedColumnName= "id")},
foreignKey = #javax.persistence.ForeignKey(name = "none"))
public class UserEntity {
#Id
private String id;
#NotEmpty
private String password;
#Column(table = "a_summary_view",
name = "b_count")
private Integer bCount;
}

Hibernate entity with restriction

We have a DB table that is mapped into a hibernate entity. So far everything goes well...
However what we want is to only map enentitys that satisty a specific criteria, like ' distinct(fieldA,fieldB) '...
Is it possible to map with hibernate and hibernate annotations? How can we do it? With #Filter?
I would recommend that you use #Where annotation. This annotation can be used on the element Entity or target entity of a collection. You provide a clause attribute written in sql that will be applied to any select that hibernate performs on that entity. It is very easy to use and very readable.
Here is an example.
#Entity
//I am only interested in Donuts that have NOT been eaten
#Where(clause = "EATEN_YN = 'N'")
public class Donut {
#Column(name = "FILLING")
private String filling;
#Column(name = "GLAZED")
private boolean glazed = true;
#Column(name = "EATEN_YN")
private boolean eaten = false;
...
}
You could create a view and then map that view to entity:
create view my_data as
select ... from ...
#Entity(table="my_data")
public class MyData { ... }
One option is to map the table normally, then you could fetch your always entities through a query or a filter.
You could also make a native SQL query and map the entity on the results:
Query q = sess.createSQLQuery("SELECT DISTINCT fieldA, fieldB FROM some_table")
.addEntity(MyEntity.class);
List<MyEntity> cats = q.list();
It might be also possible to add DISTINCT to this type of HQL query:
select new Family(mother, mate, offspr)
from DomesticCat as mother
join mother.mate as mate
left join mother.kittens as offspr
Methods 1, 3 and 4 will make a read-only mapping.
Could you be more specific about the criteria you are using? The view approach is more generic since you can't do everything with a hibernate query or filter.
perhaps you could create a new Pojo that encapsulates the fields and the condition that they should statisy . And then then make that class a 'custom user defined type', such that Hibernate will have to use the mapping class that you provide, for mapping that 'type'..
In addition to the options mentioned by Juha, you can also create an object directly out of a SQL query using the NamedNativeQuery and SqlResultSetMapping annotations.
#Entity
#SqlResultSetMapping(name = "compositekey", entities =
#EntityResult(entityClass = MiniBar.class,
fields = { #FieldResult(name = "miniBar", column = "BAR_ID"), })
)
#NamedNativeQuery(name = "compositekey",
query = "select BAR_ID from BAR", resultSetMapping = "compositekey")
#Table(name = "BAR")
public class Bar {
Flavor the SQL query to your taste

Categories