-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 14 posts ] 
Author Message
 Post subject: Criteria queries: using subselects instead of joins
PostPosted: Wed Feb 15, 2006 10:12 pm 
Newbie

Joined: Wed Feb 15, 2006 8:52 pm
Posts: 7
Hibernate version: 3.1.2

There doesn't seem to be any way of performing a criteria query similar to:

Q1: from Parent p where exists(from p.children c where c.shoesize >= 10)

The closest I can get is:
Criteria crit = session.createCriteria(Parent.class);
Criteria subCrit = crit.createCriteria("children");
subCrit.add(Expression.ge("shoesize", 10));
crit.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

but that's not quite the same, it's more like:

from Parent p join p.children c where c.shoesize >= 10

sure in this particular instance it will produce the same results, but if I had somthing like:

Q2: from Parent p where 2 <= (select count(*) from p.children c where c.shoesize >= 10)

there's no way that I can see that it can be done with criteria queries.

Has anyone got any solution?


Mapping documents:
NB: I generaly use annotations, but you'll get the gist:

Code:
<hibernate-mapping package="org.test">
   <class name="Parent">
      <id name="id">
         <generator class="auto"/>
      </id>
      <property name="name" not-null="true"/>
      <set name="children" cascade="delete">
         <key column="parentId"/>
         <one-to-many class="Child"/>
      </set>
   </class>
   <class name="Child">
      <id name="id">
         <generator class="auto"/>
      </id>
      <property name="shoesize"/>
      <many-to-one name="parent" column="parentId"/>
   </class>
</hibernate-mapping>



Last edited by swamp_ig on Thu Feb 16, 2006 2:05 am, edited 1 time in total.

Top
 Profile  
 
 Post subject:
PostPosted: Wed Feb 15, 2006 11:03 pm 
Expert
Expert

Joined: Thu Dec 23, 2004 9:08 pm
Posts: 2008
It's not simple, but it can be done. Check the code example titled "Even correlated subqueries are possible:" in section 15.8, "Detached queries and subqueries", of the ref docs.


Top
 Profile  
 
 Post subject:
PostPosted: Thu Feb 16, 2006 12:02 am 
Newbie

Joined: Wed Feb 15, 2006 8:52 pm
Posts: 7
Yes I looked at that.

I assume you're suggesting somthing like the equivalent of:

from Parent p
where exists(from Child c where c in p.children and c.shoesize >= 10)

This can't be expressed, there's Restrictions.in which finds if a property is in some externaly defined collection of values, and there's Subqueries.propertyIn which matches a property to a subquery, but there's no way of creating a subquery from a property without joining it to the parent class, so the "c in p.children" part can't be expressed.

We'd either need a Restrictions.inProperty or better yet a method like

DetachedCriteria.fromParentPath("children");


Top
 Profile  
 
 Post subject:
PostPosted: Thu Feb 16, 2006 12:21 am 
Expert
Expert

Joined: Thu Dec 23, 2004 9:08 pm
Posts: 2008
I was considering your second example, since (as you pointed out) "from Parent p where exists(from p.children c where c.shoesize >= 10)" is functionally equivalent to "from Parent p join p.children c where c.shoesize >= 10". DetachedCriteria can be use to express subqueries using the Property class. I haven't done this, but something along these lines is needed (probably):
Code:
DetachedCriteria shoeSizeCrit = DetachedCriteria.forClass(Child.class).add(Restrictions.ge("shoeSize", 10);
session.createCritera(Parent.class).add(Property.forName("child").in(shoeSizeCrit));


Top
 Profile  
 
 Post subject:
PostPosted: Thu Feb 16, 2006 2:56 am 
Newbie

Joined: Wed Feb 15, 2006 8:52 pm
Posts: 7
So you agree that Q2 (in the original post) cannot be expressed in the criteria API as written?

What I'm realy getting at is that the Criteria API as it stands doesn't provide any way to subselect within a collection on the parent. If you rely on joins and Criteria.DISTINCT_ROOT_ENTITY there are some things that are at best messy and at worst impossible to express.

If this is true then this is a crucial missing feature, which realy should be added to the JIRA issue database.

It's true you could reexpress Q2 somthing like:

select p
from Parent p join p.children c
where c.shoesize >= 10
group by p
having count(*) > 2

but this isn't realy my point. Throw in a few more sub selects and the resulting query gets messier and more complicated, and I'm sure in some cases impossible. For example try expressing this without using subselects:

from Parent p
where 2 <= (from p.children c where c.shoesize >= 10)
or (p.age*2) > all (select c.age from p.children where c.shoesize < 5)


Top
 Profile  
 
 Post subject:
PostPosted: Thu Feb 16, 2006 3:16 am 
Regular
Regular

Joined: Wed Dec 17, 2003 1:58 pm
Posts: 102
I am dealing with a similar problem, I can't quite tell if it is exactly what you have or not. I need a query to return a parent class (based on ids) containing only a subset of one of it's child lists based on a date range. IE something like:

Code:
from parent p left join fetch p.children c where p.id in (:ids) and c.date >= :begin and c.date <= :end


However this query ONLY selects parents that have children within that range. I'd like it to select all the parents with id in :ids, and then of those parents select only children for the parents that fall within the date range. Is this doable?


Top
 Profile  
 
 Post subject:
PostPosted: Thu Feb 16, 2006 3:30 am 
Newbie

Joined: Wed Feb 15, 2006 8:52 pm
Posts: 7
You're grabbing the thing from the wrong end, try:

Code:
from child c
where c.date >= :begin and c.date <= :end
  and c.parent.id in (:ids)


or if there's no mapping from child to parent:

Code:
from child c
where c.date >= :begin and c.date <= :end
  and exists(from parent p where p.id in (:ids)  and c in c.children)


but this has nothing to do with my issue, which is to do with a missing feature in Criteria queries.


Top
 Profile  
 
 Post subject:
PostPosted: Thu Feb 16, 2006 3:34 am 
Regular
Regular

Joined: Wed Dec 17, 2003 1:58 pm
Posts: 102
Sorry to hijack your thread.. back to the topic =)


