GORM - change 3 queries into 1 (or at least 2) - grails

I have such entities, stored in database using GORM (I omitted irrelevant fields):
class Payment {
static hasMany = [paymentEntries: PaymentEntry]
}
class PaymentEntry {
static hasOne = [category: PaymentCategory]
static belongsTo = [payment: Payment]
static constraints = {
category(nullable: true)
}
}
class PaymentCategory {
static hasMany = [payments: PaymentEntry, rules: PaymentRule]
String name
}
So we have Payments on the top, which can have many PaymentEntryies, and each PaymentEntry can belong to one PaymentCategory.
I need to select Payments, which meet some conditions, but only the ones that also have PaymentEntries belonging to specific categories. Currently I do it in 3 queries:
Select categories which fit to part of category name, or just omit category query (in such case we want all the payments regardless of their entries category):
private static List<PaymentCategory> getCategories(String category) {
def result = null
if (category != null && category.length() > 0) {
def c = PaymentCategory.createCriteria()
result = c {
ilike("name", "%${category}%")
}
}
result
}
Select PaymentEntries ids, based on PaymentCategory:
private static List<Long> paymentEntryIds(String category) {
def categories = getCategories(category)
def c = PaymentEntry.createCriteria()
def result = new ArrayList()
// If category is selected, but there is no matching category, return empty list
if (!categorySelectedButNoMatch(category, categories)) {
result = c {
if (category == null) {
isNull("category")
} else if (categories != null && categories.size() > 0) {
inList("category", categories)
}
projections {
property("id")
}
}
}
result
}
Finally select Payments, limited to the ones that contain specific PaymentEntries:
def paymentEntriesIds = paymentEntryIds(selectedCategory)
def c = Payment.createCriteria()
def result = new ArrayList()
// If there are no payment entries matching category criteria, we return empty list anyway, so there
// is no need querying payments.
if (paymentEntriesIds.size() > 0) {
result = c {
paymentEntries {
inList("id", paymentEntriesIds)
}
if (importEntryId != null) {
eq("importEntry.id", importEntryId)
}
if (query != null) {
ilike("description", query)
}
// Omitted ordering and paging
}
}
result
This works, but it runs 3 queries to the database. I'm pretty sure this code could be cleaner, and it could be done in less queries. All the ideas on how to improve it are welcome.

You can use at least 3 different methods: detached queries, subqueries of where queries and subqueries of HQL.
Example of DetachedQuery with criteria looks like:
def subquery = new DetachedCriteria(PaymentCategory).build {
projections {
groupProperty 'id'
}
ilike("name", "%${category}%")
}
Then you can use this subquery in another query:
return PaymentEntry.createCriteria().list {
category {
inList "id", subquery
}
}
I didn't test it on your domain exactly, this should just show the idea. Refer to DetachedCriteria part of GORM documentation: https://grails.github.io/grails-doc/latest/guide/single.html#detachedCriteria
We use it for some very complicated queries. Sometimes createAlias will be required to correctly work with associations.

Related

CreateCriteria - Use logical OR in hasMany association

I want to get instances who contain in their lists (firstsList or SecondsList) a specific user.
In my solution, the create criteria takes into account only the first list of users.
It seems to be a bad usage of the logical OR
Domain
class ClassA {
static hasMany = [firstsList:User,SecondsList:User]
}
Service
def idList = ClassA.createCriteria().list () {
projections { distinct ( "id" )
property("name")
property("id")
}
or {
firstsList{eq("login", 'John')}
SecondsList{eq("login", 'John')}
}
order("name","desc")
}
return idList
The reason behind this is hibernate by default uses inner join. But in your case you need left join. For that you can use createAlias of createCriteria.
def idList = ClassA.createCriteria().list() {
projections {
distinct("id")
property("name")
}
createAlias("firstsList", "fl", JoinType.LEFT_OUTER_JOIN)
createAlias("SecondsList", "sl", JoinType.LEFT_OUTER_JOIN)
or {
eq("fl.login", "John")
eq("sl.login", "John")
}
order("name", "desc")
}

Search and get all parents that contains a child with value

class Client {
String name
static hasMany = [courses:Course]
}
class Course {
String name
static belongsTo = [client:Client]
}
I have this and I want to get all Clients that has a Course with name = "blabla"
I was trying to do : Clients.findWhere(Course.any { course -> course.name = "math" })
You can do this with criteria:
Client.withCriteria {
courses {
eq('name', 'math')
}
}
I believe that the following where query is equivalent to the above criteria:
Client.where { courses.name == 'math' }
or you may find you need another closure:
Client.where {
courses {
name == 'math'
}
}
but I rarely use where queries myself so I'm not 100% sure of that.
There are probably a lot of different syntactical expressions to achieve the same thing. I can say definitively that this works in my project though.
def ls = Client.list {
courses {
eq('name','math')
}
}

Grails filter domain class by property of another domain

