アレについて記す

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:${groovyVersion}",
            "org.grails:grails-gorm:${gormVersion}",
            "org.grails:grails-bootstrap:${grailsVersion}",
            "org.grails:grails-spring:${grailsVersion}",
            "com.h2database:h2:${h2Version}")

    testCompile("org.springframework.boot:spring-boot-starter-test",
                "org.spockframework:spock-core:${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: ${personService.count()}")
        LOG.info ("★persons: ${personService.findAll()}")

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

        LOG.info ("★persons: ${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設定について、もっと調べる必要がありそうです。

以上です。