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

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.

Related

How to create JPA Specification join between entities

I would like to create filter based on JPA Specification. I'm using spring-data.
My entites:
public class Section {
/*some other fields*/
#OneToMany
#JoinTable(
name = "section_objective",
joinColumns = #JoinColumn(name = "section_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "objective_id", referencedColumnName = "id")
)
private List<Objective> objectives;
}
public class Objective {
/*some other fields*/
#OneToMany
#JoinTable(
name = "objective_question",
joinColumns = #JoinColumn(name = "objective_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "question_id", referencedColumnName = "id")
)
private List<Question> questions = new ArrayList<>();
}
public class Question {
/*some fields, below code is not important*/
}
Entity dependencies are: Section - one to many -> Objective - one to many -> Question
Entities do not have information about parent relationship (ex. I cannot go from Question to Objective the question is assigned to in Java code).
I have a Question filter class that converts to Specification. I would like to get all questions in section.
Normally I would use SQL query I've written below to get all questions in Section.
SELECT q.*
FROM
question q
JOIN
objective_question oq ON oq.question_id = q.id
JOIN
objective o ON o.id = oq.objective_id
JOIN
section_objective so ON so.objective_id = o.id
JOIN
section s ON s.id = so.section_id
WHERE
s.id IN (1,2);
I've tried creating specification with Join, but don't know how to create join when the reference to Objective on Question is not available.
Specification.<Question>where((root, query, criteriaBuilder) ->
root.join("section") /*throws error because **section** is not a part of attribute on Question */
.in(List.of(1L, 2L)));
You may use the following library: https://github.com/turkraft/spring-filter
It will let you run search queries such as:
/search?filter= average(ratings) > 4.5 and brand.name in ('audi', 'land rover') and (year > 2018 or km < 50000) and color : 'white' and accidents is empty
You can also run search queries even if you don't have an API, the library basically compiles a search input to JPA predicates or to Specification if you desire so. It will smoothly handle all your join queries without having to worry about the well known n+1 query problem.
You may build specifications as follows:
import static com.turkraft.springfilter.FilterBuilder.*;
Specification<Section> spec = new FilterSpecification<Section>(
and(
in("id", numbers(1, 2, 3)),
equal("objectives.questions.field", "some value")
)
);
// basically telling: get the sections which have id 1, 2 or 3, AND which have the field 'field' of the objectives questions' equal to 'some value'
You don't need to configure anything, just import the dependency:
<dependency>
<groupId>com.turkraft</groupId>
<artifactId>spring-filter</artifactId>
<version>1.0.2</version>
</dependency>

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.

JPQL for a Unidirectional OneToMany

