This has turned out to be really difficult to google for, so I'll try my luck here.
I have two classes like this:
#Entity
public class Cat {
#Id
private Integer catId;
private String name;
private String color;
#OneToMany
private List<Kitten> kittens;
}
#Entity
public class Kitten {
#Id
private Integer kittenId;
private String name;
private String color;
#ManyToOne
private Cat parent;
}
And I'm looking to find all grey Cat that has a Kitten with a name starting with P and that are white and a separate kitten that has a name starting with Q and is black. So far I've got this:
Criteria criteria = session.createCriteria(Cat.class);
criteria.add(Restriction.eq("color", "grey"));
criteria.createAlias("kittens", "k");
criteria.add(Restriction.and(
Restriction.eq("k.color", "white"),
Restriction.ilike("k.name", "P%")
));
criteria.add(Restriction.and(
Restriction.eq("k.color", "black"),
Restriction.ilike("k.name", "Q%")
));
List<Cat> results = criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY).list();
Now this doesn't work and gives me no hits at all even though such a Cat exists. Removing the very last restriction set works just fine. I imagine I need a different approach than just adding another restriction for the second Kitten but I haven't managed to figure out what it is I want to do.
It's also important that I don't match a Cat with four kittens that have one of these properties each.
The reason you fail to fetch after adding the second restrictions is that you tell criteria to find a kitten with all of those restriction: black and white - which of course doesnt exist unless it is shrodinger's cat.
You can accomplish this by joining cat with kittens twice: create two aliases for kittens: k1, k2 and restrict one by the first kitten and the second by the second kitten.
New answer based on comments:
In SQL I think something like this would work. I don't know Hibernate, but you can use this as starting point to convert the concept into code.
Select
C.name, count(distinct K.color)
From Cat C, Kitten K
Where
C.color = 'grey'
and C.id = K.catId
and (
(K.color = 'white' and K.name like 'P%')
or
(K.color = 'black' and K.name like 'Q%')
)
group by C.name
having count(distinct K.color) = 2
This first joins the eligible Cats on the eligible Kittens. By itself that does not solve the problem (as evidenced by the Old Content below and the comment chain), but then if you add a grouping to count the number of distinct kitten colors that were found per cat, if it equals two then it means the cat has one of each kitten.
See this SQLFiddle which I used to test it out: http://sqlfiddle.com/#!9/ee93f5/1/0
Old content (retained so that comments make sense):
I don't know hibernate but I think the SQL should look like:
Select <your stuff>
From Cat C, Kitten K
Where
C.color = 'grey'
and C.id = K.catId (I guess Hibernate provides these keys automatically?)
and (
(K.color = 'white' and K.name like 'P%')
or
(K.color = 'black' and K.name like 'Q%')
)
Basically, the two kitten constraints must be part of an OR clause which is added to overall constraint as an AND clause.
In your example, if I'm understanding the Hibernate code correctly, all of the constraints are added as AND clauses, so the result set will be empty because any Kitten row your Cat joins on cannot be black and white at the same time.
Related
Currently we have a class that looks something like that (depersonalised and non-relevant parts removed):
#Entity
#Table(name = "MAIN_TABLE")
public class MainTable extends AbstractTable {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "mainTable")
#OrderBy("CREATED_ON DESC")
private Set<MainTableState> states;
...
public MainTableState getActiveState(){
if(this.states == null || this.states.isEmpty()){
return null;
}
MainTableState latest = states.iterator().next();
// The reason we use this for-loop, even though we have the #OrderBy annotation,
// Is because we can later add states to this list, which aren't automatically ordered
for(MainTableState state : states){
if(state.getCreatedOn() != null && latest.getCreatedOn() != null &&
state.getCreatedOn().after(latest.getCreatedOn()){
latest = state;
}
}
return latest;
}
...
}
So currently it will retrieve all MainTableStates from the DB by default, and if we need the activeState we use the for-loop method. Obviously this is pretty bad for performance. Currently we don't use this list at all (the purpose was to have a history of states, but this has been postponed to the future), but we do use the getActiveState() method quite a bit, mostly to show a String inside of the MainTableState-class in the UI.
In addition, even if we would always use a TreeSet and keep it sorted so we won't need the loop but only need states.iterator().next() instead, it will still initialize the list of states. With some heavy performance testing we had more than 1 million MainTableState-instances when it crashed with an java.lang.OutOfMemoryError: GC overhead limit exceeded.
So, we want to change it to the following instead:
#Entity
#Table(name = "MAIN_TABLE")
public class MainTable extends AbstractEntity {
#???
private MainTableState activeState;
...
public MainTableStates getActiveState(){
return activeState;
}
...
}
So, my question, what should I put at the #??? to accomplish this? I'm assuming I need the #Formula or something similar, but how can I say to hibernate it should return a MainTableState object? I've seen #Formula being used with MAX for a date, but that was to get that date-property, not get an entire object based on that max date.
After #user2447161's suggestion I've used a #Where-annotation, which does indeed help to reduce the Collection size to 1 (sometimes), but I have two more related questions:
How to use #OnToMany and #Where but get a single object, instead of a list of objects of size one? Is this even possible? Here in a answer from December 2010 it is stated it isn't. Has this been fixed somewhere in the last six years?
How to deal with the random alias in the where clause? I could do something like this:
#OneToMany(fetch = FetchType.LAZY, mappedBy = "mainTable")
#Where(clause = "CREATED_ON = (SELECT MAX(mts.CREATED_ON) FROM MAIN_TABLE_STATES mts WHERE mts.FK_MAIN_ID = ???.MAIN_ID)")
private Set states; // TODO Get single object instead of collection with size 1
The problem with is that ??? is a random alias generated by hibernate (sometimes it's this_, sometimes it's something along the lines of mainTable_1_, etc.). How to set this alias for the entire query to the DB to use it here? I also tried MAIN_TABLE.MAIN_ID instead which doesn't work, and with no alias it also doesn't work because it uses the MainTableState-alias instead of MainTable-alias (like this below).
from
MAIN_TABLE this_
left outer join
MAIN_TABLE_STATUSES mainstat2_
on this_.main_id=mainstat2_.fk_main_id
and (
mainstat2_.created_on = (
SELECT
MAX(mts.created_on)
FROM
MAIN_TABLE_STATUSES mts
WHERE
-- mainstat2_.main_id should be this_.main_id instead here:
mts.fk_main_id = mainstat2_.main_id
)
)
Well, regarding your question #2, as it looks like you need a quick solution with minimal impact in your existing code, this may be acceptable: you can use an Interceptor to deal with the alias and generate the right sql statement. Do this:
use a unique string as alias placeholder in your #Where clause, for instance:
...WHERE mts.FK_MAIN_ID = ${MAIN_TABLE_ALIAS}.MAIN_ID...
if your application doesn't have one yet, create an Interceptor class extending EmptyInterceptor and configure it as a SessionFactory interceptor
override the onPrepareStatement method to replace the placeholder with the alias found after 'from MAIN_TABLE' with something like this:
public String onPrepareStatement(String sql) {
String modifiedSql = sql;
if (sql.contains("${MAIN_TABLE_ALIAS}")) {
String mainTableAlias = findMainTableAlias(sql);
modifiedSql = sql.replace("${MAIN_TABLE_ALIAS}", mainTableAlias);
}
return modifiedSql;
}
Be aware that this method will be called for every sql statement that hibernate generates in your application.
Additionaly, your #Where clause only works properly when a join is used, so you should set the fetch mode explicitly adding
#Fetch(FetchMode.JOIN)
to the states property to avoid that hibernate may use the select mode.
I cannot find a solution to a problem that seems to be easy. Say there are 2 entity classes:
class A {
Set<B> bs;
}
class B {
String text;
}
How to create a criteria query that returns all A's that contains at least one B entity which fulfills a given condition (like b.text = 'condition')?
I think this link can be useful:
http://mikedesjardins.net/2008/09/22/hibernate-criteria-subqueries-exists/
It contains the following example about how create n exists criteria:
"What you’re really trying to do is to obtain all Pizza Orders where an associated small pizza exists. In other words, the SQL query that you’re trying to emulate is
SELECT *
FROM PIZZA_ORDER
WHERE EXISTS (SELECT 1
FROM PIZZA
WHERE PIZZA.pizza_size_id = 1
AND PIZZA.pizza_order_id = PIZZA_ORDER.pizza_order_id)
The way that you do that is by using an “exists” Subquery, like this:
Criteria criteria = Criteria.forClass(PizzaOrder.class,"pizzaOrder");
DetachedCriteria sizeCriteria = DetachedCriteria.forClass(Pizza.class,"pizza");
sizeCriteria.add("pizza_size_id",1);
sizeCriteria.add(Property.forName("pizza.pizza_order_id").eqProperty("pizzaOrder.pizza_order_id"));
criteria.add(Subqueries.exists(sizeCriteria.setProjection(Projections.property("pizza.id"))));
List<pizzaOrder> ordersWithOneSmallPizza = criteria.list();
And voila, the result will contain two PizzaOrders!"
My desired query is to get a list of Course objects that belong to a Category. My objects are as follows:
public class Course{
String name;
List<Category> categories;
}
public class Category{
String name;
Category parent;
}
Since the categories reference each other, they can have an infinite depth:
A
A.A
A.A.A
A.A.B
A.B
A.B.A
B
B.A
B.B
C
How can I query for courses within the category "A.A", and return all Courses associated with A.A, A.A.A, and A.A.B?
If you are willing to use native SQL and your database supports recursive common table expressions (basically all major DBMS except MySQL) it's pretty easy:
WITH RECURSIVE course_tree (name) AS (
SELECT name
FROM course
WHERE name = 'A.A'
UNION ALL
SELECT name
FROM course
WHERE parent_id = course_tree.id
)
SELECT *
FROM course_tree
Because you do not know how deep is the tree, you can use some kind of pattern as follows
select distinct
c
from
Course c
left join fetch
c.categories c
where
c.name like 'A.A%'
Currently, I have an HQL query that returns all Members who possess ANY Award from a set of specified Awards:
from Member m left join m.awards as a where a.name in ("Trophy","Ribbon");
What I now need is HQL that will return all Members who possess ALL Awards specified in the set of Awards.
So, assuming this data:
Joe has Trophy, Medal
Sue has Trophy, Ribbon
Tom has Trophy, Ribbon, Medal
The query above would return Joe, Sue, and Tom because all three possess at least one of Trophy or Ribbon. But I need to return only Sue and Tom, because they are the only ones who possess all of the specified awards (Trophy and Ribbon).
Here's the class structure (simplified):
class Member {
private String name;
private Set<Award> awards;
}
class Award {
private String name;
}
select m from Member m left join m.awards as a where a.name in ("Trophy","Ribbon") group by m having count(a)=2
Just repeating myself...
The code to get the members that have EXACTLY the given collection of awards:
from Member m
where not exists (
from Award a where a.name in {"Trophy", "Ribbon"}
and a not in(
select * from Award a2 where a2.owner = m
)
) and not exists (
from Award a3 where a3.owner = m and a3 not in {"Trophy", "Ribbon"}
)
You can force distinct results by adding a DISTINCT_ROOT_ENTITY result transformer to the query call, IE:
getSession().createQuery(hql).setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)
I'm having a similar problem, but what I need to do is (following your example) to select all the members, who posess ALL of the awards and no more. So in your example the only correct result would be Sue. Any ideas?
I'm trying to build a smaller SQL, to avoid the "select * from A" that is being build by default for hibernate Criteria.
If I use simple fields (no relation), through "Transformers", I have can manage to have this SQL:
select description, weight from Dog;
Hi, I have this Entity:
#Entity
public class Dog
{
Long id;
String description;
Double weight;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "person_id", nullable = false)
Person owner;
}
#Entity
public class Person
{
Long id;
String name;
Double height;
Date birthDate;
}
My goal is to have this:
select description, weight, owner.name from Dog
I tried this with with Criteria (and subcriteria):
Criteria dogCriteria = sess.createCriteria(Dog.class);
ProjectionList proList = Projections.projectionList();
proList.add(Projections.property("description"), description);
proList.add(Projections.property("weight"), weigth);
dogCriteria.setProjection(proList);
Criteria personCriteria = dogCriteria.createCriteria("owner");
ProjectionList ownerProList = Projections.projectionList();
ownerProList.add(Projections.property("name"), description);
dogCriteria.setProjection(ownerProList); //After this line, debugger shows that the
//projection on dogCriteria gets overriden
//and the query fails, because "name" is
//not a field of Dog entity.
How should I use Projections, to get a smaller SQL, less columns ?
Thanks in advance.
First of all,
select description, weight, owner.name from Dog
is not valid SQL. It would have to be something like
select description, weight, Person.name
from Dog join Person on Dog.person_id = Person.id
instead. Secondly, why? While it's possible to do what you want (see below), it's extremely verbose to do so via Criteria API and you gain nothing to show for it. Savings on data transfer for a couple of columns are negligible unless said columns are huge blobs or you're selecting hundreds of thousands of records. In either case there are better ways to deal with this issue.
Anywho, to do what you want for criteria, you need to join linked table (Person) via alias and specify projection on main criteria using said alias:
Criteria criteria = session.createCriteria(Dog.class, "dog")
.createAlias("owner", "own")
.setProjection( Projections.projectionList()
.add(Projections.property("dog.description"))
.add(Projections.property("dog.weight"))
.add(Projections.property("own.name"))
);
There's a description and an example of the above in Criteria Projections documentation. Keep in mind that, when executed, the above criteria would return a list of object arrays. You'll need to specify a ResultTransformer in order to have results converted into actual objects.
I didn't tried it yet by myself, but I think you can also use another constructor in your Entity (Pojo) and pass the columns there.
See https://www.thoughts-on-java.org/hibernate-best-practices/ chapter "1.2 Pojo" for a detailed instruction.
Altough for me it's not yet clear if this also works for ManyToOne relationships too. I will have a try.