Top
 Profile  
 
 Post subject:
PostPosted: Thu Feb 16, 2006 4:33 pm 
Expert
Expert

Joined: Thu Dec 23, 2004 9:08 pm
Posts: 2008
swamp_ig, I think that Critera do provide a way to do subselects within a parent's collection, and that my earlier example (the one that uses Property.in(DetachedCriteria)) is something like the way to do it. Have a go at solving your problem with some variation on my sample code.


Top
 Profile  
 
 Post subject:
PostPosted: Sun Feb 19, 2006 10:35 pm 
Newbie

Joined: Wed Feb 15, 2006 8:52 pm
Posts: 7
Property.forName() works only with properties, not collections.

I'm pretty sure of what I'm saying here, there's simply no way of expressing Q1 in the criteria API. There's a few bits of evidence:

1:
Code:
      Criteria crit = Criteria.forClass(Parent.class);
      Criteria sub = crit.createCriteria("children");
      sub.add(...);
      crit.add(Subqueries.exists(sub));


This doesn't compile since Subqueries.exists expects a detached criteria.

2: If you comment the non-compiling line in the above the resulting SQL shows a join between the two tables. If the exists were to work this join couldn't be there, the property path would have to exist in the sub-select only.

3: There's no way to get around it. If we had Restrictions.inProperty we could do somthing like:

