I've been trying to add search functionality to my grails project and I'm running into a bit of a snag.
Here's my domain class
class Worker{
String name
}
and here's my controller
package main
class SiteController {
def search()
{
def results = Worker.findAll{
it.name ==~ /.*John.*/
}
[results:results]
}
}
I'm trying to figure out how to use the findAll(closure) function, but I can't find examples anywhere and I can't seam to figure it out via testing either, I just want to find all of the workers by the test criteria I put in the closure.
EDIT 1
I'm having another problem, for some reason whenever I use any special characters in my regex, such as [. * ?] or any of those, my findAll doesn't return anything. If I have a workers whose name is "John Smith" and I do
name ==~ /John Smith/
it works as it should, but if I use any of those special characters such as
name ==~ /John.*/
or even
name ==~ /John S.ith/
it won't work, this is very confusing and the regexs works as they should right outside the findall function too, if you could provide some insight into this that'd be very helpful
The example in the docs show's that you reference the attribute directly:
// Use where criteria (since Grails 2.0)
def results = Person.findAll {
lastName == "Simpson"
}
So applying to your query:
def results = Worker.findAll{
name ==~ /.*John.*/
}
Use LIKE in query instead, Try this:
results = Person.findAllByLastNameLike("%John%")
Grails uses hibernate underneath which uses HQL language which is similar to SQL. Alternatively, you can also run full query
results = Person.findAll("from Person as p where p.lastName LIKE :lastname order by p.lastName",
[lastname: '%John%'])
To look for into HQL queries:
http://docs.jboss.org/hibernate/orm/3.3/reference/en-US/html/queryhql.html
And it is sad but true, HQL/SQL doesn't support regular expressions, it only supports LIKE clause.
Related
I am trying to query a domain that looks like this:
class MyDomain {
String something
String somethingElse
Set someStrings
static hasMany = [someStrings: String]
static mapping = {
//... etc.
someStrings: cascade: 'all'
//... etc.
}
}
The domain is in a dependency I didn't write and can't modify.
I would like to find all MyDomains where the Set someStrings contains, say, 'xyz'.
Please show me how, dynamically, with a criteria, or whatever you consider the best practice, I can do this in Grails. My project and the dependency are using Grails 2.44.
In my opinion, using a Collection as a property in a grails Domain is already an anti-pattern, so asking for a "best practice" on top of that is kind of ironic.
FWIW, here's my attempt.
Grails builds a table for your Set of strings, so you can use a classic SQL query, and bind the results to the domain class, like this:
import myapp.MyDomain
class MyDomainService {
List<MyDomain> findByKeyword(String keyword) {
MyDomain.withSession {session ->
def query = session.createSQLQuery("select distinct main.* from MY_DOMAIN as main inner join MY_DOMAIN_SOME_STRINGS as detail on main.id=detail.MY_DOMAIN_ID where detail.SOME_STRINGS_STRING = :keyword")
query.addEntity(MyDomain.class)
query.setString("keyword", keyword)
query.list()
}
}
}
I could not test the code, so there may be typos. I believe that my table and column names match what grails would generate for your example. In any case the principle of binding a domain class to a resultset works in my code.
UPDATE:
Not sure if it will work, but you could try this:
MyDomain.findAll {
someStrings.contains "xyz"
}
It is theoretically possible within the DSL of where queries, but I haven't tried it. I'd be impressed if they thought about this.
In my application, the query is being built by appending the first part(where clause) with the second part(order by) using a separate script like QueryBuilder.groovy and hence the order by part is prone to HQL injection which can't be sanitized by using Named Parameters. Therefore, I want to use findAll to retrieve a set of records by passing it a query and sorting and paging parameters separately. I saw an implementation like this:
domainClass.findAll(query,[namedParams],[max: 10, offset: 5])
When i passed sortColumn and sortDirection as named parameters, sortColumn worked fine but sortDirection didn't work. i need a way to either make sortDirection work as a named parameter or any other way which will combine 'sorting by direction' with the findAll result. Many people have suggested on various forums to just use the parameters directly as part of the query but it is unacceptable for my application as it will expose the query to HQL Injection.
Thanks in advance.
here is an example:
queryString = "FROM BookCatalog b WHERE b.bookNumber = :bookNumber"
this is passed to the QueryBuilder.groovy where something like this happens:
sort = "$params.sortColumn $params.sortDirection"
queryString.order(sort)
public void sort(String query){
this.query = this.query+" order by "+query
}
finally findAll retrieves the list of records:
def list = findAll(queryString,namedParams,queryParams)
so as the logic just appends the sorting parameters to the query string a potential hacker can do something like this:
bookCatalogView?offset=2&max=5&sortColumn=1,2,3 **or 1=1**
or
bookCatalogView?offset=2&max=5&sortColumn=1,2,3;**select * from whatever**
Don't concat strings, it's bad practice.
If you want to create complex queries, consider using createCriteria()
SomeDomainClass.createCriteria().list {
order("propName", "desc")
}
or, if you need more control, in the sessionFactory way:
query = sessionFactory.getCurrentSession().createCriteria(DomainClass.class)
query.addOrder(Order.asc("someField"))
query.addOrder(Order.desc("someotherField"))
My grails app has the following classes
class Person {
Address address
// other attributes
}
class Address {
String street
City city
// more attributes
}
I would like to query the first 5 people alphabetically by street name. Currently I do something like
def criteria = Person.createCriteria();
def people = criteria.list(max:5) {
address {
order("street","asc")
}
}
This works. I'm just wondering if there's a shorter way to do this (possibly without the criteria builder).
Actually I think this is the clearest and most efficient way of doing it. You could try things like executeQuery, but to be honest, I'm not sure if that's any less verbose. If you're just trying to shorten the code, you could simplify the first two lines:
def people = Person.createCriteria().list(max:5) {
...
My Grails app is using the searchable plugin, which builds on Compass and Lucene to provide search functionality. I have two searchable classes, say Author and Book. I have mapped these classes to the search index, so that only certain fields can be searched.
To perform a search across both classes I simply call
def results = searchableService.search(query)
One of the nice features of doing the search across both class simultaneously, is that the results object includes metadata about number of results included, number of results available, pagination details etc.
I recently added a boolean approved flag to the Book class and I never want unapproved books to appear in the search results. One option is to replace the call above with:
def bookResults = Book.search(query + " approved:1")
def authorResults = Author.search(query)
However, I now need to figure out how to combine the metadata for both results, which is likely to be tricky (particularly pagination).
Is there a way to search across Book and Author with a single query, but only return approved books?
Do you want to be able to find authors or do you want to find books with a given author?
If you want to find books with a given author, you can configure your domain classes in the following way:
class Author {
String name
...
static searchable = {
root false
}
}
this will result in excluding the Author from the searchableService.search(query)-result and you'll find field names like $/Book/Author/name in your index. (use luke to examine your index: http://code.google.com/p/luke/).
You can change the name of those fields by configuring a better prefix in your Book-class:
class Book {
String name
Author author
...
static searchable = {
author component: [prefix: 'author']
}
}
this will change the name of the field in the index to bookauthor.
If you now search with searchableService.search(query), you'll find all books where the name of the book or the name of the author contains the search term. You can even restrict the search to a given author by using the authorname:xyz syntax.
If you really would like to mix the search results, I only know the solution you already mentioned: mixing both results with your own code, but I guess it will be hard to mix the scoring of the hits in a good way.
Update to your response: Here's my pagination code...
.gsp:
<div class="pagination">
<g:paginate total="${resultsTotal}" params="[q: params.q]"/>
</div>
controller:
result = searchableService.search(params.q, params)
[
resultList: result.results,
resultsTotal: result.total
]
So if you just merge the results of your two searches and add the result.totals, this could work for you.
I've created a test app and came to the following solution. maybe it helps...
if the property approved only has the states 0 and 1, the following query will work:
def results = searchableService.search(
"(${query} AND approved:1) OR (${query} -approved:0 -approved:1)"
)
I guess this can be reformulated in a better way if you don't use the QueryParser but the BooleanQueryBuilder.
BTW: if you add a method like
String getType() { "Book" }
and
String getType() { "Author" }
To your domains, you can even configure your search to do it like this
def results = searchableService.search(
"(${query} AND approved:1) OR (${query} AND type:Author)"
)
I have User class which has a field type, which is in turn a list.
So type might look like : ["a","b"]
I have a another list, say search like ["c","b"] (this is pre-defined)
Now what I want is to search all my User instances such that I can find users type matching with any one of the elements from search.
I came up with a partial solution :
def newQ = User.findAllByUsernameIsNotNull()
newQ.each { eachIndex ->
query = eachIndex.type
result = query.findAll { it == "c" }
if(result.size() > 0 )
{
ans << eachIndex.username
}
}
The above code works, ans list have all User which satisfy my condition. But you can clearly see that in query.findAll line, I'm doing a search only for one element from search. I want to perform search operation for all search element against query(which is User's type field).
How can I achieve that? Apart from my solution are there any easy way to do that?
Thanks in advance.
You could do something like:
def ans = []
search.each{s->
ans += User.findAll("from User u where u.username is not null and ? in elements(u.type)",[s])
}
I can't think of a way to do it in a single query
User.withCriteria {
isNotNull("username")
'in' ("type", search)
}
When searching you want to go to the database as few times as possible since those are usually the most expensive operations. If the User.withCriteria {..} works I'd use that (I'm not as familiar with .withCriteria{}). This would work as well if you still wanted to use the dynamic finders since mockDomain doesn't work with HSQL (again not sure if .withCriteria{} works with mockDomain).
def search = ["c", "b"]
def users = User.findAllByUsernameIsNotNull()
users = users.findAll {it.type.intersect(search)}