Upload
ted-vinke
View
2.645
Download
0
Embed Size (px)
DESCRIPTION
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.
Citation preview
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