GRAILS GORMPractical basics of how GORM can query for youwhen SQL is all you know.
Dec 2014 - Ted Vinke
Overview• Introduction
• Querying• Basic GORM
• Dynamic Finders
• Where Queries
• Criteria
• HQL
• Native SQL
• How to GORM your existing SQL?
• Summary
We already know how todo SQL in Grails, right?
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()
}}
}
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 around domain classes
Domain classes?
select * from animals where name = “Belle”
Database likes SQL We – the developers – like to talk in domain terms
Animal.findByName(“Belle”)
select * from animals where id = 5
Animal.get(5)
QUERYINGWhat Grails can do for you
SettingsdataSource {
driverClassName = "org.h2.Driver"url = "jdbc:h2:mem:devDb;..."...
}hibernate {
...format_sql = trueuse_sql_comments = true
}
We have a domain class// grails-app/domain/Animal.groovyclass Animal {
String nameint age
static mapping = {id column: "ani_id"version false
}}
And some animalsclass BootStrap {
def init = { servletContext ->
environments {
development {
new Animal(name: "Belle", age: 1).save()
new Animal(name: "Cinnamon", age: 5).save()
}
}
}
}
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
• …
AnimalController
// grails-app/controllers/AnimalController.groovyclass AnimalController {
def animalService
def show(String name) { def animal = animalService.find...(name)log.info "Found " + animal...
}}
http://localhost:8080/query/animal/show?name=Belle
AnimalServiceclass AnimalService {
....
}
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 ?
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 ?
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 ?
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=?
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 findersselect 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
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 findAllinstead!
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=?
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
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_
How to test?
Dynamic finders, Where and Criteria queries can be unit tested!
create-unit-test AnimalServicetest/unit/AnimalServiceSpec.groovytest-app –unit AnimalServiceSpec
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
Unit test with DomainClassUnitTestMixinuses
in-memory ConcurrentHashMapwhich allows mocking of large part of GORM• Simple persistence methods like list(), save()• Dynamic Finders• Named Queries• Query By Example• GORM Events
HQL
Animal findWithHQL(String animalName) {
Animal.find("from Animal as a where a.name = :name",
["name" : animalName])
}Hibernate Query Language
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 ?
Animal findWithHQL(String animalName) {
Animal.executeQuery("from Animal a where a.name = :name", ["name" : animalName, "max" : 1]).first()
}
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
class Animal {String nameint agestatic 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 nameint agestatic 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 ?
How to test?
HQL can be unit tested!
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 testsAnimal.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
Unit test with HibernateTestMixinuses
in-memory H2 databasewhich 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
HQL can be integration tested!
create-integration-test AnimalServicetest/integration/AnimalServiceSpec.groovytest-app –integration AnimalServiceSpec
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 whatsoeverclass 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
}
Integration testuses
in-memory H2 database by defaultwhich allows testing all of GORM• Each test runs in its own transaction,
which is rolled back at the end of the test
NATIVE SQLWe 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 {
// 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
}
Just a Groovy callA 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 participantIdsql.call("{? = call rxpa_general.par_id(?, ?)}", [Sql.BIGINT, participantCodeType, participantCode]) {
result -> participantId = result }
return participantId}
Sometimes troubleHas 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
...
Old-fashioned CallableStatementString 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()
}
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 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
• ?
So, what if you can dream the SQL,and want to GORM it?
STEP-BY-STEP STRATEGYHow 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 age
• you already know the SQL which does the trick:
select sum(age) from animal
On a high-level1. 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
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()
}
}
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
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 nameint age
static mapping = {id column: "ani_id"version false
}}
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()
}
}
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
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 extendsSpecification {
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 extendsSpecification {
def animalService
void "test calculating the total age"() {given:Animal.build(age: 1).save()Animal.build(age: 5).save()
expect:6 == animalService.calculateTotalAge()
}}
SUMMARY
WhereQueries
Less verbose than criteriaMore 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.
SummaryTry 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 chooseaccordingly. GORM has its strong suits, but native SQL too, e.g. performance tuning or db-
specific SQL
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