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
コマンドを実行します。
$ cd ${create-app保存先ディレクトリ}
$ grails run-app
$ cd ${create-app保存先ディレクトリ}
$ grails # インタラクティブモードでgrailsを起動する
grails> run-app
※ grails help
でオプション一覧が表示され、 grails help run-app
のように help 後にオプションを指定すると、そのオプションを実行するためのヘルプが表示されます。
アプリケーション画面
run-app でアプリケーションを実行した後、http://localhost:8080/ へアクセスすると下記のような Grails のイントロ画面が表示されます。
(画面は create-app で作成したファイルに加えて HelloController を作成した状態)
ディレクトリ構成
.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
詳細な情報
- build.gradle
Hello World を表示する
文字列で "Hello World!" を表示させてみます。
Grails ではコントローラを作成してそのクラスの中でアクションを記述すると一意の URL にマッピングされ、ブラウザでアクセスできるようになります。
マッピングルールは /<appname>/<controller>/<action>
となり、 <controller>
はコントローラクラスから Class
を除いた文字列になります。
例えば HelloController
を作成して index
アクションに "HelloWorld" を表示させるためには次の操作を行います。
grails> create-controller hello # HelloController Class が作成される
grails-app/controllers/grails/sample/app/HelloController.groovy
に作成された HelloController クラスを以下の内容に編集します。
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 ドメインクラスを作成する
$ 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
を編集して適当に属性を追加します。
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一覧画面 |
---|
![]() |
ある程度操作をすると分かると思いますが、次のルートが追加されています。
ルート | 機能 |
---|---|
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
を書き換えてください。
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
が発生する
- プロクシに getId() 以外のメソッドを呼び出した時に例外
- ID として null を渡した時に null が返される
- 指定したIDのドメインクラスのプロクシとなるインスタンスを取得する
ドメインを更新する
def p = Person.get(1)
p.name = 'Bob'
p.save()
ドメインを削除する
def p = Person.get(1)
p.delete()
Employee モデルを管理するアプリケーションを作る
もう少し Employee ドメインクラスを拡張させてより実践的にしてみます。
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/
配下に作成してください。
// the default value for this property is true
grails.databinding.convertEmptyStringsToNull = false
さて、Employee を表示させる一覧画面と編集画面を次のように変更します。
- 一覧画面
- ID, name, department, gender を表示する
- 各項目でソートが出来る
- Employee 新規作成ボタンを表示する
- いつでも新規作成ボタンを押せることとする
- Employee 編集ボタンを表示する
- どれか Employee が選択された場合だけ押せることとする
- 詳細画面
- 一覧画面で表示する項目に加えて、birth, serviceYears()実行結果, payment, note を表示する
<!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…"/></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>
<!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…"/></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 導入手順
- インストールされている 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
- Bootstrap v4 をダウンロードする
-
https://getbootstrap.com/docs/4.1/getting-started/download/
- "Compiled CSS and JS" から Download ボタンを押す
-
https://getbootstrap.com/docs/4.1/getting-started/download/
- JQuery v3 をダウンロードする
-
https://jquery.com/download/
- "Download the compressed, production jQuery 3.x.y" のリンク先ファイルを保存する
-
https://jquery.com/download/
- 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のデバッグ用途なので好みで追加してください)
-
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 ビューを編集してみましょう。
<head>
: <snip>
<asset:stylesheet src="application.css"/>
<asset:javascript src="application.js"/> <!-- body内に書かれている行をhead内へ移動させる -->
: <snip>
</head>
: <snip>
: <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
- js/
最後に grails-app/assets/application.(js|css) を編集します。
application.(js|css) に記述した require 先のファイルは上から順に読み込まれるため依存関係を持つパッケージは記述順序に気を付けてください。
: <snip>
//= require jquery-3.3.1.min
//= require bootstrap.bundle
//= require jquery.dataTables
//= require dataTables.bootstrap4
//= require_tree .
//= require_self
: <snip>
/*
: <snip>
*= require bootstrap
*= require dataTables.bootstrap4
*= require grails
*= require main
*= require mobile
*= require_self
: <snip>
*/
これで DataTables を使う準備が整いました。
一覧画面の表に DataTables を適用する
一覧画面の表に DataTables を適用します。
DataTables を適用するためには、jQuery で適用したい table を指す DOM に対して dataTable()
メソッドを適用します。
<!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 が適用されたことが分かります。
ここで、DataTables と Grails のページネーション機能が冗長となってしまっていることが分かります。
どちらの機能を有効にするかはアプリケーションの設計次第となりますが、今回はデータ処理量を減らすよう、Grails のページネーション機能を残すことにします。
DataTables のページネーション機能を無効化するためには公式ページに書かれているとおり、dataTable() メソッドに paging: true
パラメータを渡します。
$(document).ready(function() {
$('#employeeindex').dataTable({
"paging": false
});
} );
最後に、画面上部の Navbar を修正します。
また、合わせてスタイルを修正し、ICON を使うために Font Awesome を CDN で追加します。
/* 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;
}
@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>
}
<!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…"/>
</div>
</body>
</html>
Large Display Size | Small Display Size |
---|---|
![]() |
![]() |
データベースマイグレーション
これまでデータベースは 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'
}
}
}
次にデータベースの dbCreate
を none
に設定します。
そうしないと、 dbm-gorm-diff
オプションを使ってマイグレーションファイルを作成する際にうまく差分が出なくなってしまいます。
environments:
development:
dataSource:
dbCreate: none
url: jdbc:h2:./devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
: <snip>
チェンジログファイルを作成する
次にチェンジログファイルを作成します。
記述形式は選ぶことが出来ますが、Groovy DSL を使うことにします。
また、チェンジログはデータベースから作成する方法とドメインクラスから作成する方法の2つがあります。今はドメインクラスから起動時に自動でデータベースを作成していると思いますので、データベースからチェンジログを作成する方法を選択します。
$ 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
すると、次のチェンジログが作成されます。
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
を実行することになります。
- ドメインクラスを編集する
- チェンジログを編集する
- 自動で差分を作成する方法(ファイル名と
--add
オプションをつけるとチェンジログを指定したファイル名で別途保存し、changelog.groovy から include により読み込まれるよう追記される)$ grails dbm-gorm-diff add_birth_column_to_employee.groovy --add
- 自動で差分を作成する方法(ファイル名と
- データベースをバックアップする(何か問題が発生した時のため)
-
grails dbm-update
を任意の環境(development, test, production)に対して実行する
Groovy コンソール
Groovy コンソールとは、Groovy ソースコードを入力して実行することが出来るアプリケーションです。
Grails アプリケーションで実装したドメインクラスを読み込むことや、そのドメインクラスの ORM を使ってデータベースを操作することも出来ます。
grails console コマンドを実行することで Grails コンソールを起動することが出来ます。
$ cd ${applicationのディレクトリ}
$ grails console
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 を削除する