Grails query: Get list of associated objects - grails

I have a many-to-many relationship:
class Project {
Set<PrincipalInvestigator> pis
:
static hasMany = [ pis:PrincipalInvestigator ]
}
class PrincipalInvestigator {
String name
:
}
I want a query that returns a unique and sorted list of PIs that belong to a pre-defined list of projects.
A naive approach is to iterate thru the projects, and iterate thru their list of PIs, while removing the dupes. The code to do this is trivial, but it is slow.
So far, the best working solution I could come up with is:
def pi_ids = Project.createCriteria().list{ // find unique list of PI IDs
// project filters here, not relevant to the question
createAlias('pis', 'aka_pis', JoinType.LEFT_OUTER_JOIN)
isNotNull('aka_pis.id')
projections {
distinct('aka_pis.id')
}
}
def pi_list = PrincipalInvestigator.createCriteria().list{ // get PIs from list of IDs
inList('id', pi_ids)
order('name', 'asc')
}
My solution is one order of magnitude faster, but it's still 2 distinct queries. Is there a way to get the same result in a single query?

Using executeQuery makes queries as this alot easier. Something along the following should work:
PrincipalInvestigator.executeQuery("select distinct p.pis from Project p where p.id in :projectIds",
[projectIds: [1,2,3]])

The solution to my problem is this HQL:
PrincipalInvestigator.executeQuery(
"select distinct pi from Project p inner join p.pis as pi where p.id in :projectIds order by pi.name",
[projectIds:[1,2,3]])
This solution allows for sorting of the distinct results and the inner join trims all the null instances. Thanks to cfrick for putting me on the right track.

Related

Grails 3 - return list in query result from HQL query

I have a domain object:
class Business {
String name
List subUnits
static hasMany = [
subUnits : SubUnit,
]
}
I want to get name and subUnits using HQL, but I get an error
Exception: org.springframework.orm.hibernate4.HibernateQueryException: not an entity
when using:
List businesses = Business.executeQuery("select business.name, business.subUnits from Business as business")
Is there a way I can get subUnits returned in the result query result as a List using HQL? When I use a left join, the query result is a flattened List that duplicates name. The actual query is more complicated - this is a simplified version, so I can't just use Business.list().
I thought I should add it as an answer, since I been doing this sort of thing for a while and a lot of knowledge that I can share with others:
As per suggestion from Yariash above:
This is forward walking through a domain object vs grabbing info as a flat list (map). There is expense involved when having an entire object then asking it to loop through and return many relations vs having it all in one contained list
#anonymous1 that sounds correct with left join - you can take a look at 'group by name' added to end of your query. Alternatively when you have all the results you can use businesses.groupBy{it.name} (this is a cool groovy feature} take a look at the output of the groupBy to understand what it has done to the
But If you are attempting to grab the entire object and map it back then actually the cost is still very hefty and is probably as costly as the suggestion by Yariash and possibly worse.
List businesses = Business.executeQuery("select new map(business.name as name, su.field1 as field1, su.field2 as field2) from Business b left join b.subUnits su ")
The above is really what you should be trying to do, left joining then grabbing each of the inner elements of the hasMany as part of your over all map you are returning within that list.
then when you have your results
def groupedBusinesses=businesses.groupBy{it.name} where name was the main object from the main class that has the hasMany relation.
If you then look at you will see each name has its own list
groupedBusinesses: [name1: [ [field1,field2,field3], [field1,field2,field3] ]
you can now do
groupedBusinesses.get(name) to get entire list for that hasMany relation.
Enable SQL logging for above hql query then compare it to
List businesses = Business.executeQuery("select new map(b.name as name, su as subUnits) from Business b left join b.subUnits su ")
What you will see is that the 2nd query will generate huge SQL queries to get the data since it attempts to map entire entry per row.
I have tested this theory and it always tends to be around an entire page full of query if not maybe multiple pages of SQL query created from within HQL compared to a few lines of query created by first example.

Mongoid: Return documents related to relation?

Say I'm modeling Students, Lessons, and Teachers. Given a single student enrolled in many lessons, how would I find all of their teachers of classes that are level 102? For that matter, how would I find all of their lessons' teachers? Right now, I have this:
s = Mongoid::Student.find_by(name: 'Billy')
l = s.lessons.where(level: 102)
t = l.map { |lesson| lesson.teachers }.flatten
Is there a way to do turn the second two lines into one query?
Each collection requires at least one query, there's no way to access more than one collection in one query (i.e. no JOINs) so that's the best you can do. However, this:
t = l.map { |lesson| lesson.teachers }.flatten
is doing l.length queries to get the teachers per lesson. You can clean that up by collecting all the teacher IDs from the lessons:
teacher_ids = l.map(&:teacher_ids).flatten.uniq # Or use `inject` or ...
and then grab the teachers based on those IDs:
t = Teacher.find(teacher_ids)
# or
t = Teacher.where(:id.in => teacher_ids).to_a
If all those queries don't work for you then you're stuck with denormalizing something so that you have everything you need embedded in a single collection; this would of course mean that you'd have to maintain the copies as things change and periodically sanity check the copies for consistency problems.

Grails why join fetch is not recommended for a none single-end association?

i've seen in grails doc something about join fetch :
"This works well for single-ended associations, but you need to be
careful with one-to-manys. Queries will work as you'd expect right up
to the moment you add a limit to the number of results you want. At
that point, you will likely end up with fewer results than you were
expecting. The reason for this is quite technical but ultimately the
problem arises from GORM using a left outer join."
i don't see why a left outer join can raise a problem in a none single-end association like one-to-many for example.
can you give me an example ?
if i take the example given by Joshua
Class Person {
String name
static hasMany = [numbers:PhoneNumber]
static mapping = {
nubmers fetch : 'join'
}
}
Class PhoneNumber{
static belongsTo = [owner : Person]
}
//for testing
def person = Person.get(1)
result in:
select name,... from
person left outer join phone_number
on person.id = phone_number.owner_id
where person.id=1;
Can you give me a query(in gorm) which can show me the problem ?
thanks
If I understand your question correctly then it's easiest to explain with a simple example. Let's say we have a domain class Person with many PhoneNumber classes associated with it. Thus a LEFT JOIN to fetch this data will result in a SQL result like this:
PersonId, Name, PhoneNumberId, TelNumber
-----------------------------------------
1, Joe, 1, 123-123-1234
1, Joe, 2, 222-222-2222
1, Joe, 3, 333-333-3333
2, Jane, 4, 000-000-000
This would be four records returned from the datasource, but only two instances of the Person domain with their associated phone numbers. However, if you limit this query to a maximum of 2 records you will in fact only get one instance of the Person domain (Joe in this example) and his first two phone numbers.
Hope that helps you understand the underlying issue.
P.S. This is an entirely theoretical example, the actual query results from GORM would have different column names and such due to Hibernate.
Update
An example GORM based query is quite simple to demonstrate:
def people = Person.list([max: 2])
You might expect the above to give you two person instances, but in reality because of the left join you will only end up with one instance. Turning on SQL logging will show you the actual query being executed and you will notice it's using a LEFT join.

Grails criteria query retruing duplicate instances

I have a domain class called Order and that class has hasMany relation with Item class.
When I am querying for the list of orders with certain restrictions I am getting as many instances of Order as there are items.
So for example Order instance has say references to 3 instances of Item then , criteria call on Order is returning 3 duplicate instances of Order. I am not sure but if it's worth mentioning that the domain class Order has fetchMode set to "eager".
I am really puzzled with what's going on there. Any help in this regard will be greatly appreciated. Snippet of code is attached:
def clazz = "cust.Order"
def criteria = clazz.createCriteria()
println("clazz == "+Order.list())// returning correct data i.e unique instance of order
def filter = {
// trimmed down all filtering criteria for debugging
}//close filter
List results = criteria.list(max:params?.max,offset:params?.offset,filter)
results.each{Object data->
println(data.getClass())
}
println("results == "+results)
Thanks again
One solution is to use this inside your query:
resultTransformer org.hibernate.Criteria.DISTINCT_ROOT_ENTITY
If you call criteria.listDistinct instead of criteria.list duplicates will be eliminated
Criteria API is just a wrapper for constructing a SQL query. In your case, the query in question has JOINs in it (because of the eager fetching), and returns a cartesian product of Orders and their matching Items. Each row returned is included in results as a separate Order instance.
The easiest way to remove duplicates is to put all the results in a Set, like this:
def resultSet = new HashSet()
resultSet.addAll(results)
println("results == " + resultSet)
You could also use dynamic finders, as in Order.findAllBy* .Depending on how complicated your filter is, this could be easy or tough :)

