Java
Grails
Groovy
gradle
GORM


Grails とは

Grails はコア技術とそれに関連するプラグインを通じて Web 開発における課題の多くを解決できるよう開発されたフルスタックフレームワークです。

Java 界隈における Web フレームワークの多くが、複雑であり DRY 原則を受け入れていない一方で、Grails は Rails や Django のような Web アプリケーションに関する現代的な考え方を導いた動的フレームワークのコンセプトを基にして、Spring や Hibernate のような既存の Java テクノロジーをベースに構築されています。


Grails をインストールする

参考 http://docs.grails.org/latest/guide/single.html#requirements

記事では下記のバージョンを使いました。


  • Grails Version: 3.3.5

  • Groovy Version: 2.4.15

  • JVM Version: 1.8.0_111


*nix (Mac OS, Linux, BSD OS) ユーザ向けの Grails インストール方法

※ Ubuntu 16 でのみ動作確認しております。詳細は SDKMAN のドキュメント を参照してください。


  • SDKMAN をインストールする(SDKMAN の Installation を参照)

  • Java をインストールする


    $ sdk install java


  • Grails をインストールする


    $ sdk install grails



Windows ユーザ向けの Grails インストール方法


  • Grails のバイナリファイル(zip形式) をダウンロードする(ダウンロード先)

  • インストールしたい任意のフォルダ配下に zip ファイルを展開する (C:\grails 等)

  • 展開した先のディレクトリ名を GRAILS_HOME としてシステム環境変数を設定する (GRAILS_HOME=C:\grails)

  • 環境変数の PATH%GRAILS_HOME%\bin を追加する

  • インストールが出来たことを確認する



    • grails -version を実行して Grails のバージョンが表示されればインストール完了




Grails アプリケーションを作成する

Grails アプリケーションを作成する際、必要なファイルを一括で作成する grails create-app コマンドを使います。

$ cd ${アプリケーション保存先ディレクトリ}

$ grails create-app grails-sample-app
# grails-sample-app は適宜アプリケーション名に変更する。
# "grails-sample-app" ディレクトリが作成され、
# アプリケーションのパッケージ名は "grails.sample.app" となる。

Grails 各コマンドをインタラクティブに実行できるモードを使うことも出来ます。 grails コマンドをオプション無しで実行するとインタラクティブモードが利用でき、インタラクティブモードではコマンドを Tab キーにより補完することが出来ます。


Grails アプリケーションを実行する

アプリケーションを実行するには run-app コマンドを実行します。


Grailsアプリケーションを実行する(grailsインタラクティブモードを使わない場合)

$ cd ${create-app保存先ディレクトリ}

$ grails run-app


Grailsアプリケーションを実行する(grailsインタラクティブモードを使う場合)

$ cd ${create-app保存先ディレクトリ}

$ grails # インタラクティブモードでgrailsを起動する
grails> run-app

grails help でオプション一覧が表示され、 grails help run-app のように help 後にオプションを指定すると、そのオプションを実行するためのヘルプが表示されます。


アプリケーション画面

run-app でアプリケーションを実行した後、http://localhost:8080/ へアクセスすると下記のような Grails のイントロ画面が表示されます。

(画面は create-app で作成したファイルに加えて HelloController を作成した状態)

image.png


ディレクトリ構成

.gradle/

