Transcript
Page 1: Grails GORM - You Know SQL. You Know Queries. Here's GORM

GRAILS GORMPractical basics of how GORM can query for youwhen SQL is all you know.

Dec 2014 - Ted Vinke

Page 2: Grails GORM - You Know SQL. You Know Queries. Here's GORM

Overview• Introduction

• Querying• Basic GORM

• Dynamic Finders

• Where Queries

• Criteria

• HQL

• Native SQL

• How to GORM your existing SQL?

• Summary

Page 3: Grails GORM - You Know SQL. You Know Queries. Here's GORM

We already know how todo SQL in Grails, right?

Page 4: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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()

}}

}

Page 5: Grails GORM - You Know SQL. You Know Queries. Here's GORM

Is SQL your Grails application’s core business?

Page 6: Grails GORM - You Know SQL. You Know Queries. Here's GORM

In a nutshell

GORM stands for Grails Object Relational Mapping

Grails 2.4.3 uses Hibernate 4 under the hood

Evolves around domain classes

Page 7: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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)

Page 8: Grails GORM - You Know SQL. You Know Queries. Here's GORM

QUERYINGWhat Grails can do for you

Page 9: Grails GORM - You Know SQL. You Know Queries. Here's GORM

SettingsdataSource {

driverClassName = "org.h2.Driver"url = "jdbc:h2:mem:devDb;..."...

}hibernate {

...format_sql = trueuse_sql_comments = true

}

Page 10: Grails GORM - You Know SQL. You Know Queries. Here's GORM

We have a domain class// grails-app/domain/Animal.groovyclass Animal {

String nameint age

static mapping = {id column: "ani_id"version false

}}

Page 11: Grails GORM - You Know SQL. You Know Queries. Here's GORM

And some animalsclass BootStrap {

def init = { servletContext ->

environments {

development {

new Animal(name: "Belle", age: 1).save()

new Animal(name: "Cinnamon", age: 5).save()

}

}

}

}

Page 12: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

• …

Page 13: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

Page 14: Grails GORM - You Know SQL. You Know Queries. Here's GORM

AnimalServiceclass AnimalService {

....

}

Page 15: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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 ?

Page 16: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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 ?

Page 17: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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 ?

Page 18: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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=?

Page 19: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

Page 20: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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!

Page 21: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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=?

Page 22: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

Page 23: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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_

Page 24: Grails GORM - You Know SQL. You Know Queries. Here's GORM

How to test?

Page 25: Grails GORM - You Know SQL. You Know Queries. Here's GORM

Dynamic finders, Where and Criteria queries can be unit tested!

create-unit-test AnimalServicetest/unit/AnimalServiceSpec.groovytest-app –unit AnimalServiceSpec

Page 26: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

Page 27: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

Page 28: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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()

}

Page 29: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

Page 30: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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 ?

Page 31: Grails GORM - You Know SQL. You Know Queries. Here's GORM

How to test?

Page 32: Grails GORM - You Know SQL. You Know Queries. Here's GORM

HQL can be unit tested!

Page 33: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

Page 34: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

Page 35: Grails GORM - You Know SQL. You Know Queries. Here's GORM

HQL can be integration tested!

create-integration-test AnimalServicetest/integration/AnimalServiceSpec.groovytest-app –integration AnimalServiceSpec

Page 36: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

}

Page 37: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

Page 38: Grails GORM - You Know SQL. You Know Queries. Here's GORM

NATIVE SQLWe have a lot of stored procedures and functions in our

Oracle database

Page 39: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

}

Page 40: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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}

Page 41: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

...

Page 42: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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()

}

Page 43: Grails GORM - You Know SQL. You Know Queries. Here's GORM

How to test?

But wait! What to test?

Page 44: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

• ?

Page 45: Grails GORM - You Know SQL. You Know Queries. Here's GORM

So, what if you can dream the SQL,and want to GORM it?

Page 46: Grails GORM - You Know SQL. You Know Queries. Here's GORM

STEP-BY-STEP STRATEGYHow to GORMinize Your SQL

Page 47: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

Page 48: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

Page 49: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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()

}

}

Page 50: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

Page 51: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

}}

Page 52: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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()

}

}

Page 53: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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

Page 54: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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()

}}

Page 55: Grails GORM - You Know SQL. You Know Queries. Here's GORM

SUMMARY

Page 56: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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.

Page 57: Grails GORM - You Know SQL. You Know Queries. Here's GORM

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


Recommended