I need a jpql query for my Spring repository interface method, to retrieve all Posts for a given Semester.
#LazyCollection(LazyCollectionOption.FALSE)
#OneToMany(cascade = CascadeType.MERGE)
#JoinTable
(
name = "semester_post",
joinColumns = {#JoinColumn(name = "semester_id", referencedColumnName = "id")},
inverseJoinColumns = {#JoinColumn(name = "post_id", referencedColumnName = "id", unique = true)}
)
private List<PostEntity<?>> posts = new ArrayList<>();
PostEntity doesn't have a reference to Semester, and I do not want to add one, because I plan to use this PostEntity for other things than Semester. Maybe I'll have another class (let's say Group) which will also have a OneToMany of PostEntity (like the one in Semester)
So, how do I write this SQL query as a JPQL one ?
select * from posts join semester_post on semester_post.post_id = posts.id where semester_post.semester_id = 1;
My repository
public interface PostRepository extends JpaRepository<PostEntity, Long> {
String QUERY = "SELECT p FROM PostEntity p ... where semester = :semesterId";
#Query(MY_QUERY)
public List<PostEntity> findBySemesterOrderByModifiedDateDesc(#Param("semesterId") Long semesterId);
A query which will get you the result that you need is:
SELECT p FROM SemesterEntity s JOIN s.posts p WHERE s.id = :semesterId
This query uses the JOIN operator to join the SemesterEntity to the PostEntity across the posts relationship. By joining the two entities together, this query returns all of the PostEntity instances associated with the relevant SemesterEntity.

JPA 2.0 CriteriaQuery on tables in #ManyToMany relationship

I have two entities in a #ManyToMany relationship.
// Output has 4 other #ManyToOne relationships if that matters
#Entity #Table public class Output {
#Id public String address;
#ManyToMany(targetEntity = Interval.class,
cascade = CascadeType.ALL,
fetch = FetchType.LAZY)
#JoinTable(name = "output_has_interval",
joinColumns = {#JoinColumn(name = "output_address",
referencedColumnName = "address")},
inverseJoinColumns = {#JoinColumn(name = "interval_start",
referencedColumnName = "start"),
#JoinColumn(name = "interval_end",
referencedColumnName = "end")})
Collection<Interval> intervals;
#IdClass(IntervalPK.class) // I'll omit this one.
#Entity #Table public class Interval {
#Id public Calendar start;
#Id public Calendar start;
#ManyToMany(targetEntity = Output.class,
mappedBy = "intervals",
cascade = CascadeType.ALL,
fetch = FetchType.LAZY)
public Collection<Output> outputs;
The join table is called output_has_interval between output and interval.
How do I do CriteriaQuery like this?
SELECT `output`.`address`
FROM `output`, `output_has_interval`, `interval`
WHERE `output`.`address` = `output_has_interval`.`output_address`
AND `interval`.`start` = `output_has_interval`.`interval_start`
AND `interval`.`end` = `output_has_interval`.`interval_end`
AND `interval`.`start` >= '2011-04-30'
This works as expected if I issue it in MySQL.
(I have the corresponding static meta model classes as well, on request I'll could post them - nothing fancy tho'.)
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Output> cq = cb.createQuery(Output.class);
Root<Output> root= cq.from(Output.class);
CollectionJoin<Output, Interval> join = root.join(Output_.intervals);
Expression<Calendar> start = join.get(Interval_.start);
Predicate pred = cb.greaterThanOrEqualTo(start, /* calendar for '2011-04-30' */);
cq.where(pred);
TypedQuery<Output> tq = em.createQuery(cq);
However tq.getResultList returns every output row from my database. Any idea?
(On a side note: Hibernate (the provider I'm using) generates many select statements when I issue this query, one for every relationship Output has, sometimes more.)
Edit.: I wrote:
tq.getResultList returns every
output row from my database
To clarify it: it returns more than just every output row from my database. It actually does a join using output and interval however the predicate:
`interval`.`start` >= '2011-04-30'
doesn't get satisfied.
Ok, I'll managed to solve my riddle on my own.
First of all: the whole problem originated from the fact that I'm a lousy programmer. I iterated over TypedQuery<Output>.getResultList() and accessed every Interval in Output.intervals in a recursive manner, thus Hiberate loaded lazily the requested objects generating a handful of select statements.
However I had to get a hold of those Interval instaces somehow. The following change to my CriteriaQuery did the trick.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tuple> cq = cb.createTupleQuery(); // or createQuery(Tuple.class)
Root<Output> root= cq.from(Output.class); // from clause
CollectionJoin<Output, Interval> join = root.join(Output_.intervals);
Path<String> addressPath = root.get(Output_.address); // mind these Path objects
Path<Calendar> startPath = join.get(Interval_.start); // these are the key to success!
cq.multiselect(addressPath, startPath); // select clause
Expression<Calendar> start = join.get(Interval_.start);
Predicate pred = cb.greaterThanOrEqualTo(start, /* calendar for '2011-04-30' */);
cq.where(pred); // where clause
TypedQuery<Tuple> tq = em.createQuery(cq); // holds Tuples
for (Tuple tuple : tq.getResultsList()) {
String address = tuple.get(addressPath);
Calendar start = tuple.get(startPath);
...
Edit
I've just realized that I could've used Path<T> objects instead Expression<T> objects (or vice versa) as Path<T> extends Expression<T>. Oh well...

Fetching join using #JoinTable with SQLQuery

I have a mapped entity with a property "latestHistory", which is mapped through a join table, like:
class Record {
#OneToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE }, fetch = FetchType.LAZY, optional = true)
#JoinTable(name = "latest_history_join_view", joinColumns = { #JoinColumn(name = "record_id") }, inverseJoinColumns = { #JoinColumn(name = "history_id") })
#AccessType("field")
public History getLatestHistory() { ... }
}
The mapping works correctly when I call myRecord.getLatestHistory().
I have a complex native SQL query, which returns a batch of Records, and joins on the History for each record using the join table. I want to return Record entites from the query, and have the History objects populated in the result. My attempt looks like this:
StringBuffer sb = new StringBuffer();
sb.append("select {r.*}, {latestHistory.*}");
sb.append(" from record r");
sb.append(" left join latest_history_join_view lh on lh.record_id = r.record_id");
sb.append(" left join history latestHistory on latestHistory.history_id = lh.history_id");
SQLQuery query = session.createSQLQuery(sb.toString());
query.addEntity("r", Record.class).addJoin("latestHistory", "r.latestHistory");
When I do this, it generates a query like:
select
r.record_id, r.name...,
r_1.history_id, --this part is wrong; there is no such alias r_1
latestHistory.history_id, latestHistory.update_date, ...
from record r
left join latest_history_join_view lh on lh.record_id = r.record_id
left join history latestHistory on latestHistory.history_id = lh.history_id
How can I get it to join correctly and fetch my association, without messing up the select list?
[Update: some of the approaches that I've tried:
select {r.*}, {latestHistory.*} -> SQL error, generates a wrong column name "r_1.history_id"
select {r.*}, {anyOtherEntityAssociatedToR.*} -> wrong column name (as above)
select {r.*}, {r.history_id}, {latestHistory.*} -> hibernate error, r has no history_id column
select r.*, lh.history_id as history_id -> this works (though hackish), but doesn't accomplish the join
select r.*, lh.history_id as history_id, latestHistory.* -> appears correct, but results in column name collisions
select r.*, {latestHistory.*} -> error when hibernate looks for a nonexistent column in the result set (this happens if there is any alias at all in the select list)
It doesn't seem to make a lot of difference whether I use addEntity(...) or addJoin(...), as long as the leftmost (root) entity is added using addEntity.
]
I'm thinking you actually need to specify full path for your latestHistory in select e.g.
select {r.*}, {r.latestHistory.*}
otherwise Hibernate gets confused and attempts to treat it as a separate entity. The other option is to not specify injected aliases in select at all which should work for a single "to-one" relationship so long as column order in your tables matches property order in your entities.
I've never tried this on #OneToOne over association table, though.

Categories