.idea/ ... IntelliJ IDE 用設定ファイル
build/ ... ビルドファイル(create-app実行直後には存在しない)
gradle/
grails-app/ ... アプリケーションのソースコード
assets/ ... アセットリソース置き場(アセットパイプラインで処理されるファイル群)
conf/ ... ランタイム設定
controllers/ ... コントローラ(MVC モデルの C)
${app名}/ ... app 名 (ハイフンが含まれる場合、ハイフン毎にディレクトリが分かれる)
UrlMappings.groovy ... URLマッピング
domain/ ... ドメインクラス(MVC モデルの M)
i18n/ ... internationalization(i18n) 設定のサポート
init/ ... アプリケーション起動時の処理
services/ ... サービス層
taglib/ ... Tag ライブラリ(View で利用できるカスタムタグを定義したもの)
utils/ ... Grails に特化したユーティリティ
views/ ... Groovy Server Page(GSP) or JSON Views (MVC モデルの V)
src/
integration-test/ ... インテグレーションテスト
main/ ... アセットパイプライン処理を通さない静的ファイル
groovy/ ... DBテーブルと関連づけを行いたくないドメインクラス
webapp/ ... 静的ファイル(WARに含まれ、JARに含まれない)
resources/public ... 静的ファイル(/static/x/y/zでアクセスする)
test/ ... ユニットテスト
groovy/
${app名}/
${***Spec}.groovy
.gitignore ... Git用 VCS 管理外ファイル設定
build.gradle ... ビルド設定
gradle.properties ... Gradle 設定
gradlew ... Gradle 起動用スクリプト(UN*X用)
gradlew.bat ... Gradle 起動用スクリプト(Windows用)
grails-wrapper.jar
grailsw ... Grails 起動用スクリプト(UN*X用)
grailsw.bat ... Grails 起動用スクリプト(Windows用)
README.md ... README


Hello World を表示する

文字列で "Hello World!" を表示させてみます。

Grails ではコントローラを作成してそのクラスの中でアクションを記述すると一意の URL にマッピングされ、ブラウザでアクセスできるようになります。

マッピングルールは /<appname>/<controller>/<action> となり、 <controller> はコントローラクラスから Class を除いた文字列になります。

例えば HelloController を作成して index アクションに "HelloWorld" を表示させるためには次の操作を行います。


Grailsアプリケーションを実行する

grails> create-controller hello # HelloController Class が作成される


grails-app/controllers/grails/sample/app/HelloController.groovy に作成された HelloController クラスを以下の内容に編集します。


grails-app/controllers/grails/sample/app/HelloController.groovy

package grails.sample.app

class HelloController {
def index() {
render "Hello World!"
}
}


アプリケーションを実行すると http://localhost:8080/hello/index 又は http://localhost:8080/hello/ へアクセスすると文字列 "Hello World!" が表示されます。

(index アクションは URL 上で省略できる)


ドメインの CRUD 機能を作成する

ドメインは Grails における MVC の M です。

先ほどの HelloWorld を表示させた例のように、コントローラやビューを個別に作成することも出来ますが、 generate-all オプションを使うことでドメインの CRUD 機能を持つコントローラとビュー(加えてテスト用クラス)を作成することが出来ます。


EmployeeドメインクラスとCRUD用コントローラとビュー、テストを作成

# Employee ドメインクラスを作成する

$ grails create-domain-class employee
| Created grails-app/domain/grails/sample/app/Employee.groovy
| Created src/test/groovy/grails/sample/app/EmployeeSpec.groovy
# Employee ドメインクラスをCRUDするコントローラ、ビュー、テスト用クラスを作成する
$ grails generate-all grails.sample.app.Employee
| Rendered template Controller.groovy to destination grails-app\controllers\grails\sample\app\EmployeeController.groovy
| Rendered template Service.groovy to destination grails-app\services\grails\sample\app\EmployeeService.groovy
| Rendered template Spec.groovy to destination src\test\groovy\grails\sample\app\EmployeeControllerSpec.groovy
| Rendered template ServiceSpec.groovy to destination src\integration-test\groovy\grails\sample\app\EmployeeServiceSpec.groovy
| Scaffolding completed for grails-app\domain\grails\sample\app\Employee.groovy
| Rendered template create.gsp to destination grails-app\views\employee\create.gsp
| Rendered template edit.gsp to destination grails-app\views\employee\edit.gsp
| Rendered template index.gsp to destination grails-app\views\employee\index.gsp
| Rendered template show.gsp to destination grails-app\views\employee\show.gsp
| Views generated for grails-app\domain\grails\sample\app\Employee.groovy