Compare associations between domain objects in Grails

I am not sure if I am going about this the best way, but I will try to explain what I am trying to do.
I have the following domain classes
class User {
static hasMany = [goals: Goal]
}
So each User has a list of Goal objects. I want to be able to take an instance of User and return 5 Users with the highest number of matching Goal objects (with the instance) in their goals list.
Can someone kindly explain how I might go about doing this?
The easiest and most efficient way to achieve this is using plain SQL. Assuming you have these tables
users [id]
goals [id, description]
user_goals [user_id, goal_id]
You can have the following query to do what you need:
set #userId=123;
select user_id, count(*) as matched from user_goals
where user_id!=#userId
and goal_id in (select ug.goal_id from user_goals ug where ug.user_id=#userId)
group by user_id order by matched desc limit 5;
This takes a user id and returns a list of other users with matching goals, sorted by the number of matches. Wrap it up in a GoalService and you're done!
class GoalService {
def findUsersWithSimilarGoals(user) {
// ...
}
}
It may also be possible to do this with criteria or HQL, but with queries like this it's usually easier to use SQL.
If you're looking for a simple match, perhaps the easiest way would be to do a findAll for each Goal and then count the number of results that each other User appears in:
Map user2Count = [:]
for (goal in myUser.goals){
for (u in User.findAllByGoal(goal)){
def count = user2Count.containsKey(u) ? user2Count.get(u) : 0
count++
user2Count.put(u, count)
}
}
// get the top 5 users
def topUsers = user2Count.entrySet().sort({ it.value }).reverse()[0..5]
This may be too slow, depending on your needs, but it is simple. If many users share the same goals then you could cache the results of findAllByGoal.

Resources