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.
Related
I have a dummy cafeteria project wherein I have three domain classes: User, Product and Transaction.
Here's how the classes are defined:
class User {
String name
int employeeId
long balance = 800
static constraints = {
balance(max: 800L)
}
}
class Product {
String type
int quantityInStock
float price
static constraints = {
}
}
class Transaction {
int quantityBought
static belongsTo = [user: User, product: Product]
static constraints = {
}
}
Now I want to find out the number of/list of users who have bought more than, say 2 products.
How do I do it using Grails createCriteria?
This is what I tried:
Transaction.createCriteria().list(){
projections {
groupProperty('user')
'product' {
count('id','numberOfPurchases')
}
}
gt('numberOfPurchases',2)
}
But it gives the following error:
Stacktrace:
org.hibernate.QueryException: could not resolve property: numberOfPurchases of: cafeteria.dummy.Transaction
at grails.orm.HibernateCriteriaBuilder.invokeMethod(HibernateCriteriaBuilder.java:1618)
at Script1.run(Script1.groovy:4)
at org.grails.plugins.console.ConsoleService.eval(ConsoleService.groovy:37)
at org.grails.plugins.console.ConsoleController.execute(ConsoleController.groovy:59)
at grails.plugin.cache.web.filter.PageFragmentCachingFilter.doFilter(PageFragmentCachingFilter.java:195)
at grails.plugin.cache.web.filter.AbstractFilter.doFilter(AbstractFilter.java:63)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:745)
How can I access this alias numberOfPurchases outside the projections block so as to use it in gt?
Internally GORM uses Hibernate and currently Hibernate doesn't have support for having clause in Criteria API. The reason you are not able to get numberOfPurchases outside projections block is that the restrictions are added to where clause and you can't use an aggregation alias in where clause.
Although JPA has support for having clause.
But if you still want to achieve this using GORM Criteria you can. But you would have to modify your query a little bit and it would not involve use of having clause and will require a subquery.
Modified query will be:
SELECT DISTINCT this_.user_id FROM transaction this_ where 2 < (SELECT count(sub_.id) FROM transaction sub_ WHERE sub_.user_id=this_.user_id)
And GORM for it would be:
import org.hibernate.criterion.DetachedCriteria
import org.hibernate.criterion.Projections
import org.hibernate.criterion.Restrictions
import org.hibernate.criterion.Subqueries
DetachedCriteria subQuery = DetachedCriteria.forClass(Transaction, 'sub').with {
setProjection Projections.count('sub.id')
add Restrictions.eqProperty('sub.user', 'this.user')
}
List<User> users = Transaction.createCriteria().list() {
projections {
distinct("user")
}
add Subqueries.lt(2l, subQuery)
}
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.
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")
}
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')
}
}
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])
}
}
}