We have an Shop with ShopArticles and want to add an Rating system.
Our ShopArticle looks like this:
class ShopArticle {
String contUnit = 'STK', orderUnit = 'PCK'
Double value, tax = 0.19
String name, description, keyword
String group1, group2, group3, articleNumber
String producer
Boolean unlocked
static hasMany = [ratings: ShopArticleRating]
}
And the Rating looks like this:
class ShopArticleRating {
String comment
int rating
ShopArticle shopArticle
User user
static belongsTo = ShopArticle
}
Now we want to filter for the average Rating of an atricle, so we made this:
def shopArticleList = ShopArticleRating.createCriteria().listDistinct {
projections {
groupProperty("shopArticle")
}
}
def ids = []
shopArticleList.each { shopArticle ->
def sum = 0
shopArticle.ratings.each {
sum += it.rating
}
if ((sum / shopArticle.ratings.size()) >= filter.rating) {
ids.add(shopArticle.id)
}
}
List<ShopArticle> list = ShopArticle.createCriteria().list {
if (ids.size() > 0) {
'in'("id", ids)
}
}
Is there a better way to filter for the average Rating?
Maybe like this:
List<ShopArticle> list = ShopArticle.createCriteria().list {
createAlias('ratings','r')
projections {
groupProperty('r.rating')
}
gt("r.rating",filter.rating)
}
Were it me, I'd add an averageRating attribute to the ShopArticle class itself.
Do the math to compute the average when a rating is added/deleted/changed for that ShopArticle, and distribute the 'cost' of doing the math across each rating entry/change. Ten people add ratings, you do the math 10 times. A thousand people do queries, you don't do the math 1000 times.
Showing the average rating becomes nothing more than showing another attribute on the screen, filtering is trivial -- no extra work when querying data (and I think it is safe to bet that there will be more queries than ratings added).
Try this:
ShopArticleRating.withCriteria {
projections {
avg("rating")
groupProperty("shopArticle")
}
}
This should give you the article object together with the average of ratings.
I could not test the other way around but it should be like this:
ShopArticle.withCriteria {
projections {
ratings {
avg("rating)
}
property("id")
}
}
which should give you the article id together with the average of the correspondent ratings
another way would be to write your own sql syntax which gives you the absolute freedom to return everything you want. In that case look for "grails HQL". Sometimes I touch the borders when I need to do very complex queries. But in your case you should be good.
Related
I am using grails-2.5.6 version. I am using spring-security-core plugin. I have a criteria query on UserRole table. Where I want to find all distinct users by a role. It is working properly.
But the problem is the pagination effect. When I am counting on the list it is counting on UserRole list object. But I need the count on distinct projection items. Here is my attempt below:
def list(Integer max) {
def userInstanceList = UserRole.createCriteria().list(params) {
createAlias('user', 'au')
createAlias('role', 'ar')
projections {
distinct("user")
}
if (params.roleId) {
eq('ar.id', params.getLong("roleId"))
}
}
def totalCount = userInstanceList.totalCount
[userInstanceList: userInstanceList, totalCount: totalCount]
}
Here, totalCount is the number of UserRole list. But I want the distinct projection count.
I would tackle this slightly differently, you want to analyse the users, not the userroles.
So I'd do something like:
List<User> usersWithRole = UserRole.createCriteria().list(params) {
role {
eq('id', params.getLong("roleId"))
}
}*.user
int count = usersWithRole.size()
Unless of course there's hundreds or thousands of users, in which case I wouldn't want to load all of them each time and would revert to SQL.
Is this a custom version of spring security you're using? I've never seen Roles with a 'long' based ID, usually, the key is a String representing the Authority name.
Usually the DBAs see the use of distinct keyword as a code-smell.
In your case I would rather use the User as the main domain object to run the query against and a group by clause:
long id = params.getLong "roleId"
def list = User.createCriteria().list( params ) {
projections{
groupProperty 'userRole.role.id'
}
if( id )
userRole{
role{
eq 'id', id
}
}
}
Just for background, let us have these three domain classes:
class Group {
Long id
Person person
}
class Person {
Long id
Country country
String name
}
class Country {
Long id
}
So, with these classes in mind, I am given a Group object's id as well as a Country object's id. I would like to get the list of Person objects based on these two.
It seems relatively simple, but I am new to criteria queries and so I am struggling to figure out what I am doing wrong. This is what I have so far:
def c = Group.createCriteria()
def names = c.list (order: "asc") {
createAlias('person', 'p')
createAlias('p.country', 'c')
and {
eq ('c.id', Long.valueOf(countryId))
eq ('id', groupId)
}
projections {
property('p.name')
}
}
Of course, this is wrong as it is throwing errors. Can someone please let me know what I am doing wrong?
Thanks for your help!
static def searchPersons(Map params) {
return Group.createCriteria().list(params, {
if(params.groupId) {
eq('id', params.groupId.toLong())
}
person {
order('name')
if(params.countryId) {
country {
eq('id', params.countryId.toLong())
}
}
}
projections {
property('person')
}
})
}
Still, it might be better to add the necessary associations (hasMany, etc.) on your domains.
The first thing you can do is improve the associations between your domain classes. This will help make criteria queries simpler (and deter monkey-patching later).
In your example, the association between Person an Group is a one-to-many; one person can have many groups. That may be your intention, but it also means that a group can have only one person. Basically, there's no way to group people together.
I'm going to assume that you want a many-to-one relationship so that many Person (people) can be in the same Group. With this in mind, the domain classes (with the explicit IDs left in) would look like this:
class Group {
Long id
}
class Person {
Long id
Country country
String name
Group group
}
class Country {
Long id
}
As for the query, since your expected result is instances of Person, the best place to start is with Person rather than Group.
List of Person instances
Here's how to get a list of Person instances.
Where query
def people = Person.where {
country.id == countryId
group.id == groupId
}.list()
Criteria query
def people = Person.withCriteria {
country {
eq 'id', countryId as Long
}
group {
eq 'id', groupId
}
}
List of Person names
Notice that there's a discrepancy between your question and example. You asked for a list of Person instances, yet your example demonstrates attempting to get a list of Person names .
Here's how to get a list of names of the Person instances.
Where query
def names = Person.where {
country.id == countryId
group.id == groupId
}.projections {
property 'name'
}.list()
Criteria query
def names = Person.withCriteria {
country {
eq 'id', countryId as Long
}
group {
eq 'id', groupId
}
projections {
property 'name'
}
}
I already searched a lot but I didn't found a solution yet. I guess I really need your help. My problem is about filtering with a "special" goal. Let's say we have got a few Persons, who have got many skills. I am able to filter this Persons by name and matching skills. But I need to get these persons, who just have one explizit skill and no other. i.e. persons which can run very fast but nothing more than this :-). But it is possible that a person have many entries of the skill swim. i.e. Peter which has got three skills: [swim, swim, swim] should be in the result too. An example is following. I am really looking forward to hear some tips. Thank you very much.
class Person {
String name;
static hasMany = [skills: Skill]
}
class Skill {
Double skillLevel;
SkillType skillName;
}
class ControllerClass {
def filterMethod() {
def personCriteria = Person.createCriteria()
results = personCriteria.list(params) {
and {
eq("name", "Peter")
//I can do this to get Person, who have the skill to swim
skills {
eq("skillName", "swim")
}
//Problem: this Person might have other skills, too. i.e. "run fast", "jump high", "Math"
//My Goal is: I want only these persons, who only have the skill "swim" and no other skill, but the item swim can appear more than once
}
}
}
}
In my scenario I know all different types of skills, so they are limited. This would make it easier wouldn't it? Let's pretend there are three skills possible: swim, run, sing. But they can appear more than once. Why does something like this doesn't work?:
and {
and {
sizeGe("skills", 1)
skills{
eq("skillName", "swim")
}
}
and {
sizeEq("skills", 0)
skills {
eq("skillName", "run")
}
}
and {
sizeEq("subgruppen", 0)
skills {
eq("skillName", "sing")
}
}
}
You are on the right path - sizeEq should do what you whant.
You just used it the wrong way. As described in the docs, you could check the size of a collection.
Try this:
def results = personCriteria.list(params) {
and {
sizeEq("skills", 1)
skills {
eq("skillName", "swim")
}
}
}
This should get you all person who have exactly one skill with the name 'swim'.
I've got color and shade domains in my grails application.
class Color {
static hasMany = [shades: Shade]
}
class Shade {
static belongsTo = [color: Color]
}
Using criteria, how can I get a list of Colors where there are X shades? where X is a number that I can pass in.
I know that this returns entire list of colors:
def list = Color.createCriteria.listDistinct {
shade {
count()
}
}
but I don't know how to get list where shade count is specific. I tried this but it didnt work.
def list = Color.createCriteria.listDistinct {
shade {
count() == 5
}
}
Looking at the docs the sizeEq seems to be the the criteria method which fits your needs:
def list = Color.createCriteria().listDistinct {
sizeEq("shades", 5)
}
I'm relatively new to Grails.
I have the following
class House {
Integer number
Integer maxResidents
static belongsTo = [town: Town]
}
class Town {
String name
static hasMany = [houses: House]
}
I want to get five towns with most Houses. I have seen the possibility to create a criteria but I can't deal with it now. Can someone support?
Thank you!
As you have a bidirectional association you can do this with a query on House:
def result = House.withCriteria {
projections {
groupProperty("town", "town")
rowCount("numHouses")
}
order("numHouses", "desc")
maxResults(5)
}
This would return you a list of results where each result res has the town as res[0] and the number of houses as res[1]. If you'd prefer each result to be a map giving access to res.town and res.numHouses then you should add
resultTransformer(AliasToEntityMapResultTransformer.INSTANCE)
after the maxResults line (along with the appropriate import at the top of your file).