Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
GRAILS GORM 
Practical basics of how GORM can query for you 
when SQL is all you know. 
Dec 2014 - Ted Vinke
Overview 
• Introduction 
• Querying 
• Basic GORM 
• Dynamic Finders 
• Where Queries 
• Criteria 
• HQL 
• Native SQL 
•...
We already know how to 
do SQL in Grails, right?
Just use Groovy! (jeej) 
class AnimalService { 
def dataSource 
def findWithGroovySql(String animalName) { 
def sql = new ...
Is SQL your Grails application’s core 
business?
In a nutshell 
GORM stands for Grails Object Relational Mapping 
Grails 2.4.3 uses Hibernate 4 under the hood 
Evolves aro...
Domain classes? 
Database likes SQL We – the developers – like 
select * from animals 
where name = “Belle” 
to talk in do...
QUERYING 
What Grails can do for you
Settings 
dataSource { 
driverClassName = "org.h2.Driver" 
url = "jdbc:h2:mem:devDb;..." 
... 
} 
hibernate { 
... 
format...
We have a domain class 
// grails-app/domain/Animal.groovy 
class Animal { 
String name 
int age 
static mapping = { 
id c...
And some animals 
class BootStrap { 
def init = { servletContext -> 
environments { 
development { 
new Animal(name: "Bell...
Basic GORM 
def animal = Animal.get(1) 
select animal0_.id as id1_0_0_, animal0_.version as version2_0_0_, 
animal0_.age a...
AnimalController 
// grails-app/controllers/AnimalController.groovy 
class AnimalController { 
def animalService 
def show...
AnimalService 
class AnimalService { 
.... 
}
Dynamic 
finders 
Animal findWithDynamicFinder(String animalName) { 
Animal.findByName(animalName) 
} 
• findBy, countBy 
...
Dynamic 
finders 
Animal findWithDynamicFinder(String animalName) { 
Animal.findByNameAndAgeLessThan(animalName, 3) 
} 
• ...
Dynamic 
finders 
Animal findWithDynamicFinder(String animalName) { 
Animal.findByName(animalName, [sort: "age"]) 
} 
• fi...
Where 
Animal findWithWhereQuery(String animalName) { 
def query = Animal.where { 
name == animalName 
} 
return query.fin...
Where 
Animal findWithWhereQuery(String animalName) { 
def query = Animal.where { 
name == animalName && (age < 3) 
} 
ret...
Where 
Animal findWithWhereQuery(String animalName) { 
def query = Animal.where { 
name == animalName && (age < 3) 
} 
ret...
Criteria 
Animal findWithCriteria(String animalName) { 
// Criteria 
def c = Animal.createCriteria() 
return c.get { 
eq "...
Criteria 
Animal findWithCriteria(String animalName) { 
// Criteria 
def c = Animal.createCriteria() 
return c.get { 
eq "...
Criteria and 
projections 
Long calculateTotalAge() { 
// Criteria 
def c = Animal.createCriteria() 
return c.get { 
proje...
How to test?
Dynamic finders, Where and Criteria queries 
can be unit tested! 
create-unit-test AnimalService 
test/unit/AnimalServiceS...
AnimalServiceSpec 
• Uses DomainClassUnitTestMixin, simple in-memory ConcurrentHashMap 
@Mock(Animal) 
@TestFor(AnimalServ...
Unit test with DomainClassUnitTestMixin 
uses 
in-memory ConcurrentHashMap 
which allows mocking of large part of GORM 
• ...
HQL 
Animal findWithHQL(String animalName) { 
Animal.find("from Animal as a where a.name = :name", 
["name" : animalName])...
HQL 
HQL almost looks like SQL… 
Animal.executeQuery("select distinct a.name 
from Animal a order by a.name") 
select dist...
class Animal { 
String name 
int age 
static mapping = { 
name column: "ani_name" 
age column: "ani_age" 
} 
} 
select ani...
How to test?
HQL can be unit tested!
AnimalServiceHibernateSpec 
• Uses HibernateTestMixin, Hibernate 4 and in-memory H2 
@Domain(Animal) 
@TestFor(AnimalServi...
Unit test with HibernateTestMixin 
uses 
in-memory H2 database 
which allows testing all of GORM, including 
• String-base...
HQL can be integration tested! 
create-integration-test AnimalService 
test/integration/AnimalServiceSpec.groovy 
test-app...
AnimalServiceIntegrationSpec 
 Full Grails container is started in test-environment 
 Uses H2 in-memory database. Each t...
Integration test 
uses 
in-memory H2 database by default 
which allows testing all of GORM 
• Each test runs in its own tr...
NATIVE SQL 
We have a lot of stored procedures and functions in our 
Oracle database
Groovy SQL 
and Hibernate 
Nastive SQL 
def findWithGroovySql(String animalName) { 
def sql = new Sql(dataSource) 
try { 
...
Just a Groovy call 
A breeze with Groovy SQL - no need to register all kinds of parameters. Perform a direct call with all...
Sometimes trouble 
Has a lot of input parameters, some optional. This might sometimes cause some ORA issues when using the...
Old-fashioned CallableStatement 
String messageCode 
try { 
Connection c = sql.createConnection() 
CallableStatement cs = ...
How to test? 
But wait! What to test?
From a Grails perspective 
• I’m not interested in the workings of the existing db 
procedures/functions – those should ha...
So, what if you can dream the SQL, 
and want to GORM it?
STEP-BY-STEP STRATEGY 
How to GORMinize Your SQL
Step-by-step 
• Let’s say, 
• you have an existing database with animals 
• in Grails you needs to show their accumulated ...
On a high-level 
1. have a method execute your SQL directly by Groovy 
SQL or Hibernate Native SQL 
• have a Grails test v...
1. Create an integration test 
• create-integration-test AnimalServiceIntegration 
• create a skeleton test and discover 
...
1. Create an integration test 
• create-integration-test AnimalServiceIntegration 
• create a skeleton test and discover w...
2. Create domain class(es) 
• Either create a domain class from scratch, or 
• use the Grails Database Reverse Engineering...
2. Implement with Groovy SQL 
• Implement your business method taking the original query. See the various Groovy SQL examp...
3. Refactor into GORM 
• Now that you have (enough) test coverage, you can safely refactor into a version which doesn’t us...
A few tips 
• Use the Grails Build Test Data plugin to refactor your tests to only include the relevant test data, and 
st...
SUMMARY
Where 
Queries 
Less verbose than criteria 
More flexible than Dynamic Finders 
Dynamic 
Finders 
Simple queries with few ...
Summary 
Try simplest possible query option first: easy to write, read & test 
GORM 
• allows you to think in domains ra...
More info 
• GORM 
• Querying with GORM 
• Dynamic Finders 
• Where Queries 
• Criteria 
• HQL 
• Groovy SQL 
• groovy.sql...
Upcoming SlideShare
Loading in …5
×

Grails GORM - You Know SQL. You Know Queries. Here's GORM.

8,679 views

Published on

This presentation shows practical basics of how Grails Object Relational Mapping (GORM) can help you query data, test it, and think in domain terms along the way when SQL at the moment is all you know.

Published in: Technology

Grails GORM - You Know SQL. You Know Queries. Here's GORM.

  1. 1. GRAILS GORM Practical basics of how GORM can query for you when SQL is all you know. Dec 2014 - Ted Vinke
  2. 2. Overview • Introduction • Querying • Basic GORM • Dynamic Finders • Where Queries • Criteria • HQL • Native SQL • How to GORM your existing SQL? • Summary
  3. 3. We already know how to do SQL in Grails, right?
  4. 4. Just use Groovy! (jeej) class AnimalService { def dataSource def findWithGroovySql(String animalName) { def sql = new Sql(dataSource) try { return sql.rows("select * from animal where name = ?", [animalName]) } finally { sql.close() } } }
  5. 5. Is SQL your Grails application’s core business?
  6. 6. In a nutshell GORM stands for Grails Object Relational Mapping Grails 2.4.3 uses Hibernate 4 under the hood Evolves around domain classes
  7. 7. Domain classes? Database likes SQL We – the developers – like select * from animals where name = “Belle” to talk in domain terms Animal.findByName(“Belle”) select * from animals where id = 5 Animal.get(5)
  8. 8. QUERYING What Grails can do for you
  9. 9. Settings dataSource { driverClassName = "org.h2.Driver" url = "jdbc:h2:mem:devDb;..." ... } hibernate { ... format_sql = true use_sql_comments = true }
  10. 10. We have a domain class // grails-app/domain/Animal.groovy class Animal { String name int age static mapping = { id column: "ani_id" version false } }
  11. 11. And some animals class BootStrap { def init = { servletContext -> environments { development { new Animal(name: "Belle", age: 1).save() new Animal(name: "Cinnamon", age: 5).save() } } } }
  12. 12. Basic GORM def animal = Animal.get(1) select animal0_.id as id1_0_0_, animal0_.version as version2_0_0_, animal0_.age as age3_0_0_, animal0_.name as name4_0_0_ from animal animal0_ where animal0_.id=? Hibernate uses unique table and column aliases, e.g. alias(column name)_(column unique integer)_(table unique integer)_(some suffix) int total = Animal.count() select count(*) as y0_ from animal this_ new Animal(name: "Belle", age: 1).save() insert into animal (id, version, age, name) values (null, ?, ?, ?) def animal = Animal.first(sort: "age") select ... from animal this_ order by this_.age asc limit ? • addTo • count • countBy • delete • exists • first • get • getAll • indent • last • list • listOrderBy • …
  13. 13. AnimalController // grails-app/controllers/AnimalController.groovy class AnimalController { def animalService def show(String name) { def animal = animalService.find...(name) log.info "Found " + animal ... } } http://localhost:8080/query/animal/show?name=Belle
  14. 14. AnimalService class AnimalService { .... }
  15. 15. Dynamic finders Animal findWithDynamicFinder(String animalName) { Animal.findByName(animalName) } • findBy, countBy • findAllBy select this_.id as id1_0_0_, this_.version as version2_0_0_, this_.age as age3_0_0_, this_.name as name4_0_0_ from animal this_ where this_.name=? limit ?
  16. 16. Dynamic finders Animal findWithDynamicFinder(String animalName) { Animal.findByNameAndAgeLessThan(animalName, 3) } • findBy, countBy • findAllBy  Combines properties with all kinds of operators • LessThan • LessThanEquals • Between • Like • Not • Or select this_.id as id1_0_0_, this_.version as version2_0_0_, this_.age as age3_0_0_, this_.name as name4_0_0_ from animal this_ where this_.name=? and this_.age<? limit ?
  17. 17. Dynamic finders Animal findWithDynamicFinder(String animalName) { Animal.findByName(animalName, [sort: "age"]) } • findBy, countBy • findAllBy  Combines properties with all kinds of operators • LessThan • LessThanEquals • Between • Like • Not • Or  Pagination (sort, max, etc) and meta params (readOnly, timeout, etc.) select this_.id as id1_0_0_, this_.version as version2_0_0_, this_.age as age3_0_0_, this_.name as name4_0_0_ from animal this_ where this_.name=? order by this_.age asc limit ?
  18. 18. Where Animal findWithWhereQuery(String animalName) { def query = Animal.where { name == animalName } return query.find() } Defines a new grails.gorm.DetachedCriteria select this_.id as id1_0_0_, this_.version as version2_0_0_, this_.age as age3_0_0_, this_.name as name4_0_0_ from animal this_ where this_.name=?
  19. 19. Where Animal findWithWhereQuery(String animalName) { def query = Animal.where { name == animalName && (age < 3) } return query.find([sort: "age"]) } Defines a new grails.gorm.DetachedCriteria  Enhanced, compile-time checked query DSL.  More flexible than dynamic finders select this_.id as id1_0_0_, this_.version as version2_0_0_, this_.age as age3_0_0_, this_.name as name4_0_0_ from animal this_ where (this_.name=? and this_.age<?) order by this_.age asc
  20. 20. Where Animal findWithWhereQuery(String animalName) { def query = Animal.where { name == animalName && (age < 3) } return query.find() } The DetachedCriteria defined for where can also be used for find Animal findWithWhereQuery(String animalName) { Animal.find { name == animalName && (age < 3) } } Tip! If your query can return multiple rows, use findAll instead!
  21. 21. Criteria Animal findWithCriteria(String animalName) { // Criteria def c = Animal.createCriteria() return c.get { eq "name", animalName } }  Type-safe Groovy way of building criteria queries select this_.id as id1_0_0_, this_.version as version2_0_0_, this_.age as age3_0_0_, this_.name as name4_0_0_ from animal this_ where this_.name=?
  22. 22. Criteria Animal findWithCriteria(String animalName) { // Criteria def c = Animal.createCriteria() return c.get { eq "name", animalName lt "age", 3 order "age", "desc" } }  Type-safe Groovy way of building criteria queries • c.list • c.get • c.scroll • c.listDinstinct select this_.id as id1_0_0_, this_.version as version2_0_0_, this_.age as age3_0_0_, this_.name as name4_0_0_ from animal this_ where this_.name=? and this_.age<? order by this_.age desc
  23. 23. Criteria and projections Long calculateTotalAge() { // Criteria def c = Animal.createCriteria() return c.get { projections { sum("age") } } }  Projections change the nature of the results select sum(this_.age) as y0_ from animal this_
  24. 24. How to test?
  25. 25. Dynamic finders, Where and Criteria queries can be unit tested! create-unit-test AnimalService test/unit/AnimalServiceSpec.groovy test-app –unit AnimalServiceSpec
  26. 26. AnimalServiceSpec • Uses DomainClassUnitTestMixin, simple in-memory ConcurrentHashMap @Mock(Animal) @TestFor(AnimalService) class AnimalServiceSpec extends Specification { void "test finding animals with various queries"() { given: new Animal(name: "Belle", age: 1).save() new Animal(name: "Cinnamon", age: 5).save() expect: "Belle" == service.findWithDynamicFinder("Belle").name "Belle" == service.findWithWhereQuery("Belle").name "Belle" == service.findWithCriteria("Belle").name } } Just @Mock the domain class and insert and verify your test data through the GORM API
  27. 27. Unit test with DomainClassUnitTestMixin uses in-memory ConcurrentHashMap which allows mocking of large part of GORM • Simple persistence methods like list(), save() • Dynamic Finders • Named Queries • Query By Example • GORM Events
  28. 28. HQL Animal findWithHQL(String animalName) { Animal.find("from Animal as a where a.name = :name", ["name" : animalName]) } Hibernate Query Language Animal findWithHQL(String animalName) { Animal.executeQuery("from Animal a where a.name = :name", ["name" : animalName, "max" : 1]).first() } select animal0_.id as id1_0_, animal0_.version as version2_0_, animal0_.age as age3_0_, animal0_.name as name4_0_ from animal animal0_ where animal0_.name=? limit ?
  29. 29. HQL HQL almost looks like SQL… Animal.executeQuery("select distinct a.name from Animal a order by a.name") select distinct animal0_.name as col_0_0_ from animal animal0_ order by animal0_.name
  30. 30. class Animal { String name int age static mapping = { name column: "ani_name" age column: "ani_age" } } select animal0_.id as id1_0_, animal0_.version as version2_0_, animal0_.ani_age as ani_age3_0_, animal0_.ani_name as ani_name4_0_ from animal animal0_ where animal0_.ani_name=? limit ? HQL …but uses domain classes and properties instead of tables and columns Animal.executeQuery("select distinct a.name from Animal a order by a.name") class Animal { String name int age static mapping = {} } select animal0_.id as id1_0_, animal0_.version as version2_0_, animal0_.age as age3_0_, animal0_.name as name4_0_ from animal animal0_ where animal0_.name=? limit ?
  31. 31. How to test?
  32. 32. HQL can be unit tested!
  33. 33. AnimalServiceHibernateSpec • Uses HibernateTestMixin, Hibernate 4 and in-memory H2 @Domain(Animal) @TestFor(AnimalService) @TestMixin(HibernateTestMixin) class AnimalServiceHibernateSpec extends Specification { def cleanup() { // unit test does not clear db between tests Animal.list()*.delete(flush: true) } void "test finding animal with HQL"() { given: new Animal(name: "Belle").save() new Animal(name: "Cinnamon").save() expect: "Belle" == service.findWithHQL("Belle").name
  34. 34. Unit test with HibernateTestMixin uses in-memory H2 database which allows testing all of GORM, including • String-based HQL queries • composite identifiers • dirty checking methods • other direct interaction with Hibernate • Hibernate needs to know about all domain classes, more than you would like to annotate with @Domain, so the Hibernate mixin is not really useful in practice
  35. 35. HQL can be integration tested! create-integration-test AnimalService test/integration/AnimalServiceSpec.groovy test-app –integration AnimalServiceSpec
  36. 36. AnimalServiceIntegrationSpec  Full Grails container is started in test-environment  Uses H2 in-memory database. Each test runs in transaction, which is rolled back at end of the test // no annotations whatsoever class AnimalServiceIntegrationSpec extends Specification { def animalService void "test finding animal with HQL"() { given: new Animal(name: "Belle").save() new Animal(name: "Cinnamon").save() expect: "Belle" == animalService.findWithHQL("Belle").name }
  37. 37. Integration test uses in-memory H2 database by default which allows testing all of GORM • Each test runs in its own transaction, which is rolled back at the end of the test
  38. 38. NATIVE SQL We have a lot of stored procedures and functions in our Oracle database
  39. 39. Groovy SQL and Hibernate Nastive SQL def findWithGroovySql(String animalName) { def sql = new Sql(dataSource) try { // returns rows of resultset // e.g. [[ID:1, VERSION:0, AGE:1, NAME:Belle]] String query = "select * from animal where name = ? limit 1" return sql.rows(query, [animalName]) } finally { sql.close() } } def findWithHibernateNativeSql(String animalName) { def session = sessionFactory.currentSession def query = session.createSQLQuery("select * from animal where name = :name limit 1") List results = query.with { // map columns to keys resultTransformer = AliasToEntityMapResultTransformer.INSTANCE setString("name", animalName) list() } // results are [[AGE:1, VERSION:0, ID:1, NAME:Belle]] results }
  40. 40. Just a Groovy call A breeze with Groovy SQL - no need to register all kinds of parameters. Perform a direct call with all the parameters. The closure is called once. Some more examples here. FUNCTION par_id ( i_participant_code_type IN participant.participant_code_type%TYPE , i_participant_code IN participant.participant_code%TYPE ) RETURN participant.par_id%TYPE IS return_waarde participant.par_id%TYPE := NULL; Long getParticipantId(String participantCodeType, String participantCode) { Sql sql = new groovy.sql.Sql(dataSource) Long participantId sql.call("{? = call rxpa_general.par_id(?, ?)}", [Sql.BIGINT, participantCodeType, participantCode]) { result -> participantId = result } return participantId }
  41. 41. Sometimes trouble Has a lot of input parameters, some optional. This might sometimes cause some ORA issues when using the direct call method…. PROCEDURE set_dry_off_date ( o_message_code OUT VARCHAR2 , i_ani_id IN lactation_period.ani_id_cow%TYPE , i_lactation_end_date IN lactation_period.lactation_end_date%TYPE , i_jou_id IN oxop_general.t_jou_id%TYPE DEFAULT NULL , i_par_id_last_change IN lactation_period.par_id_last_change%TYPE , i_prc_code_last_change IN lactation_period.prc_code_last_change%TYPE ) IS ...
  42. 42. Old-fashioned CallableStatement String messageCode try { Connection c = sql.createConnection() CallableStatement cs = c.prepareCall("{call axmi_general_mut.set_dry_off_date(?,?,?,?,?,?)}"); cs.setLong('i_ani_id', aniId) cs.setDate('i_lactation_end_date', new java.sql.Date(lactationEndDate.time)) if (journalId) { cs.setLong('i_jou_id', journalId) } else { cs.setNull('i_jou_id', Types.NUMERIC) } cs.setLong('i_par_id_last_change', parIdLastChange) cs.setString('i_prc_code_last_change', prcCodeLastChange) cs.registerOutParameter("o_message_code", Types.VARCHAR) cs.execute() messageCode = cs.getString("o_message_code") } catch (java.sql.SQLException e) { throw new RuntimeException("axmi_general_mut.set_dry_off_date failed", e) } finally { sql.close() }
  43. 43. How to test? But wait! What to test?
  44. 44. From a Grails perspective • I’m not interested in the workings of the existing db procedures/functions – those should have their own tests • I want to test that my service is 1. calling them properly 2. return any results properly • Unfortunately, there’s currently no easy way to do that • Options for CI: • Docker container with Oracle XE • ?
  45. 45. So, what if you can dream the SQL, and want to GORM it?
  46. 46. STEP-BY-STEP STRATEGY How to GORMinize Your SQL
  47. 47. Step-by-step • Let’s say, • you have an existing database with animals • in Grails you needs to show their accumulated age • you already know the SQL which does the trick: select sum(age) from animal
  48. 48. On a high-level 1. have a method execute your SQL directly by Groovy SQL or Hibernate Native SQL • have a Grails test verify the logic in that original form • in order to do that, create domain class(es) to init test data 2. refactor the Groovy SQL into GORM • your test informs you behaviour is still correct
  49. 49. 1. Create an integration test • create-integration-test AnimalServiceIntegration • create a skeleton test and discover class AnimalServiceIntegrationSpec extends Specification { def animalService void "test calculating the total age"() { given: new Animal(name: "Belle", age: 1).save() new Animal(name: "Cinnamon", age: 5).save() expect: 6 == animalService.calculateTotalAgeWithGroovySql() 6 == animalService.calculateTotalAge() } }
  50. 50. 1. Create an integration test • create-integration-test AnimalServiceIntegration • create a skeleton test and discover what you need... class AnimalServiceIntegrationSpec extends Specification { def animalService void "test calculating the total age"() { given: // some animals with each an age expect: // call calculation method, verify total age } } Some Animal domain classes for test data The actual business method 
  51. 51. 2. Create domain class(es) • Either create a domain class from scratch, or • use the Grails Database Reverse Engineering Plugin • use them to initialize your test with class AnimalServiceIntegrationSpec extends Specification { def animalService void "test calculating the total age"() { given: new Animal(name: "Belle", age: 1).save() new Animal(name: "Cinnamon", age: 5).save() expect: // call calculation method, verify total age } } class Animal { String name int age static mapping = { id column: "ani_id" version false } }
  52. 52. 2. Implement with Groovy SQL • Implement your business method taking the original query. See the various Groovy SQL examples Long calculateTotalAgeWithGroovySql() { new Sql(dataSource).firstRow("select sum(age) as total from animal").total } • Invoke it from the test. Verify with enough testcases that it does what it’s supposed to do. void "test calculating the total age"() { given: new Animal(name: "Belle", age: 1).save() new Animal(name: "Cinnamon", age: 5).save() expect: 6 == animalService.calculateTotalAgeWithGroovySql() } }
  53. 53. 3. Refactor into GORM • Now that you have (enough) test coverage, you can safely refactor into a version which doesn’t use Groovy SQL, but GORM or Hibernate features instead Long calculateTotalAgeWithGroovySql() { new Sql(dataSource).firstRow("select sum(age) as total from animal").total } • can become e.g. Long calculateTotalAge() { Animal.executeQuery("select sum(age) as total from animal").total } • and verify with your tests everything still works as expected
  54. 54. A few tips • Use the Grails Build Test Data plugin to refactor your tests to only include the relevant test data, and still have valid domain classes class AnimalServiceIntegrationSpec extends Specification { def animalService void "test calculating the total age"() { given: new Animal(name: "Belle", age: 1).save() new Animal(name: "Cinnamon", age: 5).save() expect: 6 == animalService.calculateTotalAge() } } @Build(Animal) class AnimalServiceIntegrationSpec extends Specification { def animalService void "test calculating the total age"() { given: Animal.build(age: 1).save() Animal.build(age: 5).save() expect: 6 == animalService.calculateTotalAge() } }
  55. 55. SUMMARY
  56. 56. Where Queries Less verbose than criteria More flexible than Dynamic Finders Dynamic Finders Simple queries with few properties Criteria Hibernate projections & restrictions Hibernate HQL Fully object-oriented SQL Hibernate Native SQL Native SQL through Hibernate Groovy SQL Native SQL through Groovy My opinionated ranking of query options, when considering readability, writability and testability for 90% of my use cases.
  57. 57. Summary Try simplest possible query option first: easy to write, read & test GORM • allows you to think in domains rather than SQL • is easy to test with unit and integration tests • gives you more we haven’t covered yet: caching, named queries, GORM events, etc. Know the pros and cons of your SQL approach and choose accordingly. GORM has its strong suits, but native SQL too, e.g. performance tuning or db-specific SQL
  58. 58. More info • GORM • Querying with GORM • Dynamic Finders • Where Queries • Criteria • HQL • Groovy SQL • groovy.sql.Sql • database features • Hibernate Query Language (HQL) • Further reading: • How and When to Use Various GORM Querying Options • Recipes for using GORM with Grails

×