Code:
      Criteria crit = Criteria.forClass(Parent.class, "p");
      Criteria sub = DetachedCriteria.forClass(Child.class, "c");
      sub.add(Restrictions.inProperty("this", "p.children");
      sub.add(...);
      crit.add(Subqueries.exists(sub));


or as HQL:

Q3: from Parent p where exists(from Child c where c in p.children and ...)

but there's no in restriction which restricts to a existing collection, they only take either a subquery (no good to us) or a set of values (also no good).

If you don't believe this is correct please give me some code rather than a vauge assertion to the contrary, otherwise I'll submit this as a bug.


Top
 Profile  
 
 Post subject:
PostPosted: Sun Feb 19, 2006 11:39 pm 
Expert
Expert

Joined: Thu Dec 23, 2004 9:08 pm
Posts: 2008
Is this close to correct? I have a MeterPoint class, containing a Set of Installations. Installations have start dates and end dates. I want to select all meter points that have installations date ranges between 2 and 1 years ago, and where the meter point name begins with H:
Code:
      DetachedCriteria sub = DetachedCriteria.forClass(InstallationImpl.class);
      sub.setProjection(Projections.property("id"));
      sub.add(Restrictions.gt("Start", EIDate.getInstance().addYears(-2)));
      sub.add(Restrictions.lt("End", EIDate.getInstance().addYears(-1)));
      Criteria crit = session.createCriteria(MeterPointImpl.class);
      crit.add(Restrictions.like("Name", "H", MatchMode.START);
      crit.add(Subqueries.exists(sub));
      for (Object mp : crit.list())
      {
        System.out.println("MP: " + ((MeterPoint) mp).getName());
      }
This generated the follow SQL, which looks correct to me:
Code:
Hibernate: select this_.MeterPointID as MeterPoi1_6_,
this_.MeterPointName as MeterPoi2_14_6_,
this_.RetailerNote as Retailer3_14_6_,
this_.LocationID as LocationID14_6_,
this_.ManagementCompanyID as Manageme5_14_6_,
from MeterPoint this_
where exists (select this0__.InstallationID as y0_ from Installation this0__
              where this0__.StartDateTime>? and this0__.EndDateTime<?)
Unfortunately I'm a bit pressed for time today, so I haven't compared the results with what I'd expect, but even if it's not right, it's close. I'm sure I could get the right results from this.

However, it's an excessively complex solution. You could submit inProperty as a development request?

Note that I did encounter one bug-like issue, in that the subquery required a projection. It threw an exception until I added the dummy one.


Top
 Profile  
 
 Post subject:
PostPosted: Sun Feb 19, 2006 11:54 pm 
Newbie

Joined: Wed Feb 15, 2006 8:52 pm
Posts: 7
Nope. The subquery here is not correlated with the containing query at all. The subquery clause would have to look somthing like "exists(from parent.parent_collection where ...". IE: checking for some criteria being true in one of the collections on the parent.

The clue here is that the generated SQL doesn't refer to the containing table.


Top
 Profile  
 
 Post subject:
PostPosted: Mon Feb 20, 2006 1:02 am 
Expert
Expert

Joined: Thu Dec 23, 2004 9:08 pm
Posts: 2008
Right them, criteria where the subselect clause includes comparison with tables in the principal select:

MeterPoint contains a reference to Location, which contains a reference to an Address. This criteria finds all meter points that are named after the city they're in, so long as it begins with W:
Code:
      DefaultMeterPointFactory fact = (DefaultMeterPointFactory) lookup(MeterPointFactory.ROLE);
      Session session = fact.session();

      DetachedCriteria sub1 = DetachedCriteria.forClass(AddressImpl.class, "addr");
      sub1.setProjection(Projections.property("id"));
      sub1.add(Property.forName("addr.City").eqProperty("mp.Name"));

      Criteria crit = session.createCriteria(MeterPointImpl.class, "mp");
      crit.add(Restrictions.like("Name", "W%"));
      crit.createAlias("Location", "mploc");
      crit.createAlias("mploc.Address", "mpaddr");
      crit.add(Subqueries.propertyEq("mpaddr.id", sub1));
This generates the following SQL:
Code:
select this_.MeterPointID as MeterPoi1_6_,
this_.MeterPointName as MeterPoi2_14_6_,
this_.RetailerNote as Retailer3_14_6_,
this_.LocationID as LocationID14_6_,
this_.ManagementCompanyID as Manageme5_14_6_,
mploc1_.LocationID as LocationID0_,
mploc1_.Locator1 as Locator2_4_0_,
mploc1_.AddressID as AddressID4_0_,
mpaddr2_.AddressID as AddressID1_,
mpaddr2_.Number as Number3_1_,
mpaddr2_.Street as Street3_1_,
mpaddr2_.Suburb as Suburb3_1_,
mpaddr2_.City as City3_1_,
mpaddr2_.State as State3_1_,
mpaddr2_.PostalCode as PostalCode3_1_,
mpaddr2_.Country as Country3_1_
inner join Location mploc1_ on this_.LocationID=mploc1_.LocationID
inner join Address mpaddr2_ on mploc1_.AddressID=mpaddr2_.AddressID
where this_.MeterPointName like ?
  and mpaddr2_.AddressID = (select this0__.AddressID as y0_ from Address this0__
                            where this0__.City=this_.MeterPointName)
It tested it this time, it works. this0__ is the Address in the subquery, this_ is the meter point from the main query, and they're compared in the subquery. Is that what you wanted?


Top
 Profile  
 
 Post subject:
PostPosted: Mon Feb 20, 2006 3:02 am 
Newbie

Joined: Wed Feb 15, 2006 8:52 pm
Posts: 7
That will produce somthing more like it in this case, but it still requires rearranging the original query in odd ways. Address still appears as a join in the outer query. In this particular example it's not going to be a problem, (asuming you use DISTINCT_ROOT_ENTITY as a filter) but there are cases where it will prove difficult.

for example the following HQL:

Code:
from Parent p
where (select count(*) from p.child c where c.age < 5) = 2
     or p.age > (select 2*avg(c.age) from p.child c and c.age > 10)


(in english: parent's age is either twice the average age of the children over ten, or they have two children under 5)

If we only had the bit before the first OR it wouldn't be such a problem, you could even get rid of the subselect entirely and use group by and having to replace it. But as soon as you try and filter the same collection in two different ways the rearangement becomes impossible.

So what I'm looking for is a criteria query which contains a subselect that refers to a collection owned by the parent, but without the parent containing any joins. The joins appear in the subselect only.

Anyhow, given the continued fragility of the criteria API I've given up and am just concatenating bits of HQL together like I ended up doing in the last project.


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 14 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.