Hibernate query error with collection defined as Set - java

This error is thrown from one of my jersey-glassfish rest endpoint ( from domain logs ):
Caused by: java.lang.IllegalArgumentException: Type specified for TypedQuery [com.tanukis.streetama.entity.Flow] is incompatible with query return type [interface java.util.Set]
at org.hibernate.ejb.AbstractEntityManagerImpl.createNamedQuery(AbstractEntityManagerImpl.java:458)
at com.sun.enterprise.container.common.impl.EntityManagerWrapper.createNamedQuery(EntityManagerWrapper.java:566)
at com.tanukis.streetama.dao.FluxManager.getBlacklist(FluxManager.java:571)
The query is defined in orm.xml :
SELECT DISTINCT s.blacklistedFlow FROM StreetamaUser s WHERE s.uid = :uid
Here is my StreetamaUser entity:
#ManyToMany(cascade= javax.persistence.CascadeType.ALL)
#JoinTable(
name="ws_user_blacklist",
uniqueConstraints = #UniqueConstraint(columnNames = { "blacklisted_flow_id", "user_id" }),
joinColumns = {
#JoinColumn(name="user_id",referencedColumnName="uid")
},
inverseJoinColumns = {
#JoinColumn(name="blacklisted_flow_id",referencedColumnName="id")
}
)
#XmlTransient
private Set<Flow> blacklistedFlow = new HashSet<Flow>();
And the query call:
List<Flow> result = em.createNamedQuery( "StreetamaUser.findBlacklist", Flow.class )
.setParameter("iduser", uid )
.setFirstResult(startitem)
.setMaxResults(itemnbr)
.getResultList();
I can't understand the Hibernate exception. getResultList return a list, so why it complains about the query return type ?

You can try to use a java.Util.List instead of a Set.

You don't. It is always a List even if it doesn't contains duplicate values like set. I don't see why it matters in the first place.

Related

JPA select association and use NamedEntityGraph

Our in-house framework built with Java 11, Spring Boot, Hibernate 5 and QueryDSL does a lot of auto-generation of queries. I try to keep everything efficient and load associations only when needed.
When loading full entities, the programmer can declare a NamedEntityGraph to be used. Now there is one case where a query like this is generated:
select user.groups
from User user
where user.id = ?1
Where the Entities in question look like this:
#Entity
#NamedEntityGraph(name = User.ENTITY_GRAPH,
attributeNodes = {
#NamedAttributeNode(User.Fields.permissions),
#NamedAttributeNode(value = User.Fields.groups, subgraph = "user-groups-subgraph")
},
subgraphs = #NamedSubgraph(
name = "user-groups-subgraph",
attributeNodes = {
#NamedAttributeNode(Group.Fields.permissions)
}
))
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Enumerated(EnumType.STRING)
#ElementCollection(targetClass = Permission.class)
#CollectionTable(name = "USERS_PERMISSIONS", joinColumns = #JoinColumn(name = "uid"))
private Set<Permission> permissions = EnumSet.of(Permission.ROLE_USER);
#ManyToMany(fetch = LAZY)
private Set<Group> groups = new HashSet<>();
}
#Entity
#NamedEntityGraph(name = Group.ENTITY_GRAPH,
attributeNodes = {
#NamedAttributeNode(value = Group.Fields.permissions)
})
public class Group {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
#Enumerated(EnumType.STRING)
#ElementCollection(targetClass = Permission.class)
#CollectionTable(
name = "GROUPS_PERMISSIONS",
joinColumns = #JoinColumn(name = "gid")
)
#NonNull
private Set<Permission> permissions = EnumSet.noneOf(Permission.class);
}
When selecting either User or Group directly, the generated query simply applies the provided NamedEntityGraphs. But for the above query the exception is:
org.hibernate.QueryException:
query specified join fetching, but the owner of the fetched association was not present in the select list
[FromElement{explicit,collection join,fetch join,fetch non-lazy properties,classAlias=user,role=foo.bar.User.permissions,tableName={none},tableAlias=permission3_,origin=null,columns={,className=null}}]
I first tried the User graph, but since we are fetching Groups, I tried the Group graph. Same Exception.
Problem is, there is no easy way to add a FETCH JOIN to the generated query, since I don't know which properties of the association should be joined in anyway. I would have to load the Entitygraph, walk it and any subgraph and generated the right join clauses.
Some more details on Query generation:
// QueryDsl 4.3.x Expressions, where propType=Group.class, entityPath=User, assocProperty=groups
final Path<?> expression = Expressions.path(propType, entityPath, assocProperty);
// user.id = ?1
final BooleanExpression predicate = Expressions.predicate(Ops.EQ, idPath, Expressions.constant(rootId));
// QuerydslJpaPredicateExecutor#createQuery from Spring Data JPA
final JPQLQuery<P> query = createQuery(predicate).select(expression).from(path);
// Add Fetch Graph
((AbstractJPAQuery<?, ?>) query).setHint(GraphSemantic.FETCH.getJpaHintName(), entityManager.getEntityGraph(fetchGraph));
EDIT:
I can reproduce this with a simple JPQL Query. It's very strange, if I try to make a typed query, it will select a List of Sets of Group and untyped just a List of Group.
Maybe there is something conceptually wrong - I'm selecting a Collection and I'm trying to apply a fetch join on it. But JPQL doesn't allow a SELECT from a subquery, so I'm not sure what to change..
// em is EntityManager
List gs = em
.createQuery("SELECT u.groups FROM User u WHERE u.id = ?1")
.setParameter(1, user.getId())
.setHint(GraphSemantic.FETCH.getJpaHintName(), em.getEntityGraph(Group.ENTITY_GRAPH))
.getResultList();
Same Exception:
org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list
So the problem can be distilled down to a resolution problem of the Entit Graphs attributes:
select user.groups
from User user
where user.id = ?1
With the Entity Graph
EntityGraph<Group> eg = em.createEntityGraph(Group.class);
eg.addAttributeNodes(Group.Fields.permissions);
Gives an Exception that shows that Hibernate tries to fetch User.permissions instead of Group.permissions. This is the bug report.
And there is another bug regarding the use of #ElementCollection here.