I have a Grails application and want to create filters for my domain class using named query.
I have domains Act and Status, StatusName is an Enum:
class Act {
static hasMany = [status : Status]
}
class Status {
Date setDate
StatusName name
static belongsTo = [act : Act]
}
I want to filter Acts which have their most recent Status's name equal to specific name.
For now I have this code in Act:
static namedQueries = {
filterOnStatus { StatusName s ->
status {
order('setDate', 'desc')
eq 'name', s
// I need only first Status, with most recent setDate
// among all Statuses of that Act
}
}
}
But this filter all Acts that have Status with specific name, not only with most recent. I tried to place maxResult 1 in query, but it seems not to work.
Any help would be appreciated.
EDIT: Problem was solved that way:
filteronStatus {
createAlias('status', 's1')
eq 's1.name', s
eq 's1.setDate', new DetachedCriteria(Status).build {
projections {
max('setDate')
eqProperty('act', 's1.act')
}
}
}
see 'namedQueries' from Grails Doc
// get a single recent Act
def recentAct = Act.filterOnStatus(statusName).get()
ADD:
HQL
"select s1.act from Status as s1 \
where s1.name = :statusName \
and s1.setDate = (select max(s0.setDate) from s1.act.status s0)"
NamedQuery
listByStatus { statusName ->
createAlias('status', 's1')
eq 's1.name', statusName
eq 's1.setDate', new DetachedCriteria(Status).build{ projections { max('setDate')} eqProperty('act','s1.act') }
}

GORM Criteria Query: Finding Children with specific properties

Have the following Domain modal:
class TransactionHeader {
static hasMany = [details: TransactionDetail]
}
class TransactionDetail {
static belongsTo = [header: TransactionHeader]
Product product
}
I'm trying to write a criteria query that will return all the TransactionHeader rows that contain TransactionDetails with 2 different Products. This is what I have so far and it isn't doing exactly what I'm after:
def list = TransactionHeader.withCriteria {
details {
and {
eq("product", product1)
eq("product", product2)
}
}
}
What's happening is it is return rows that contain at least 1 detail with 1 of the products. I need rows that have 2 details, each with one of the products.
Feels like you want to move details out of that, so actually
def list = TransactionHeader.withCriteria {
and {
details {
eq("product", product1)
}
details {
eq("product", product2)
}
}
}
Not sure how hibernate / gorm will deal this, though.
Have you tried using the "in" statement in your criteria dsl?
def list = TransactionHeader.withCriteria {
and {
details {
'in'("product", [product1, product2])
}
}
}

Grails GORM 'or' not working with associations

In the following example, I'd expect Product.searchAll to match both
additives and products, but it seems to ignore eq('name', taste).
class Additive {
String flavor
static belongsTo = [product:Product]
}
class Product {
String name
static hasMany = [additives:Additive]
static constraints = {
name nullable:true
}
static namedQueries = {
searchAll { taste ->
or {
eq('name', taste)
additives { eq('flavor', taste) }
}
}
searchAdditives { taste ->
additives { eq('flavor', taste) }
}
searchProducts { taste ->
eq('name', taste)
}
}
}
class SearchSpec extends grails.plugin.spock.IntegrationSpec {
def choc, muff
def 'searchAll should return products and additives that match - THIS FAILS'() {
setup:
createTestProducts()
expect:
Product.searchAll("chocolate").list() == [choc, muff]
}
def 'searchProducts should return only products that match - THIS PASSES'() {
setup:
createTestProducts()
expect:
Product.searchProducts("chocolate").list() == [choc]
}
def 'searchAdditives should return only additives that match - THIS PASSES'() {
setup:
createTestProducts()
expect:
Product.searchAdditives("chocolate").list() == [muff]
}
private def createTestProducts() {
// create chocolate
choc = new Product(name:'chocolate').save(failOnError:true, flush:true)
// create a chocoloate-flavored muffin
muff = new Product(name:'muffin').addToAdditives(flavor:'chocolate').save(failOnError:true, flush:true)
}
}
The SQL generated is as follows:
select this_.id as id1_1_, this_.version as version1_1_,
this_.name as name1_1_, additives_1_.id as id0_0_,
additives_1_.version as version0_0_, additives_1_.flavor as
flavor0_0_, additives_1_.product_id as product4_0_0_ from product
this_ inner join additive additives_1_ on
this_.id=additives_1_.product_id where (this_.name=? or
(additives_1_.flavor=?))
Is there something wrong with my syntax, or is this a problem with Grails, GORM or H2?
My guess, quickly looking at your query, is that Grails / GORM is performing an inner join. An inner join only matches if a relationship exists between the tables. In the example above, that query will never match choc, because choc does not have any associated additives.
So, it's not the or that's failing, it's the actual query. Fire up localhost:8080/{yourapp}/dbConsole and run that same query, but without the where statement. You should see that you only get products with one or more additives.
I believe (not tested) you can force a LEFT JOIN using syntax like this:
import org.hibernate.criterion.CriteriaSpecification
...
searchAll { taste ->
createAlias("additives", "adds", CriteriaSpecification.LEFT_JOIN)
or {
eq('name', taste)
eq('adds.flavor', taste)
}
}
This should force a left (or outer) join, allowing for products that do not have a matching additive. Note: It's possible to get duplicate results when using outer joins, but this depends on your particular usage scenario.

Resources