以上で Employee ドメインクラスを CRUD する機能が作成できました。

まだドメインクラスには何も属性が無いため、ファイル grails-app/domain/grails/sample/app/Employee.groovy を編集して適当に属性を追加します。


grails-app/domain/grails/sample/app/Employee.groovy

package grails.sample.app

class Employee {

String name

static constraints = {
}
}


run-app を再度実行してブラウザでアプリケーションの TOP 画面を表示すると、 Available Controllers: に grails.sample.app.EmployeeController が追加されています。

grails.sample.app.EmployeeController の文字列はリンクになっており、クリックすると Employee 一覧画面(http://localhost:8080/employee/index) に遷移します。

Employee一覧画面

image.png

ある程度操作をすると分かると思いますが、次のルートが追加されています。

ルート
機能

GET /employee/index
Employee一覧画面を表示

GET /employee/create
Employee新規作成画面を表示

GET /employee/show/:id
Employee詳細画面(:idはドメインクラスのID)を表示

GET /employee/edit/:id
Employee編集画面(:idはドメインクラスのID)を表示

POST /employee/save
Employee新規作成

PUT /employee/update/:id
Employee更新(:idはドメインクラスのID)

DELETE /employee/delete/:id
Employee削除(:idはドメインクラスのID)

アプリケーションが利用する DB は development, test, production といった環境変数毎に変えることが出来ます。

Grails では全ての環境においてデフォルトで H2 Database を利用するよう設定されています。

また CRUD 操作を一通り終えて run-app を終了して再度アプリケーションを実行すると DB が初期化されていることに気が付くと思います。

これは H2 はインメモリ DB であり、加えて開発環境の DB はアプリケーション終了時に DB を DROP し、起動時に作成するように設定されているためです。

これらの設定は grails-app/conf/application.yml で設定します。

(設定内容の詳細は The Grails Framework > The DataSource を参照してください。)

今回、development 環境で作成したデータベースは破棄しないよう設定することにします。

次の内容で grails-app/conf/application.yml を書き換えてください。


grails-app/conf/application.yml

environments:

development:
dataSource:
# create-drop から update へ変更する
dbCreate: update
# mem:devDbから./devDbへ変更する
url: jdbc:h2:./devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE

尚、dbCreate で設定できる値は次のとおりです。

dbCreateの設定値
説明

create
起動時に既存スキーマ(テーブル, 索引, 他)をドロップし再作成する

create-drop
createと同じだが、アプリケーション終了時にも table を削除する

update
存在しないテーブルや索引を作成又は更新する。既存のテーブルやデータはドロップしない。(カラム名の変更などを適切に処理できず古いカラムとデータが残ることに注意)

validate
DBに変更を加えず、既存スキーマと設定を比較してワーニングを出力する

any other value
何もしない


ドメインクラスをソースコードで操作する


ドメインを作成する

def p = new Person(name: "Fred", age: 40, lastVisit: new Date())

p.save()


ドメインを読み取る

def p = Person.get(1)

println('name: ' + p.name)


  • get (http://docs.grails.org/latest/ref/Domain%20Classes/get.html)


    • 指定したIDのドメインクラスのインスタンスを取得する

    • 指定したIDが見付からなかった場合は null が返される



  • read (http://docs.grails.org/latest/ref/Domain%20Classes/read.html)


    • 指定したIDのドメインクラスのインスタンスを読み取り専用で取得する

    • 指定したIDが見付からなかった場合は null が返される

    • 読み取り専用とは自動でダーティ検出をしないことを指し、明示的に save() を呼び出すことで DB の変更は可能である



  • load (http://docs.grails.org/latest/ref/Domain%20Classes/load.html)


    • 指定したIDのドメインクラスのプロクシとなるインスタンスを取得する


      • プロクシは getId() 以外のメソッドを呼び出した時に初期化される



    • 指定したIDが見付からなかった場合もプロクシが返される


      • プロクシに getId() 以外のメソッドを呼び出した時に例外 org.hibernate.ObjectNotFoundException が発生する



    • ID として null を渡した時に null が返される




ドメインを更新する

def p = Person.get(1)

p.name = 'Bob'
p.save()


ドメインを削除する

def p = Person.get(1)

p.delete()


Employee モデルを管理するアプリケーションを作る

もう少し Employee ドメインクラスを拡張させてより実践的にしてみます。


grails-app/domain/grails/sample/app/Employee.groovy

package grails.sample.app

class Employee {

String name
String department
String gender
Date birth
Date joinedDate
Long payment
String note

static constraints = {
name blank: false, unique: true
department blank: false
gender blank: false, inList: ['male', 'female', 'other']
birth blank: false
joinedDate blank: false
payment min: new Long(0), blank: false
note blank: true
}

/**
* 勤続年数
*/

public int serviceYears() {
def today = new Date()
return Math.max(0, today.year - this.joinedDate.year)
}
}


ひとまず note 以外は nullable: false (default なので constraints には未指定), blank: false としました。

note のように blank: true にする場合は、データが blank である場合に null へ変換する処理を無効化する必要があります。

その設定は application.groovy に下記のとおり設定することで無効化できます。

ファイルが存在しなければ grails-app/conf/ 配下に作成してください。


grails-app/conf/application.groovy

// the default value for this property is true

grails.databinding.convertEmptyStringsToNull = false

さて、Employee を表示させる一覧画面と編集画面を次のように変更します。


  • 一覧画面


    • ID, name, department, gender を表示する

    • 各項目でソートが出来る

    • Employee 新規作成ボタンを表示する


      • いつでも新規作成ボタンを押せることとする



    • Employee 編集ボタンを表示する


      • どれか Employee が選択された場合だけ押せることとする





  • 詳細画面


    • 一覧画面で表示する項目に加えて、birth, serviceYears()実行結果, payment, note を表示する




一覧画面のビュー(grails-app/views/employee/index.gsp)

<!DOCTYPE html>

<html>
<head>
<meta name="layout" content="main" />
<g:set var="entityName" value="${message(code: 'employee.label', default: 'Employee')}" />
<title><g:message code="default.list.label" args="[entityName]" /></title>
<script type="text/javascript">
/* 編集ボタンのステータスをラジオボタンのステータスを基に設定する */
function setEditButtonStatusByRadioButton() {
var edit_button_id = "edit_button";
var radios = document.getElementsByName('id');
var checkedNum = 0;
radios.forEach(e => e.checked && checkedNum++);
if (checkedNum > 0) {
document.getElementById(edit_button_id).disabled = false;
} else {
document.getElementById(edit_button_id).disabled = true;
}
}
</script>
</head>
<body>
<a href="#list-employee" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content&hellip;"/></a>
<div class="nav" role="navigation">
<ul>
<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
</ul>
</div>
<div id="list-employee" class="content scaffold-list" role="main">
<h1><g:message code="default.list.label" args="[entityName]" /></h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>

<g:form action="edit">
<table class="table table-striped">
<thead>
<tr>
<th>Edit</th>
<g:each in="${['id', 'name', 'department', 'gender']}" var="p">
<g:sortableColumn property="${p}" title="${p}" />
</g:each>
</tr>
</thead>
<tbody>
<g:each in="${employeeList}" var="employee">
<tr>
<td><input name="id" type="radio" value="${employee.id}" onclick="setEditButtonStatusByRadioButton()" /></td>
<g:each in="${['id', 'name', 'department', 'gender']}" var="p">
<g:if test="${p=='id'}">
<td><g:link method="GET" resource="${employee}">${employee.properties[p]}</g:link></td>
</g:if>
<g:else>
<td>${employee.properties[p]}</td>
</g:else>
</g:each>
</tr>
</g:each>
</tbody>
</table>

<button disabled="false" id="edit_button"><g:message code="default.edit.label" args="[entityName]" /></button>
</g:form>

<div class="pagination">
<g:paginate total="${employeeCount ?: 0}" />
</div>
</div>
</body>
</html>



詳細画面のビュー(grails-app/views/employee/show.gsp)

<!DOCTYPE html>

<html>
<head>
<meta name="layout" content="main" />
<g:set var="entityName" value="${message(code: 'employee.label', default: 'Employee')}" />
<title><g:message code="default.show.label" args="[entityName]" /></title>
</head>
<body>
<a href="#show-employee" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content&hellip;"/></a>
<div class="nav" role="navigation">
<ul>
<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
</ul>
</div>
<div id="show-employee" class="content scaffold-show" role="main">
<h1><g:message code="default.show.label" args="[entityName]" /></h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>

<ol class="property-list employee">
<li class="fieldcontain">
<g:each in="${['id', 'name', 'department', 'gender', 'birth', 'serviceYears', 'payment', 'note']}" var="p">
<span id="name-label" class="property-label">${p}</span>
<g:if test="${p=='serviceYears'}">
<div class="property-value" aria-labelledby="name-label">${employee.serviceYears()}</div>
</g:if>
<g:else>
<div class="property-value" aria-labelledby="name-label">${employee.properties[p]}</div>
</g:else>
</g:each>
</li>
</ol>

<g:form resource="${this.employee}" method="DELETE">
<fieldset class="buttons">
<g:link class="edit" action="edit" resource="${this.employee}"><g:message code="default.button.edit.label" default="Edit" /></g:link>
<input class="delete" type="submit" value="${message(code: 'default.button.delete.label', default: 'Delete')}" onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');" />
</fieldset>
</g:form>
</div>
</body>
</html>


以上で、少し実践的な内容になりました。

尚、ビューは Groovy Server Pages (GSP) で記述されており、 <g:link>, <g:form> 等は GSP タグです。(詳細は Groovy Server Pages (GSP) を参照してください)


Twitter Bootstrap を導入する

Grails 3.3.5 にはデフォルトで Bootstrap 3.3.6 が導入されていました。


  • grails-app/assets/javascripts/bootstrap.js

  • grails-app/assets/stylesheets/bootstrap.css

Grails plugin の twitter-bootstrap-grails-plugin があるようですが、'18/09/18 現在のバージョンが 3.3.5 のようであり、最終コミットが 2 年前ということであまりメンテされている様子もないため、Bootstrap v4 を手動で導入してみることにします。


Twitter Bootstrap v4 導入手順


  1. インストールされている Bootstrap(js, css), JQuery を削除する


    • grails-app/assets/javascripts/bootstrap.js

    • grails-app/assets/stylesheets/bootstrap.css

    • grails-app/assets/javascripts/jquery-2.2.0.min.js



  2. Bootstrap v4 をダウンロードする



  3. JQuery v3 をダウンロードする



  4. Bootstrap v4, JQuery v3 を Grails アプリケーションフォルダにコピーする


    • grails-app/assets/javascripts/bootstrap.bundle.js

    • grails-app/assets/javascripts/jquery-3.3.1.min.js

    • grails-app/assets/stylesheets/bootstrap.css

    • grails-app/assets/stylesheets/bootstrap.css.map (CSSのデバッグ用途なので好みで追加してください)




  5. grails-app/assets/javascripts/application.js を編集する



    • //= require jquery-2.2.0.min から //= require jquery-3.3.1.min へ変更する


    • //= require bootstrap から //= require bootstrap.bundle へ変更する



アプリケーションを再実行すれば完了です。(navbar が崩れますが使っていないので無視します)


導入後の確認

先ほど記述した index.gsp ビューを編集してみましょう。


grails-app/views/layouts/main.gsp

<head>

: <snip>
<asset:stylesheet src="application.css"/>
<asset:javascript src="application.js"/> <!-- body内に書かれている行をhead内へ移動させる -->
: <snip>
</head>
: <snip>


grails-app/views/employee/index.gsp

  : <snip>

<button class="btn btn-secondary" disabled="false" id="edit_button"><g:message code="default.edit.label" args="[entityName]" /></button>
: <snip>

ボタンに Bootstrap 4 のスタイルが適用されれば OK です。


DataTable を使って一覧画面にフィルタ機能を追加する


事前準備

Employee 一覧画面にフィルタ機能を追加して、もう少し実用的にしてみます。

今回一覧画面に表示したテーブルに検索機能を追加するために、DataTables を使うことにします。

尚、Grails plugin として grails-datatables 等がありましたが、bootstrap, jquery と同様に最新バージョンの DataTables をインストールすることにします。

DataTables の Download ページから下記の項目を選択してファイルをダウンロードします。


  • Styling framework: Bootstrap4

  • Packages: DataTables

  • Extensions: 好みに応じて(例では何も選択してません)

  • Minify, Concatenate: 無効

ダウンロードしたファイルから下記のファイルを種別に応じて、grails-app/assets/(javascripts|stylesheets)/配下にコピーします。


  • DataTables-x.y.z/


    • js/


      • dataTables.bootstrap4.js

      • jquery.dataTables.js



    • css/


      • dataTables.bootstrap4.css





最後に grails-app/assets/application.(js|css) を編集します。

application.(js|css) に記述した require 先のファイルは上から順に読み込まれるため依存関係を持つパッケージは記述順序に気を付けてください。


grails-app/assets/javascripts/application.js

  : <snip>

//= require jquery-3.3.1.min
//= require bootstrap.bundle
//= require jquery.dataTables
//= require dataTables.bootstrap4
//= require_tree .
//= require_self
: <snip>


grails-app/assets/stylesheets/application.css

/*

: <snip>
*= require bootstrap
*= require dataTables.bootstrap4
*= require grails
*= require main
*= require mobile
*= require_self
: <snip>
*/


これで DataTables を使う準備が整いました。


一覧画面の表に DataTables を適用する

一覧画面の表に DataTables を適用します。

DataTables を適用するためには、jQuery で適用したい table を指す DOM に対して dataTable() メソッドを適用します。


grails-app/views/employee/index.gsp

<!DOCTYPE html>

<html>
<head>
: <snip>
<script type="text/javascript">
/* 編集ボタンのステータスをラジオボタンのステータスを基に設定する */
function setEditButtonStatusByRadioButton() {
var edit_button_id = "edit_button";
var radios = document.getElementsByName('id');
var checkedNum = 0;
radios.forEach(e => e.checked && checkedNum++);
if (checkedNum > 0) {
document.getElementById(edit_button_id).disabled = false;
} else {
document.getElementById(edit_button_id).disabled = true;
}
}

$(document).ready(function() {
$('#employeeindex').dataTable();
} );
</script>
</head>
<body>
: <snip>
<table id="employeeindex" class="display table table-striped">
: <snip>
</table>
: <snip>
</body>
</html>


アプリケーションを実行すると DataTables が適用されたことが分かります。

image.png

ここで、DataTables と Grails のページネーション機能が冗長となってしまっていることが分かります。

どちらの機能を有効にするかはアプリケーションの設計次第となりますが、今回はデータ処理量を減らすよう、Grails のページネーション機能を残すことにします。

DataTables のページネーション機能を無効化するためには公式ページに書かれているとおり、dataTable() メソッドに paging: true パラメータを渡します。


#employeeindexのDOMに対してページネーション機能なしでDataTablesを適用する

$(document).ready(function() {

$('#employeeindex').dataTable({
"paging": false
});
} );

最後に、画面上部の Navbar を修正します。

また、合わせてスタイルを修正し、ICON を使うために Font Awesome を CDN で追加します。


grails-app/assets/stylesheets/main.css

/* NAVIGATION MENU */

.nav, nav {
zoom: 1;
}

.nav ul {
overflow: hidden;
padding-left: 0;
zoom: 1;
}

.nav li {
display: block;
float: left;
list-style-type: none;
margin-right: 0.5em;
padding: 0;
}

.nav a, nav a {
color: #666666;
display: block;
padding: 0.25em 0.7em;
text-decoration: none;
-moz-border-radius: 0.3em;
-webkit-border-radius: 0.3em;
border-radius: 0.3em;
}

.nav a:active, .nav a:visited, nav a:active, nav a:visited {
color: #666666;
}

.nav a:focus, .nav a:hover, nav a:focus, nav a:hover {
background-color: #999999;
color: #ffffff;
outline: none;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
}

.no-borderradius .nav a:focus, .no-borderradius nav a:focus, .no-borderradius .nav a:hover, .no-borderradius nav a:hover {
background-color: transparent;
color: #444444;
text-decoration: underline;
}

.nav a.home, .nav a.list, .nav a.create, nav a.home, nav a.list, nav a.create {
background-position: 0.7em center;
background-repeat: no-repeat;
text-indent: 25px;
}

.nav a.home, nav a.home {
background-image: url(../images/skin/house.png);
}

.nav a.list, nav a.list {
background-image: url(../images/skin/database_table.png);
}

.nav a.create, nav a.create {
background-image: url(../images/skin/database_add.png);
}

.nav li.dropdown ul.dropdown-menu, nav li.dropdown ul.dropdown-menu {
background-color: #424649;
}



grails-app/assets/stylesheets/mobile.css

@media screen and (max-width: 480px) {

.nav, nav {
padding: 0.5em;
}

.nav li, nav li {
margin: 0 0.5em 0 0;
padding: 0.25em;
}
: <snip>
}



grails-app/views/layouts/main.gsp

<!doctype html>

<html lang="en" class="no-js">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>
<g:layoutTitle default="Grails"/>
</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<asset:link rel="icon" href="favicon.ico" type="image/x-ico" />

<asset:stylesheet src="application.css"/>
<asset:javascript src="application.js"/>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous">

<g:layoutHead/>
</head>
<body>

<nav class="navbar-expand-lg pr-3 navbar navbar-default">
<a class="navbar-brand" href="/#">
<asset:image src="grails.svg" alt="Grails Logo"/>
</a>
<button class="navbar-toggler mx-2" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="fas fa-bars" style="font-size: 3rem;"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<g:pageProperty name="page.nav" />
</ul>
</div>
</nav>

<g:layoutBody/>

<div class="footer" role="contentinfo"></div>

<div id="spinner" class="spinner" style="display:none;">
<g:message code="spinner.alt" default="Loading&hellip;"/>
</div>

</body>
</html>


Large Display Size
Small Display Size

image.png
image.png


データベースマイグレーション

これまでデータベースは dbCreate: update に設定することでアプリケーション起動時にモデルファイルを参考にして更新をしましたが、変更が出来る条件が限られていることや古いカラムが残ることから、複数人でアプリケーションを開発する時には人によって違うデータベース構造となってしまう可能性があります。

そこで、マイグレーションファイルを作成して、そこからデータベースを作成することにします。

Grails でデータベースマイグレーションを行うためには database-migration プラグインを使います。(database-migratation プラグインは Liquibase ライブラリを使っています。)

マイグレーションは Groovy DSL 又は native Liquibase XML により記述された1つ以上のチェンジログファイルを使って処理されます。

チェンジログファイルはグローバルにユニークな ID を持つ。(ID にはチェンジログを作成したユーザのユーザ名が含まれる)


事前準備

database-migration プラグインを使うために bundle.gradle を下記のように編集します。(プラグインのバージョンは適宜変更する)

尚、 bundle.gradle は Gradle 用の設定ファイルであり、Gradle とは Apache Ant や Apache Maven のコンセプトに基づくオープンソースビルド自動化システムです。(参考: Wikipedia > Gradle)

buildscript {

repositories {
: <snip>
}
dependencies {
: <snip>
classpath 'org.grails.plugins:database-migration:3.0.4' # database-migrationプラグインを追加
}
}

dependencies {
: <snip>
compile 'org.grails.plugins:database-migration:3.0.4'
compile 'org.liquibase:liquibase-core:3.5.5'
: <snip>
}

: <snip>

sourceSets {
main {
resources {
srcDir 'grails-app/migrations'
}
}
}

次にデータベースの dbCreatenone に設定します。

そうしないと、 dbm-gorm-diff オプションを使ってマイグレーションファイルを作成する際にうまく差分が出なくなってしまいます。


grails-app/conf/application.yml

environments:

development:
dataSource:
dbCreate: none
url: jdbc:h2:./devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
: <snip>


チェンジログファイルを作成する

次にチェンジログファイルを作成します。

記述形式は選ぶことが出来ますが、Groovy DSL を使うことにします。

また、チェンジログはデータベースから作成する方法とドメインクラスから作成する方法の2つがあります。今はドメインクラスから起動時に自動でデータベースを作成していると思いますので、データベースからチェンジログを作成する方法を選択します。


データベースからチェンジログをGroovyDSL形式で作成する方法

$ grails dbm-generate-changelog changelog.groovy

: <snip> 必要に応じてプラグインがダウンロードされる
:compileJava NO-SOURCE
:compileGroovy UP-TO-DATE
:buildProperties UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:findMainClass
:dbmGenerateChangelog

BUILD SUCCESSFUL

Total time: 16.478 secs


すると、次のチェンジログが作成されます。


チェンジログ(grails-app/migrations/changelog.groovy)

databaseChangeLog = {

changeSet(author: "tatsurou (generated)", id: "1537102923747-1") {
createTable(tableName: "EMPLOYEE") {
column(autoIncrement: "true", name: "ID", type: "BIGINT(19)") {
constraints(primaryKey: "true", primaryKeyName: "CONSTRAINT_7")
}

column(name: "VERSION", type: "BIGINT(19)") {
constraints(nullable: "false")
}

column(name: "NAME", type: "VARCHAR(255)") {
constraints(nullable: "false")
}
}
}
}


これでチェンジログを基に DB を構築することが出来るようになりました。

(構築されるのはデータベースのスキーマだけであり、データはマイグレーションされません)

今後、ドメインクラスを編集した後にチェンジログを手動又は自動で作成し、 dbm-update を実行することになります。


  1. ドメインクラスを編集する

  2. チェンジログを編集する


    • 自動で差分を作成する方法(ファイル名と --add オプションをつけるとチェンジログを指定したファイル名で別途保存し、changelog.groovy から include により読み込まれるよう追記される)


      $ grails dbm-gorm-diff add_birth_column_to_employee.groovy --add




  3. データベースをバックアップする(何か問題が発生した時のため)


  4. grails dbm-update を任意の環境(development, test, production)に対して実行する


Groovy コンソール

Groovy コンソールとは、Groovy ソースコードを入力して実行することが出来るアプリケーションです。

Grails アプリケーションで実装したドメインクラスを読み込むことや、そのドメインクラスの ORM を使ってデータベースを操作することも出来ます。

grails console コマンドを実行することで Grails コンソールを起動することが出来ます。


groovyコンソールを起動する

$ cd ${applicationのディレクトリ}

$ grails console

image.png

package grails.sample.app

def id = 1
def e = Employee.get(id)
if (e == null) {
println("Not found employee id " + id)
exit()
}
println("employee: " + e)
println("name: " + e.name)
e.name = 'test99'
e.save(flush: true)


エラー対応

Grails コマンドを実行する中で発生するエラーとその対応方法を記述します。


Could not acquire change log lock

Command execution error: Could not acquire change log lock.  Currently locked by ${COMPUTER_NAME} (${IP_ADDRESS}) since 18/09/17 0:09



  • grails dbm-release-locks 又は DB を削除する