How to create JPA Specification join between entities - java

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>

Related

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

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.

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.

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.

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...

Categories