How to combine distinct and sort in spring data jpa specification query with joins

Example setup:
Entity
#Entity
class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
#ManyToMany(cascade = [CascadeType.PERSIST, CascadeType.MERGE])
#JoinTable(name = "book_authors",
joinColumns = [JoinColumn(name = "book_id")],
inverseJoinColumns = [JoinColumn(name = "author_id")])
var authors: MutableSet<Author> = HashSet()
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "publisher_id")
lateinit var publisher: Publisher
}
Both Author and Publisher are simple entities with just an id and a name.
The spring data jpa BookSpecification: (notice the distinct on query)
fun hasAuthors(authorNames: Array<String>? = null): Specification<Book> {
return Specification { root, query, builder ->
query.distinct(true)
val matchingAuthors = authorRepository.findAllByNameIn(authorNames)
if (matchingAuthors.isNotEmpty()) {
val joinSet = root.joinSet<Book, Author>("authors", JoinType.LEFT)
builder.or(joinSet.`in`(matchingContentVersions))
} else {
builder.disjunction()
}
}
}
Executing the query (pageable containing a sort on publisher.name)
bookRepository.findAll(
Specification.where(bookSpecification.hasAuthors(searchRequest)),
pageable!!)
The REST request:
MockMvcRequestBuilders.get("/books?authors=Jane,John&sort=publisherName,desc")
This results in the following error:
Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Order by expression "PUBLISHERO3_.NAME" must be in the result list in this case;
The problem is in the combination of the distinct and sort. The distinct requires the publisher name to be in the select fields to be able to sort.
How can I fix this with Specification query?
You'll likely have to explicitly select the PUBLISHERO3_.NAME column like so:
query.select(builder.array(root.get("PUBLISHERO3_.NAME"), root.get("yourColumnHere")));
Joined columns are probably not included by default because they're out of scope with regards to the root generic type.
you can't do this. basically, if you have distinct and you want to sort, you can only use the selected columns.
what you can do is to use row_number() window function instead of distinct, and then select everything with row_number=1.
you can find an (a little bit old) example here: https://stackoverflow.com/a/30827497/10668681

How to limit a ManyToMany with an extra where clause in JPA

I'm retrieving data from a legacy database, thus have no control at all over the schema.
I frequently need to check for a static value in a separate column to remove false matches.
create table mySource (
id int,
...
)
create table aRelation (
srcId int,
myFK int,
relationLimit varchar
)
create table aTarget (
id int,
...
myFK int,
aLimit varchar,
...
)
"relationLimit" and/or "aLimit" above must match a static value.
I can only find the standard annotations:
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(
name = "aRelation",
joinColumns = {
#JoinColumn(name = "srcId") },
#inverseJoinColumns = {
#JoinColumn(name = "myFK", referencedColumnName = "myFK")
})
private List<ATarget> targets;
But I can't find any way to annotate the requirement(s)
aLimit = "Something"
relationLimit="SomethingElse"
If this is obvious in the documentation please tell me how and where to read it.
If you use Hibernate you can try #Filter annotation on entity.
#Filter(name="betweenLength", condition="aLimit = 'Something' and relationLimit='SomethingElse'")
More on this topic you can find here

Hibernate left join constraint in a many-to-many relationship

