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.
Related
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>
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.
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.
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...
I have three tables. AvailableOptions and PlanTypeRef with a ManyToMany association table called AvailOptionPlanTypeAssoc. The trimmed down schemas look like this
CREATE TABLE [dbo].[AvailableOptions](
[SourceApplication] [char](8) NOT NULL,
[OptionId] [int] IDENTITY(1,1) NOT NULL,
...
)
CREATE TABLE [dbo].[AvailOptionPlanTypeAssoc](
[SourceApplication] [char](8) NOT NULL,
[OptionId] [int] NOT NULL,
[PlanTypeCd] [char](2) NOT NULL,
)
CREATE TABLE [dbo].[PlanTypeRef](
[PlanTypeCd] [char](2) NOT NULL,
[PlanTypeDesc] [varchar](32) NOT NULL,
)
And the Java code looks like this.
//AvailableOption.java
#ManyToMany(cascade={CascadeType.ALL}, fetch=FetchType.EAGER)
#JoinTable(
name = "AvailOptionPlanTypeAssoc",
joinColumns = { #JoinColumn(name = "OptionId"),
#JoinColumn(name="SourceApplication")},
inverseJoinColumns=#JoinColumn(name="PlanTypeCd"))
List<PlanType> planTypes = new ArrayList<PlanType>();
//PlanType.java
#ManyToMany
#JoinTable(
name = "AvailOptionPlanTypeAssoc",
joinColumns = { #JoinColumn(name = "PlanTypeCd")},
inverseJoinColumns={#JoinColumn(name="OptionId"),
#JoinColumn(name="SourceApplication")})
List<AvailableOption> options = new ArrayList<AvailableOption>();
The problem arises when making a select on AvailableOptions it joins back onto itself. Note the following SQL code from the backtrace. The second inner join should be on PlanTypeRef.
SELECT t0.OptionId,
t0.SourceApplication,
t2.PlanTypeCd,
t2.EffectiveDate,
t2.PlanTypeDesc,
t2.SysLstTrxDtm,
t2.SysLstUpdtUserId,
t2.TermDate
FROM dbo.AvailableOptions t0
INNER JOIN dbo.AvailOptionPlanTypeAssoc t1
ON t0.OptionId = t1.OptionId AND t0.SourceApplication = t1.SourceApplication
INNER JOIN dbo.AvailableOptions t2
ON t1.PlanTypeCd = t2.PlanTypeCd
WHERE (t0.SourceApplication = ? AND t0.OptionType = ?)
ORDER BY t0.OptionId ASC, t0.SourceApplication ASC
[params=(String) testApp, (String) junit0]}
You are mapping a bidirectional association. That means you have to choose one side as the owner of the association. This side will be responsible for updating the relationship in the database.
If you choose AvailableOption as the owner of the relationship and you want a new PlanType for it, you have to add the plantype to the option. Adding the option only to the plantype will have no effect.
Here is the Mapping:
//AvailableOption.java
#ManyToMany(cascade={CascadeType.ALL}, fetch=FetchType.EAGER)
#JoinTable(
name = "AvailOptionPlanTypeAssoc",
joinColumns = { #JoinColumn(name = "OptionId"),
#JoinColumn(name="SourceApplication")},
inverseJoinColumns=#JoinColumn(name="PlanTypeCd"))
List<PlanType> planTypes = new ArrayList<PlanType>();
//PlanType.java
#ManyToMany(
mappedBy = "planTypes"
)
List<AvailableOption> options = new ArrayList<AvailableOption>();
You may also refer to the hibernate annotation documentation chapter 2.2.5
Regards
David