アレについて記す

Spring Boot で GORM を使う(Stand Alone Application)

Posted on July 28, 2014 at 00:43 (JST)

Spring Boot の勉強会を前に、ちょっと遊んでみました。
GrailsのGORMがStand Aloneで使えるので、DBアクセスにはGORMを使用しています。
テストを動かすところまでのサンプルです。

サンプルはGithubにて公開しています。[ spboot-gorm-standalone ]

1. Gradleの設定

[ build.groovy ]

apply plugin: 'java'
apply plugin: 'groovy'
apply plugin: 'spring-boot'


sourceCompatibility = '1.8'
targetCompatibility = '1.8'

mainClassName = "${'com.example.myproject.Application'}"

buildscript {
    repositories {
        mavenCentral()
        maven { url "http://repo.spring.io/snapshot" }
        maven { url "http://repo.spring.io/milestone" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.0.BUILD-SNAPSHOT")
    }
}

jar {
    baseName = 'myproject'
    version =  '0.0.1-SNAPSHOT'
}

repositories {
    mavenCentral()
    maven { url "http://repo.spring.io/snapshot" }
    maven { url "http://repo.spring.io/milestone" }
    mavenLocal()
    maven { url 'http://repo.grails.org/grails/core/' }
}

def groovyVersion = '2.3.4'
def grailsVersion = '2.3.11'
def gormVersion   = '1.3.7'
def h2Version     = '1.3.170'
def spockVersion  = '0.7-groovy-2.0'

dependencies {
    compile("org.springframework.boot:spring-boot-starter",
            "org.springframework.boot:spring-boot-starter-logging",
            "org.codehaus.groovy:groovy-all:<span>$</span>{groovyVersion}",
            "org.grails:grails-gorm:<span>$</span>{gormVersion}",
            "org.grails:grails-bootstrap:<span>$</span>{grailsVersion}",
            "org.grails:grails-spring:<span>$</span>{grailsVersion}",
            "com.h2database:h2:<span>$</span>{h2Version}")

    testCompile("org.springframework.boot:spring-boot-starter-test",
                "org.spockframework:spock-core:<span>$</span>{spockVersion}")
}

2. GORMを使用するための設定

DB接続先などもこのファイルに定義します

[ SpringBeans.groovy ]

import org.apache.commons.dbcp.BasicDataSource
import org.codehaus.groovy.grails.orm.hibernate.events.PatchedDefaultFlushEventListener
import org.codehaus.groovy.grails.orm.hibernate.support.ClosureEventTriggeringInterceptor
import org.springframework.context.support.ResourceBundleMessageSource

beans {
    xmlns gorm:"http://grails.org/schema/gorm"
    xmlns context:"http://www.springframework.org/schema/context"
    xmlns tx:"http://www.springframework.org/schema/tx"

    context."annotation-config"()
    tx."annotation-driven"()

    messageSource(ResourceBundleMessageSource) {
        basename = "messages"
    }

    dataSource(BasicDataSource) {
        driverClassName = "org.h2.Driver"
        url = "jdbc:h2:mem:test"
        username = "sa"
        password = ""
    }

    gorm.sessionFactory("data-source-ref": "dataSource",
            "base-package": "com.example.myproject",
            "message-source-ref": "messageSource") {

        hibernateProperties = ["hibernate.hbm2ddl.auto": "create",
                               "hibernate.dialect": "org.hibernate.dialect.H2Dialect"]

        eventListeners = ["flush": new PatchedDefaultFlushEventListener(),
                          "pre-load": new ClosureEventTriggeringInterceptor(),
                          "post-load": new ClosureEventTriggeringInterceptor(),
                          "save": new ClosureEventTriggeringInterceptor(),
                          "save-update": new ClosureEventTriggeringInterceptor(),
                          "post-insert": new ClosureEventTriggeringInterceptor(),
                          "pre-update": new ClosureEventTriggeringInterceptor(),
                          "post-update": new ClosureEventTriggeringInterceptor(),
                          "pre-delete": new ClosureEventTriggeringInterceptor(),
                          "post-delete": new ClosureEventTriggeringInterceptor()]
    }

    context."component-scan"("base-package": "com.example.myproject")
}

3. アプリケーション作成

まずはdomainクラスから。
基本的にはGrailsと同様でOKです。

[ Person.groovy ]

package com.example.myproject.domain
import grails.persistence.Entity
import org.apache.commons.lang.builder.ToStringBuilder
import org.apache.commons.lang.builder.ToStringStyle  
@Entity
class Person {
    String familyName
    String givenName
    Date dateCreated
    Date lastUpdated  
    static constraints = {
        familyName maxSize: 10, blank: false
        givenName maxSize: 10, blank: false
        dateCreated nullable:true
        lastUpdated nullable:true
    }  
    @Override
    String toString() {
        ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
}

※ @ToStringや@Canonicalが効かないみたいなので、ToStringBuilderを使用しています。

つづいて、Serviceクラス。
インターフェースをかませなくてもOK。

[ PersonService.groovy ]

package com.example.myproject.service

import com.example.myproject.domain.Person
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service("personService")
@Transactional
class PersonService {

    private static final Logger LOG = LoggerFactory.getLogger(PersonService.class)

    def findAll() {
        return Person.findAll()
    }

    def count() {
        return Person.count()
    }

    def save(Person person) {
        LOG.debug('★ #save(Person) was called ★')
        person.save()
    }

    def save(Collection<Person> persons) {
        LOG.debug('★ #save(Collection) was called ★ count: ' + persons.size())
        persons.each { person ->
            person.save()
        }
    }

    def validate(Person p) {
        p.validate()
    }
}

最後に、Applicationクラス。

[ Application.groovy ]

package com.example.myproject

import com.example.myproject.domain.Person
import com.example.myproject.service.PersonService
import grails.spring.BeanBuilder
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationContext

class Application {

    private static final Logger LOG = LoggerFactory.getLogger(Application.class)

    public static void main(String[] args) {

        BeanBuilder beanBuilder = new BeanBuilder()
        beanBuilder.loadBeans("classpath:SpringBeans.groovy")

        ApplicationContext context = beanBuilder.createApplicationContext()
        PersonService personService = context.getBean("personService") as PersonService

        personService.save([
                new Person(familyName: 'Yamada', givenName: 'Taro'),
                new Person(familyName: 'Asakura', givenName: 'Hanako')
        ])

        LOG.info("★persons count: <span>$</span>{personService.count()}")
        LOG.info ("★persons: <span>$</span>{personService.findAll()}")

        Person person = personService.findAll()[0]
        person.givenName = "Koziro"
        personService.save(person)

        LOG.info ("★persons: <span>$</span>{personService.findAll()}")
    }
}

以上で動きます。XMLにて設定を行わなくてもSpringが動かせるようになったのは嬉しいですね♪

4. テストクラス作成

フレームワークはテストのやりやすさも重要!
ということで、JUnitベースとSpockベースで作ってみました。

[ PersonServiceTest.groovy ]

package com.example.myproject  
import com.example.myproject.domain.Person
import com.example.myproject.service.PersonService
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner
import java.text.MessageFormat
import static org.hamcrest.CoreMatchers.*
import static org.junit.Assert.assertThat  
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:SpringBeans.groovy")
class PersonServiceTest {  
    @Autowired
    PersonService sut  
    @Test
    public void saveで登録できる() {
        Person p = new Person(familyName: 'Yagyuu', givenName: 'ZyuBei')
        assert !p.id  
        sut.save(p)  
        def actual = Person.findAll()[0].id
        assertThat(actual, is(not(nullValue())))
    }
    @Test
    public void 名前が11文字の場合は文字数チェックでエラーとなること() {  
        Person p = new Person(familyName: 'Yagyuu', givenName: '12345678901')
        sut.validate(p)  
        def error = p.errors.fieldErrors.first()
        def message = MessageFormat.format(
                error.defaultMessage,
                error.field,
                error.rejectedValue,
                10)  
        assertThat(message,
                is('Property [givenName] with value [12345678901] exceeds the maximum size of [10]'))
    }
}

エラーメッセージはmessage.propertiesを用意して、任意の値に上書きできます。
各チェックに対応するkeyは、Grailsと同様です。
なお、メッセージの合成([0]に値を埋め込む)は自分で処理を書く必要があります。

[ PersonServiceSpeck.groovy ]

package com.example.myproject  
import com.example.myproject.domain.Person
import com.example.myproject.service.PersonService
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner
import spock.lang.Specification  
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:SpringBeans.groovy")
class PersonServiceSpec extends Specification {
    @Autowired
    PersonService personService  
    @Test
    def "Spockでテストを行う"(){  
        when: "app is run without arguments"
        personService.save(new Person(familyName: 'Yagyuu', givenName: 'ZyuBei'))  
        then:
        Person.findAll().size() > 0
    }
}

Spockのテストクラスはとりあえず上記で動きましたが、いろいろ微妙な気が。。。
公式ドキュメント では

@ContextConfiguration(loader = SpringApplicationContextLoader.class)

を使用していますが、上記ではserviceがDIされない。。。
SpringのDI設定について、もっと調べる必要がありそうです。

以上です。