I have built a list of taggable documents, with a many-to-many relationship between the tags and the documents. I would now like to use the hibernate criteria mechanism to query a "summary" of each tag, which includes a count of how often a particular tag has been used, with an additional restriction on whether or not the document has been published.
The entities I'm using roughly look like this (You'll note an SQL join table in the middle there):
#Entity
public class DocumentTag {
... various things ...
#ManyToMany(fetch = FetchType.LAZY, mappedBy = "tags")
private List<Document> documents = new ArrayList<>();
}
#Entity
public class Document {
... various things ...
#Basic
#Column(name = "published", columnDefinition = "BIT", length = 1)
protected boolean published = false;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "document_tag_joins",
uniqueConstraints = #UniqueConstraint(
columnNames = {"document", "tag"}
),
joinColumns = {#JoinColumn(name = "document")},
inverseJoinColumns = {#JoinColumn(name = "tag")})
private List<DocumentTag> tags = new ArrayList<>();
}
Given the above, I've managed to figure out that building the query should work more or less as follows:
Criteria c = session.createCriteria(DocumentTag.class);
c.createAlias("documents", "docs",
JoinType.LEFT_OUTER_JOIN,
Restrictions.eq("published", true)
);
c.setProjection(
Projections.projectionList()
.add(Projections.alias(Projections.groupProperty("id"), "id"))
.add(Projections.alias(Projections.property("createdDate"), "createdDate"))
.add(Projections.alias(Projections.property("modifiedDate"), "modifiedDate"))
.add(Projections.alias(Projections.property("name"), "name"))
.add(Projections.countDistinct("docs.id"), "documentCount"));
// Custom response entity mapping
c.setResultTransformer(
Transformers.aliasToBean(DocumentTagSummary.class)
);
List<DocumentTagSummary> results = c.list();
Given the above, the hibernate generated SQL query looks as follows:
SELECT
this_.id AS y0_,
this_.createdDate AS y1_,
this_.modifiedDate AS y2_,
this_.name AS y3_,
count(DISTINCT doc1_.id) AS y5_
FROM tags this_
LEFT OUTER JOIN tag_joins documents3_
ON this_.id = documents3_.tag AND (doc1_.published = ?)
LEFT OUTER JOIN documents doc1_
ON documents3_.document = doc1_.id AND (doc1_.published = ?)
GROUP BY this_.id
As you can see above, the publishing constraint is applied to both of the left outer joins. I'm not certain whether that is by design, however what I need is for the published constraint to be applied ONLY to the second left outer join.
Any ideas?
I was able to circumvent this problem by coming at it sideways. First, I had to change the "published" column to use an integer rather than a bit. Then I was able to slightly modify the projection of the result as follows:
// Start building the projections
ProjectionList projections =
Projections.projectionList()
.add(Projections.alias(
Projections.groupProperty("id"), "id"))
.add(Projections.alias(
Projections.property("createdDate"),
"createdDate"))
.add(Projections.alias(
Projections.property("modifiedDate"),
"modifiedDate"))
.add(Projections.alias(
Projections.property("name"), "name"));
if (isAdmin()) {
// Give the raw count.
projections.add(Projections.countDistinct("docs.id"), "documentCount");
} else {
// Use the sum of the "published" field.
projections.add(Projections.sum("docs.published"), "documentCount");
}
I acknowledge that this doesn't actually answer the question about why hibernate criteria constraints on many-to-many tables get applied to all tables, but it solved my problem.

Hibernate join table restrictions

I have the following tables:
[ table : column1, column2 ]
A : id, name
B : id, name
AB : idA, idB
AB is a join table.
Then I have this method on hibernate class B
#OneToMany( fetch = FetchType.EAGER)
#JoinTable( name = "AB",
joinColumns = #JoinColumn( name = "idB"),
inverseJoinColumns = #JoinColumn( name = "idA") )
public List<A> getAs(){
//return the list of matching stuff
}
This works perfectly fine.
Now I want to do this sql query in hibernate:
select * from B inner join AB on B.id = AB.idB where AB.idA = 1234
Essentially, 'list me all B's that reference A with id 1234'
I could do straight sql, but that would defeat the purpose of getAs()
Is it possible to construct a Criterion/Restriction clause to achieve this?
Relationship between A and B is not one-to-many in this case, but rather many-to-many. You should map it as such:
#ManyToMany
#JoinTable(name = "AB",
joinColumns = #JoinColumn( name = "idB"),
inverseJoinColumns = #JoinColumn( name = "idA") )
public List<A> getAs(){
//return the list of matching stuff
}
Note that eagerly fetching a collection is not a good idea in most cases, hence fetch = FetchType.EAGER removed above. You can now do the same on A side to make relationship bidirectional:
#ManyToMany(mappedBy='As')
public List<B> getBs(){
//return the list of matching stuff
}
Now getting all Bs for given A is just a matter of calling getBs() on that A instance. You can create criteria / write HQL to do that as well - from either side.

Categories