58
GRAILS GORM Practical basics of how GORM can query for you when SQL is all you know. Dec 2014 - Ted Vinke

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

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

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