GrailsのGORMをSpringBootで利用する
今回は、Grailsが持つGORMというO/Rマッパー機構をSpring Bootで利用する、という話を紹介します。
中野靖治のGroovy活用術 第2回
- 2015年10月16日公開
GrailsのGORMをSpring Bootで利用する
はじめに
こんにちは。NTTソフトウェアの中野です。昨今、Spring Bootの流行がとどまるところを知りませんが、皆様いかがお過ごしでしょうか。今後Springプロダクトを利用してアプリケーションを開発する場合は、もはやSpring Boot一択といっても過言ではない流れが来ていると思います。
前回も書きましたが、われわれGrails推進室で推進しているGrailsも、2015年3月31日にリリースされたバージョン3.0.0からは基板部分がSpring Bootを使って書き直されており、Spring Bootの持つモダンなビルド/ランタイム機能を利用できるようになりました。
今回は逆に、Grailsが持つGORMというO/Rマッパー機構をSpring Bootで利用する、という話を紹介します。
GORMとは
これも前回に紹介しましたが、復習も兼ねてもう一度説明します。
GORM(Grails' Object Relational Mapping)とは、名前の通りGrailsのO/Rマッパーであり、Hibernateのラッパーになっています。Hibernateを使う際のあれこれと雑多な設定や、APIの定型的な呼び出しなどが、GroovyによるDSLで隠蔽されています。
GORMでは、ドメインクラスというクラスを定義するだけで、
- データベースのカラムとのマッピング
- 入力値のバリデーション
- 対応するテーブルへの保存(INSERT/UPDATE)、削除(DELETE)、リスト取得などクエリ発行(SELECT)
などが実現できます。
たとえば、書籍についてのドメインクラスを次のように書きます。
// 書籍ドメインクラス class Book { String title // 書籍名 String author // 著者名
// 制約 static constraints = { title blank:false, maxSize: 100 // 空文字禁止、最大100文字 author blank:false, maxSize: 100 } }
たったこれだけで、アプリケーションの起動時に対応する book テーブルが自動生成されます。PostgreSQLの場合は、以下のようなスキーマが生成されます。
book=# \d book Table "public.book" Column | Type | Modifiers ---------+------------------------+----------- id | bigint | not null version | bigint | not null author | character varying(100) | not null title | character varying(100) | not null Indexes: "book_pkey" PRIMARY KEY, btree (id)
また、テーブルへのアクセスのための便利なメソッドやバリデーションも使えるようになります。
def book = new Book(title: "プログラミングGROOVY", author: "中野 靖治、他") book.save() // データベースへの保存
assert Book.count() == 1 // レコード数の取得
def books = Book.where { title == "プログラミングGROOVY" }.list() // データベースからの取得 assert books*.title == ["プログラミングGROOVY"]
def book1 = new Book(title: "ダミータイトル", author: "") book1.save() // 自動的にバリデーションが実行される assert book1.hasErrors() // 空文字禁止制約のためエラーが検出される
def book2 = new Book(title: "ダミータイトル", author: "x" * 101) // author=xを101個つなげた文字列 book2.save() assert book2.hasErrors() // 100文字超過のためエラーが検出される
詳しくはGrailsのユーザガイドを参照してください。
- https://grails.github.io/grails-doc/latest/guide/GORM.html
- http://grails.jp/doc/latest/guide/GORM.html(有志による日本語翻訳版)
- 元となっているドキュメントバージョンが若干古いですが、GORMの仕様としてはこのときからほとんど変わっていません。
Spring BootからスタンドアロンGORMを使う
GORMはGrailsを構成する一機能ですが、Grailsの外で使えるようにした スタンドアロンGORM も提供されています。これを使うと、Spring BootからもGORMを利用できます。
Gradleのビルドスクリプトは次のようになります。ポイントは、以下の2点です。
- GORMのドメインクラスはGroovyによるDSLで記述するので、Groovyが必要になります([1]、[2])。
- スタンドアロンGORMのSpring Boot用の依存関係を指定します([3])。
buildscript { ext { springBootVersion = '1.2.6.RELEASE' } repositories { mavenCentral() } dependencies { classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}" classpath 'io.spring.gradle:dependency-management-plugin:0.5.2.RELEASE' classpath "org.springframework:springloaded:1.2.4.RELEASE" } }
apply plugin: 'groovy' // --[1] apply plugin: 'spring-boot' apply plugin: 'io.spring.dependency-management'
jar { baseName = 'book' version = '1.0.0' } sourceCompatibility = 1.8 targetCompatibility = 1.8
repositories { mavenCentral() }
dependencies { compile 'org.springframework.boot:spring-boot-starter-web' compile 'org.codehaus.groovy:groovy' // --[2] runtime 'com.h2database:h2' compile "org.grails:gorm-hibernate4-spring-boot:1.1.0.RELEASE" // --[3] }
さて、ドメインクラスとしては、先ほどのBookクラスを実装してから、以下のようなコントローラを実装します。コントローラはJavaで実装してもよいのですが、コードが短くて済むのでここではGroovyで記述します。
@RestController @RequestMapping("api/books") class BookController { @RequestMapping(value = "{id}", method = RequestMethod.GET) def getBook(@PathVariable("id") Long id) { def book = Book.get(id) // id指定でエンティティを取得する if (!book) { return new ResponseEntity(HttpStatus.NOT_FOUND) } return [title: book.title, author: book.author] // マップを返すだけでJSONに変換される }
@RequestMapping(method = RequestMethod.GET) def getBooks() {
// list()で全レコードを取得して、それぞれのエンティティのtitleとauthorを抽出して返す Book.list().collect { [title: it.title, author: it.author] } }
@RequestMapping(method = RequestMethod.POST) @ResponseStatus(HttpStatus.CREATED) def postBooks(@RequestBody Book book) { if (!book.save(flush: true)) { // データベースに保存する return new ResponseEntity(HttpStatus.BAD_REQUEST) } return new ResponseEntity(HttpStatus.CREATED) }
@RequestMapping(value = "{id}", method = RequestMethod.DELETE) @ResponseStatus(HttpStatus.NO_CONTENT) def deleteBooks(@PathVariable("id") Long id) { def book = Book.get(id) // id指定でエンティティを取得する if (!book) { return new ResponseEntity(HttpStatus.NOT_FOUND) } book.delete() // データベースから削除する } }
Gradleを使ってアプリケーションを起動します。
$ ./gradlew bootRun
httpieコマンドを使ってアクセスしてみます。
$ http http://localhost:8080/api/books HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Date: Tue, 13 Oct 2015 09:23:31 GMT Server: Apache-Coyote/1.1 Transfer-Encoding: chunked
[]
$ http post http://localhost:8080/api/books title=プログラミングGROOVY author=中野、他 HTTP/1.1 201 Created Content-Length: 0 Date: Tue, 13 Oct 2015 09:23:37 GMT Server: Apache-Coyote/1.1
$ http http://localhost:8080/api/books/1 HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Date: Tue, 13 Oct 2015 09:24:29 GMT Server: Apache-Coyote/1.1 Transfer-Encoding: chunked
{ "author": "中野、他", "title": "プログラミングGROOVY" }
$ http http://localhost:8080/api/books HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Date: Tue, 13 Oct 2015 09:23:39 GMT Server: Apache-Coyote/1.1 Transfer-Encoding: chunked
[ { "author": "中野、他", "title": "プログラミングGROOVY" } ]
$ http delete http://localhost:8080/api/books/1 HTTP/1.1 204 No Content Date: Tue, 13 Oct 2015 09:23:46 GMT Server: Apache-Coyote/1.1
$ http http://localhost:8080/api/books HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Date: Tue, 13 Oct 2015 09:23:49 GMT Server: Apache-Coyote/1.1 Transfer-Encoding: chunked
[]
きちんとCRUDが動作していることがわかりますね。
おわりに
JVM上で動作する動的型付け言語であるGroovyと、Groovyで記述するWebアプリケーションフレームワークのGrailsを社内外へ推進するために日々奮闘している。 Groovyスクリプトの起動時間を短縮するGroovyServや、GroovyスクリプトでのExcel操作を劇的に楽にするGExcelAPIなどのOSSを業務/プライベートで開発、 一般に公開。Groovy/Grails/GradleなどのOSSへのバグフィックスや機能パッチの提供などにも積極的に貢献している。 また、国内外(JJUG CCC、Java Day Tokyo, Gr8conf EUなど)での講演や書籍執筆などでも活動中。 著書に『プログラミングGroovy』(技術評論社/共著)がある。自他共に認めるビール党。