hidemium's blog

日々学んだことをアウトプットする。

PackerでWindows Server 2012 R2のVMwareイメージをビルドする

この記事は NIFTY Cloud Advent Calendar 2015の 20 日目です。
ESXi上で動作するOSイメージを自動ビルドするのにPackerが使われています。
LinuxのOSイメージをPackerでビルドする事例はありますが、Windowsについてはまだまだ少ないと感じており、今回は、WindowsのOSイメージをPackerでビルドする方法を紹介します。

構成

※Workstation上に、UbuntuとESXiを稼働させています。

インストール

Packerのインストール

まず、Packerのインストールを行います。

$ wget https://dl.bintray.com/mitchellh/packer/packer_0.8.6_linux_amd64.zip
$ mkdir /usr/local/packer
$ unzip -d /usr/local/packer packer_0.8.6_linux_amd64.zip
$ echo 'export PATH="${PATH}:/usr/local/packer"' | sudo tee -a /etc/profile.d/packer.sh; source /etc/profile.d/packer.sh
$ packer --version
0.8.6

Packer Template

Packerを使ってビルドするには、Packer用のテンプレートファイル(json形式)を作成する必要があります。GitHub上にいくつかプロジェクトがあり、設定ファイル一式を取得することができます。

ただし、上記の設定ファイルだけだと一部動作しないところもあり、環境に合わせて変更を行います。今回の環境に合わせて修正を行った設定ファイルを使います。

$ git clone https://github.com/hideakihal/packer-windows-esxi.git
$ cd packer-windows-esxi

ISOイメージの配置

packer-windows-esxi/iso配下にWindows Server 2012 R2のOSイメージを配置します。
また、合わせてwindows_2012_r2.jsonのvariablesにISOファイルのファイル名とハッシュ値を入力します。

$ vi windows_2012_r2.json
  "variables": {
    "iso_url": "./iso/9600.17050.WINBLUE_REFRESH.140317-1640_X64FRE_SERVER_EVAL_EN-US-IR3_SSS_X64FREE_EN-US_DV9.ISO",
    "iso_checksum_type": "md5",
    "iso_checksum": "5B5E08C490AD16B59B1D9FAB0DEF883A",
   :

ESXi のファイアウォールの設定

ESXiのFirewall設定を開き、VM serial port connected over network にチェックを入れ、OKを選択します。

ESXi の GuestIPHack の有効化

VMware Tools が起動していない仮想マシンの IP を取得するため、 GuestIPHack を有効にします。
ESXi に SSH でログインして、下記のコマンドを実行します。

esxcli system settings advanced set -o /Net/GuestIPHack -i 1

ビルド方法

上記の準備ができたところで、以下のコマンドを実行し、ビルドを行います。コマンドに PACKER_LOG=1 PACKER_LOG_PATH=build.log を追加すると、詳細なログを出力することができるのでデバッグに便利です。

PACKER_LOG=1 PACKER_LOG_PATH=build.log \
packer build \
-var 'remote_host=<ESXi の IP アドレス>' \
-var 'remote_datastore=<データストア名>' \
windows_2012_r2.json

Packer Templateの解説

ビルドの流れ

今回作成した設定ファイルは、以下の流れでビルドを行っています。

  1. buildersによるOSリソースの設定
  2. 応答ファイル(Autounattend.xml)によるOSインストール
    1. 言語設定(SetupUILanguage等)
    2. インストールエディション選択(ImageInstall>OSImage>InstallFrom)
    3. ディスク構成(DiskConfiguration)
    4. コンピュータ名(ComputerName)
    5. アカウント設定(UserAccounts)
    6. 初期設定スクリプト(FirstLogonCommands)
      1. 初期設定(init.bat)
      2. Windows Update(win-updates.ps1)
      3. WinRM有効化(winrm.bat)
  3. WinRMにてログインし、provisionersで設定した事後スクリプトを実行
    1. VMware Toolsインストール(vm-guest-tools.bat)
    2. RDP有効化(enable-rdp.bat)

Windows Updateについて

Windows Updateを行っていますが、応答ファイルを以下のように修正することで無効にすることもできます。

$ vi Autounattend.xml
<!-- WITHOUT WINDOWS UPDATES -->
<SynchronousCommand wcm:action="add">
    <CommandLine>cmd.exe /c a:\winrm.bat</CommandLine>
    <Description>Fire-up winrm!</Description>
    <Order>99</Order>
</SynchronousCommand>
<!-- WITH WINDOWS UPDATES
<SynchronousCommand wcm:action="add">
    <CommandLine>cmd.exe /c C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File a:\win-updates.ps1 -DoneScript a:\winrm.bat</CommandLine>
    <Description>Run Windows Updates</Description>
    <Order>99</Order>
    <RequiresUserInput>true</RequiresUserInput>
</SynchronousCommand>
-->

参考までにビルド時間は、Windows Updateがある場合は3時間30分、Windows Updateがない場合は15分かかりました。そのため、デバッグ中はWindows Updateはオフにしたほうが良いかと思います。
また、応答ファイルをデバッグする場合は、Windows ADKをインストールし、Windows システム イメージ マネージャーを利用すると便利です。

Windowsのprovisionerについて

Windowsイメージをビルドする際に利用できる provisioner は、PowerShell(powershell) と Windows Shell(windows-shell) しかありません。
OpenSSHをインストールして、Shell provisioner(shell) を試してましたが動作しませんでした。(動かす方法があるかもしれませんが。)
powershellpowershellスクリプトを実行でき、windows-shellはcmdによるコマンドを実行できます。
今回は以下のように、VMware ToolsのインストールとRDP有効化のために利用しています。他の設定を行いたい場合は、ここにスクリプトを追加するかたちになります。

$ vi windows_2012_r2.json
  "provisioners": [
    {
      "type": "windows-shell",
      "scripts": [
        "scripts/vm-guest-tools.bat",
        "scripts/enable-rdp.bat"
      ]
    }
  ],

ただし、powershellwindows-shellを利用するためには、WinRMによる接続を行う必要があり、テンプレートファイルにWinRM接続の設定を定義しておく必要があります。

$ vi windows_2012_r2.json
      "communicator": "winrm",
      "winrm_username": "packer",
      "winrm_password": "packer",
      "winrm_timeout": "6h",

おわりに

今回、PackerによるWindowsのOSイメージをビルドを行いましたが、GitHub上にいくつかプロジェクトをそのまま利用するだけでは難しく、はまりどころが多いように感じました。実用化に向けては、環境に合わせてテンプレートファイルを修正し、provisionerをカスタマイズしていく方向になるかと思います。
今回の記事がみなさんのお役に立てられればと思います。

vCenterの開発環境としてVCSIMを構築する

VMware vSphere APIPythonバインディングであるpyvmomiを利用して、スクリプトから仮想マシンを操作することができます。しかし、pyvmomiのスクリプトを開発するため、vCenterの開発環境を用意する必要がありますが、構築には手間がかかります。そこで、今回はvCenterをシミュレートするvCenter Server Simulator (VCSIM)を試してみました。
※VCSIMはVMwareの公式サポートは受けられないため自己責任となります。

構成

インストール

以下のリポジトリを参考に導入を行ったのですが、Packerを使って、vCSAのovaファイルからVagrantで使うVirtualBox用のboxの生成することが上手くできなかったので手動でインストールしています。

vCenter Server Applianceのインポート

VMware社のサイトからvCenter Server ApplianceのOVAファイル (VMware-vCenter-Server-Appliance-5.5.0.5100-1312297_OVF10.ova) をダウンロードします。

VirtualBoxを起動し、ファイル>仮想アプライアンスのインポートをクリックします。

上記のovaファイルを選択し、開くをクリックします。

次へをクリックします。

アプライアンスの設定にて、RAMを8192MBから4096MBに変更し、インポートをクリックします。

vCenter Server Applianceを選択し、起動をクリックします。

コンソール画面に「Press any key and proceed with booting.」を表示されるので、適当なキーをクリックします。

起動が完了したことを確認し、DHCPで割り当てられたIPアドレスを確認します。

VCSIMの設定

起動したvCenter Serverにログインします。
※ID: root、PASS: vmware(vCSAのデフォルト設定)

gitコマンドをインストールします。

$ sudo zypper addrepo http://download.opensuse.org/repositories/devel:/tools:/scm/SLE_11_SP2/devel:tools:scm.repo
$ sudo zypper install git-core

以下のリポジトリを取得します。

$ git clone https://github.com/tehranian/vagrant-vcenter-simulator.git

以下のスクリプトを実行します。

$ cd vagrant-vcenter-simulator/scripts
$ chmod 700 vcenter-55-simulator-setup.sh
$ ./vcenter-55-simulator-setup.sh

VCSIMの構成は以下のファイルで指定しています。以下の構成では、クラスターが2個、クラスターごとにホストが8個、ホストごとにVMが8個の構成であるため、ホストが16個、VMが128個設定されることになります。

$ cat /etc/vmware-vpx/vcsim/model/initInventory-vagrant.cfg
<config>
  <inventory>
    <dc>1</dc>
    <host-per-dc>0</host-per-dc>
    <vm-per-host>0</vm-per-host>
    <poweron-vm-per-host>0</poweron-vm-per-host>
    <cluster-per-dc>2</cluster-per-dc>
    <host-per-cluster>8</host-per-cluster>
    <rp-per-cluster>8</rp-per-cluster>
    <vm-per-rp>8</vm-per-rp>
    <poweron-vm-per-rp>6</poweron-vm-per-rp>
    <dv-portgroups>0</dv-portgroups>
  </inventory>
  <worker-threads>2</worker-threads>
  <synchronous>true</synchronous>
</config>

vSphere Web ClientからVCSIMに接続すると以下のように見えます。

f:id:hidemium:20150510142235p:plain

動作確認

pyvmomiを使って、VCSIMに接続できるか確認してみます。pyvmomi community samplesを使えるDockerfileがあったため、こちらを利用します。

Dockerをインストール済みのUbuntuサーバにログインし、以下のようにDockerfileをビルドします。

$ git clone https://github.com/ozzyjohnson/docker-pyvmomi.git
$ cd docker-pyvmomi
$ sudo docker  build -t debian-pyvmomi .

以下のようにコンテナを起動します。

$ sudo docker run -t -i debian-pyvmomi /bin/bash

pyvmomi community samplesディレクトリに移動し、サンプルスクリプトを実行します。

# cd /pyvmomi-community-samples/samples
# python hello_world_vcenter.py -s <VCSIMのIPアドレス> -u root -p vmware
Hello World!

If you got here, you authenticted into vCenter.
The server is 192.168.xxx.xxx!
current session id: 5216d94f-21bc-9e49-1ce9-d6bcece2c165
Well done!


Download, learn and contribute back:
https://github.com/vmware/pyvmomi-community-samples

こちらはvCenter配下の全VMを確認しています。

# python getallvms.py -s <VCSIMのIPアドレス> -u root -p vmware
Name       :  DC0_C0_RP0_VM2
Path       :  [GlobalDS_0] DC0_C0_RP0_VM2/DC0_C0_RP0_VM2.vmx
Guest      :  Microsoft Windows Server 2003 Standard (32-bit)
Instance UUID :  5038f77b-ce04-6ca9-e481-89958a8ce3eb
Bios UUID     :  42381903-f65d-d3c4-9d9b-d7c3b25fefd3
State      :  poweredOn
:

おわりに

どこまで利用できるか確認中ですが、仮想マシンやデータストアを確認する参照系のスクリプトであれば問題はなさそうです。また、メトリクスの設定もできるようなので今後試してみたいと思います。なお、今回はVCSIMをPCで動作させている関係で、メモリを4GBまでしか割り当てができなかったため、vSphere Web Clientへの接続は不安定でした。

Play Frameworkでイベント支援サービスを作ってみました

id:hirahiro56id:nemorosus4160と一緒にPlay Frameworkを使ってWebサービスを作ってみました。

キャッチボールを目的に、告知ページ・申し込みフォームを作成するサービス「Chiball.net」です。

Chiball.net:http://chiball.net/

キャッチボールをする機会が減ってきて、キャッチボールできる場所ってどこにあるんだろうと思って調べてみると、あまり情報がまとまっているところが少なかったので、サービスとしてあれば便利かもと思い作ってみました。

機能として、以下のものが利用できます。

  • 告知ページ・申し込みフォームを作成
  • チャット機能
  • キャッチボール場所の検索

サービスを作ったときに使ったツールをメモしておくと、後々便利そうだったのでまとめてみました。

全体の構成

フレームワーク

Scala/Javaで書かれたWebアプリケーションフレームワークを利用しました。チャット機能を実現するために、Play FrameworkでWebSocketを使っています。

Play Frameworkの開発環境の構築やModelの使い方については以下で触れています。
※過去の記事のリンクになります。

Play Frameworkライブラリ

ログイン認証には、Play FrameworkのライブラリであるSecureSocialを利用しました。

Bootstrap系

見栄えを整えるためBootstrapを利用しました。BootstrapはレスポンシブWebデザインに対応しているため、スマホ対応も楽にできました。

バージョン管理

プライベートリポジトリを無料で利用できるため、Bitbucketを利用しました。WikiやIssueも利用できるので共通手順の整理や課題管理に利用することができました。

BitbucketではGitHub Flowを使いましたが、GitHub Flowついては以下で触れています。
※過去の記事のリンクになります。

フリー画像

画像はFlickrクリエイティブ・コモンズ画像を利用しました。

ロゴ作成

おわりに

機能としてはまだまだですが、無事リリースまでもっていくことができました。機能拡張やデプロイやテストの自動化などまだまだ試してみたいことが多く、今後の課題にしたいと思います。
また、作るのにあたってPlay Framework 2徹底入門を読みましたが、書籍の後半に実用的な記載が多くがとても参考になりました。

リポジトリ共有式Pull Requestの手順について

GitHubを使った開発の方法であるリポジトリ共有式のPull Requestの手順を整理してみました。

全体の流れ

大まかに以下のような手順になります。

  • ローカルにclone
  • ブランチ作成 (名前はissue[Issueの番号]とか※)
  • 修正完了後、Push
  • masterにプルリク作成
  • 確認したらmasterにマージ

※本家GitHub Flowでは説明的なブランチを作成するようですが。

ソースコードの取得

新たにcloneする場合

$ git clone <リポジトリ>  #ソースコードを取得

以前にcloneをしたことがある場合

$ git checkout master                     #最新のリモートリポジトリを取得
$ git branch -a                           #ブランチを確認
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
$ git pull     #手元のソースコードを最新化

開発作業の流れ

1. 以下のコマンドでブランチを作成し、 リモートにpushします。

$ git branch     現在のブランチを確認
* master
$ git checkout -b issue1               #Pull Request用ブランチを作成
$ echo "# Hello, Pull Request" > README.md     #見出しを変更
$ git add README.md                            #変更をadd
$ git commit -m "Update README.md"             #変更をcommit
$ git push origin issue1                #リモートにpush

2. リポジトリを開いて、Pull Requestsをクリックします。
3. New pull requestをクリックします。
4. ブランチを指定して、Create pull requestをクリックし、masterに対してプルリクを実行します。

Pull Requestの確認

1. 以下のコマンドでチェックアウトし、 動作を確認します。

$ git fetch                                #変更を取得
$ git branch -a                            #ブランチを確認
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
  remotes/origin/issue1
$ git checkout -b issue1 origin/issue1     #チェックアウト
#ここから動作確認

2. リポジトリを開いて、Pull Requestsをクリックします。
3. Mergeをクリックし、masterに対してマージを実行します。

ブランチの削除

マージが出来たら、もうブランチが不要になるので消しておきます。
リモートブランチの削除はプルリクエストの画面からもできます。

$ git checkout master
$ git branch -d issue1      #ローカルブランチの削除
$ git push origin :issue1   #リモートブランチの削除

マージ後にリモートブランチが消えてもgit branch -aに出てくる場合は、以下のコマンドを実行します。

$ git fetch --prune

最初に戻る

ソースコードの取得」に戻り、手元のソースコードを最新化します。

トピックブランチで作業中に、master ブランチで変更があった場合

作業中のトピックブランチにいる状態で以下のコマンドを実行します。

$ git merge master

Play Framework(Java)でEbeanを使ってManyToManyのリレーションを行う

この記事は Play framework Advent Calendar 2014の 14 日目です。

前回、Ebeanを使ってリレーションを持つテーブルの操作を試してみました。しかし、多対多のリレーションについて、更新時に関係付けしたインスタンスが削除される事象が発生し、しばらくはまってしまいました。今回は、その時に確認した内容についてまとめました。*1

構成

サンプル用のアプリについて

ソースコードhideakihal/play-ebean-relation · GitHubで公開しています。

ManyToManyを試すために以下のの2つのModelを用意しました。

  • Message・・・メッセージを管理
  • Member・・・ユーザ名を管理

models パッケージに,Message.javaとMember.javaを作成し、以下のように@ManyToManyのアノテーションを追加します。

Message.java

@ManyToMany(cascade = CascadeType.ALL)
public List<Member> members;

Member.java

@ManyToMany(mappedBy="members", cascade = CascadeType.ALL)
public List<Message> messages;

controller パッケージに,以下のようにApplication.javaとContacts.javaを作成します。

Application.java

Messageインスタンスを作成、更新するためにApplication.javaを用意します。CRUD処理で必要なcreateメソッド、updateメソッド、deleteメソッドは以下通りです。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		List<String> names = data.name;
		for (String nm : names){
			Member m = Member.findByName(nm);
			data.members.add(m);
		}
 		data.save();
public static Result update() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		List<String> names = data.name;
		for (String nm : names){
			Member m = Member.findByName(nm);
			data.members.add(m);
		}
 		data.update();
public static Result delete(Long id) {
	Form<Message> f = new Form(Message.class).fill(
		Message.find.byId(id)
	);
	if (!f.hasErrors()) {
		Message data = f.get();
		if(data != null){
			data.delete();

Contacts.java

Memberインスタンスを作成、更新するためにContacts.javaを用意します。CRUD処理で必要なcreateメソッド、updateメソッド、deleteメソッドは以下通りです。

public static Result create() {
	Form<Member> f = new Form(Member.class).bindFromRequest();
	if (!f.hasErrors()) {
		Member data = f.get();
		data.save();
public static Result update() {
	Form<Member> f = new Form(Member.class).bindFromRequest();
	if (!f.hasErrors()) {
		Member data = f.get();
		data.update();
public static Result delete(Long id) {
	Form<Member> f = new Form(Member.class).fill(
		Member.find.byId(id)
	);
	if (!f.hasErrors()) {
		Member data = f.get();
		if(data != null){
			data.delete();

サンプルアプリの実行画面

実行画面は以下のようになっています。MessageとMemberに対して作成、読み込み、更新、削除ができるCRUDのアプリになります。

以下は右サイドバーのMemberをクリックした画面になります。messagesには関係付けたMessageインスタンスのIDが表示されます。

f:id:hidemium:20141214100433p:plain

Member側からユーザ名のみを登録することができ、以下はMember>+addをクリックした画面になります。

f:id:hidemium:20141214100421p:plain

以下は右サイドバーのMesssageをクリックした画面になります。memberには関係付けたMemberインスタンスの名前が表示されます。

f:id:hidemium:20141214100341p:plain

Messsage側からはMemberを複数登録できることができ、以下はMesssage>+addをクリックした画面になります。

f:id:hidemium:20141214100358p:plain

問題になったこと

Message側からの作成、更新、削除は問題ありませんでしたが、Member側からMemberインスタンスの更新を行った際に、関係付けたMessageインスタンスが削除されてしまう事象が発生しました。

Model Create Update Delete Annotation
Message @ManyToMany(cascade = CascadeType.ALL)
Member × @ManyToMany(mappedBy="members", cascade = CascadeType.ALL)

画面イメージとして、以下はMember>+editをクリックした画面になります。
以下のようにMemberのpasswordを変更して更新を行います。

f:id:hidemium:20141214113111p:plain

Memberの一覧を見ると、messagesにてMessageのidが削除されていることが分かります。

f:id:hidemium:20141214113125p:plain

原因について

当初は、cascadeやmappedByといったアノテーションの指定方法に問題があるかと思い、いくつかバリエーションを試してみましたが、問題は解決しませんでした。

次に、Memberインスタンスの更新には、Contacts.javaのupdateメソッドを使用しているため、updateメソッド注目してどこでMessageインスタンスが消失するか確認しました。
updateメソッドでは、bindFromRequestでフォームから渡された値を保管するFormインスタンスを作成し、FormインスタンスからMemberインスタンスを取得しています。

public static Result update() {
	Form<Member> f = new Form(Member.class).bindFromRequest();
	if (!f.hasErrors()) {
		Member data = f.get();
		data.update();

このMemberインスタンスの値を確認したとろ、すでにMessageインスタンスが消失した状態となっていました。つまり、bindFromRequestはフォームに入力されてpostされたものしか取得されないため、関係レコードのオブジェクトは取得できないことが原因となっていました。当初は、bindFromRequestで関係レコードのオブジェクトも取得できると勘違いしていたため原因をつかむのに時間がかかってしまいました。

解決方法について

原因は判明しましたが、すでに関係付けたMessageインスタンスをどのように取得するか悩みました。例えば、View側で関係付けたMessageインスタンスを一旦IDにし、IDの配列として渡す方法など考えましたが、あまりいい方法とも思えませんでした。最終的に、MemberのIDからfindByIdでMemberインスタンスを取得することで、更新前に関係付けたMessageインスタンスが取得することが確認できたため、以下のような実装を行いました。

Contacts.java

public static Result update() {
	Form<Member> f = new Form(Member.class).bindFromRequest();
	if (!f.hasErrors()) {
		Member data = f.get();
		Member name = Member.findById(data.id.toString());
		List<Message> arr = name.messages;
		for(Message m : arr){
			data.messages.add(m);
		}
		data.update();

Member.findById(data.id.toString())で更新前に関係付けたMessageインスタンスを取得しています。また、cascade = CascadeType.ALLを使用しているため、messages側からaddするだけで自動的に更新を行うことができます。

Member.javaのfindByIdメソッドは以下のように作成します。

Member.java

public static Member findById(String input){
	return Member.find.where().eq("id", input).findList().get(0);
}

細かな実装については、hideakihal/play-ebean-relation · GitHubを確認してみてください。

おわりに

Ebeanを使ったManyToManyのリレーションについて、更新処理が上手くできずにはまっていましたが、なんとか解決することができました。今回の問題は、Stack Overflowに似たような問題が上がっていましたが、欲しい情報が見つからず解決に時間がかかってしまいました。
今回の記事がみなさんのお役に立てられればと思います。

*1:あくまで個人的に検証した結果であるため、正しい実装ではないかもしれません。よい方法があればどなかた教えてください。

Play Framework(Java)でEbeanを使ってリレーションを持つテーブルを操作する

前回、EBeanを使ったデータベースの操作を試してみましたが、Ebeanには複数のModelを関連付けて処理する仕組みも用意されています。Modelクラスのフィールドにアノテーションを付けることで、1対1、1対多、多対多といった関係付けを行うことができます。今回は、Ebeanを使ってリレーションを持つテーブルの操作について試してみました。

構成

Modelの連携の種類について

Modelの連携には以下の種類があります。

  • OneToOne(1対1)
  • OneToMany(1対多)、ManyToOne(多対1)
  • ManyToMany(多対多)

サンプル用のModelについて

それぞれの連携を説明するために、今回以下の2つのModelを用意しました。

  • Message・・・メッセージを管理
  • Member・・・ユーザ名を管理

まずはmodels パッケージに,以下のようにMessage.javaとMember.javaを作成します。

Message.java
package models;

import java.util.*;
import javax.persistence.*;
import javax.validation.*;
import com.avaje.ebean.*;
import play.db.ebean.*;
import play.data.validation.*;

@Entity 
public class Message extends Model {

  @Id
  public Long id;
  
  @Required
  public String name;

  @Required
  public String message;
  
  @CreatedTimestamp
  public Date postdate;
	
  public static Finder<Long, Message> find = 
    new Finder<Long, Message>(Long.class, Message.class);
}
Member.java
package models;

import java.util.*;
import javax.persistence.*;
import javax.validation.*;
import com.avaje.ebean.*;
import play.db.ebean.*;
import play.data.validation.*;

@Entity
public class Member extends Model {

  @Id
  public Long id;

  @Required
  public String name;

  @CreatedTimestamp
  public Date postdate;

  public static Finder<Long, Member> find = 
    new Finder<Long, Member>(Long.class, Member.class);
}

OneToOne

OneToOneは、あるModelのエンティティ1つに対して、別のModelのエンティティ1つが対応する関係です。

Message.java

Messageインスタンスに1つのMemberインスタンスが関連するようにするには、Message.javaに以下の内容を追加します。@OneToOneというアノテーションが、OneToOneの関係を指定するものになります。

@OneToOne(cascade = CascadeType.ALL)
public Member member;

エンティティと関連付けられたエンティティに対する処理を自動で行う場合はCascadeTypeを指定します。cascade = CascadeType.ALLを追加することで、参照や更新などのすべての操作に対して処理を自動で行うことができるようになります。この設定がない場合は、操作ごとにApplication.javaに処理を書く必要があります。

Member.java

ユーザ名からMemberインスタンスを取得できるように、Member.javaにfindByIdメソッドを追加します。

public static Member findById(String input){
	return Member.find.where().eq("id", input).findList().get(0);
}

Application.java

Application.javaに以下のようにfindByIdを追加します。findByIdでMemberインスタンスを取得し、saveによりMessageインスタンスに保存するだけで関連付けをすることができます。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		data.member = Member.findById(data.name);
		data.save();
		return redirect("/");
	} else {
		return badRequest(add.render("ERROR",f));
	}
}

Evolutionを実行すると、以下のDLLが自動的に作成されます。Message.javaに追加したmemberがmember_idとして作成されていることが分かります。

create table message (
  id                        bigint not null,
  name                      varchar(255),
  message                   varchar(255),
  member_id                 bigint,
  postdate                  timestamp not null,
  constraint pk_message primary key (id))
;

OneToMany、ManyToOne

OneToManyは、あるModelのエンティティ1つに対して、別のModelの複数のエンティティが対応する関係です。ManyToOneは、逆にあるModelの複数のエンティティが、別のModelのエンティティ1つが対応します。
Memberインスタンス複数のMessageインスタンスが関連するようにするには、Message.javaとMember.javaに以下の内容を追加します。@OneToMany、@ManyToOneというアノテーションが、OneToMany、ManyToOneの関係を指定するものになります。

Message.java

@OneToMany
public Member members;

Member.java

@ManyToOne(cascade = CascadeType.ALL)
public List<Message> messages;

public static Member findById(String input){
	return Member.find.where().eq("id", input).findList().get(0);

こちらでも、エンティティと関連付けられたエンティティに対する処理を自動で行うため、cascade = CascadeType.ALLを指定しておきます。

Application.java

Application.javaに以下のようにfindByIdを追加します。findByIdでMemberインスタンスを取得し、saveによりMessageインスタンスに保存するだけで関連付けをすることができます。
Messageインスタンスの操作しかしていませんが、Memberインスタンスも自動的に反映されるため処理を追加する必要はありません。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		data.member = Member.findById(data.name);
		data.save();
		return redirect("/");
	} else {
		return badRequest(add.render("ERROR",f));
	}
}

Evolutionを実行すると、DLLが自動的に作成されますが、OneToManyやManyToOneでは変化はありません。

ManyToMany

ManyToManyは、あるModelのの複数のエンティティに対して、別のModelの複数のエンティティが対応する関係です。
複数のMemberインスタンス複数のMessageインスタンスが関連するようにするには、Message.javaとMember.javaに以下の内容を追加します。@ManyToManyというアノテーションが、ManyToManyの関係を指定するものになります。Message.javaには、複数のMemberインスタンスを格納するために、配列を追加しました。

Message.java

public List<String> name;

@ManyToMany(cascade = CascadeType.ALL)
public List<Member> members;

Member.java

@ManyToMany(mappedBy="members", cascade = CascadeType.ALL)
public List<Message> messages;

public static Member findById(String input){
	return Member.find.where().eq("id", input).findList().get(0);

こちらでも、エンティティと関連付けられたエンティティに対する処理を自動で行うため、cascade = CascadeType.ALLを指定しておきます。
また、ManyToManyではエンティティの主従関係を明示的にするためmappedByを指定しています。mappedBy="members"は、Messageが主、Memberが従であることを示しています。また、mappedByを指定しない場合、エンティティと関連付けした状態でインスタンスを削除をすると参照整合性制約違反が発生するため、自動的に処理をするには指定したほうがいいかもしれません。

Application.java

MemberのIDの配列を取得し、それらのIDからfindByIdでMemberインスタンスを取得しています。取得した複数のMemberインスタンスをmembersにaddにより追加します。saveによりMessageインスタンスに保存し関連付けをすることができます。Messageインスタンスの操作しかしていませんが、Memberインスタンスも自動的に反映されるため処理を追加する必要はありません。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
    		List<String> names = data.name;
    		for (String nm : names){
    			Member m = Member.findById(nm);
    			data.members.add(m);
    		}
		data.save();
		return redirect("/");
	} else {
		return badRequest(add.render("ERROR",f));
	}
}

Evolutionを実行すると、以下のDLLが自動的に作成されます。MessageとMemberの関係テーブルがmessage_memberとして作成されていることが分かります。

create table message_member (
  message_id                     bigint not null,
  member_id                      bigint not null,
  constraint pk_message_member primary key (message_id, member_id))
;

Play Framework(Java)でEbeanを使ってデータベースを操作する

Play FrameworkにはEbeanというORMが標準搭載されており、Ebeanを継承したModelクラスを作成することでデータベースを操作することができます。今回は、Ebeanを使ったデータベースの操作ついて試してみました。

構成

データベース接続の準備

application.confの設定

H2でデータベースを利用するには、application.confの設定を行います。confフォルダ内のapplication.confが対象となります。
application.confに以下の内容を追加します。

db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play"
ebean.default="models.*"

上の2行で、JDBCドライバと、H2のメモリ上にデータが格納されるDBインスタンス、最後の1行で、EBeanを使用する設定を行っています。

モデルの作成

Modelクラスは、play.db.ebean.Modelというクラスとして用意されています。
app/models パッケージに,play.db.ebean.Model を継承したModelクラスを作成することで、Modelクラスを使用できます。app/models パッケージに,以下のようにMessage.javaを作成します。

package models;

import java.util.*;
import javax.persistence.*;

import play.db.ebean.*;
import play.data.format.*;
import play.data.validation.*;
import play.data.validation.Constraints.*;

@Entity 
public class Message extends Model {

  @Id
  public Long id;
  
  @Constraints.Required
  public String name;

  public String mail;

  @Required
  public String message;
  
  @CreatedTimestamp
  public Date postdate;
	
  public static Finder<Long, Message> find = 
    new Finder<Long, Message>(Long.class, Message.class);
}

Modelクラスには、@がついたコードがありますが、これはJavaオブジェクトとデータベースのテーブルの間の関係を定義するアノテーションというものです。
@Entityは、このクラスがエンティティであることを示します。
@idは、記述したフィールドをプライマリキーであることを示します。
@Requiredは、入力必須項目であることを示しています。
@CreatedTimestampは、作成時の時間を格納することを示しています。

コントローラーの作成

controllersパッケージの配下にApplication.javaに以下のようなcreateメソッドを作成します。bindFromRequestのところでフォームから渡された値を保管するFormインスタンスを作成しています。FormインスタンスからMessageインスタンスを取得し、saveを呼び出すことでインスタンスが保存されます。一見インスタンスを操作しているだけですが、実際にはEBeanによりデータベースのテーブルに必要な値が保存されます。
※routersやviewについての説明は省いています。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		data.save();
		return redirect("/");
	} else {
		return badRequest(add.render("ERROR",f));
	}
}

Evolutionの実行

Play frameworkはEvolutionというマイグレーションシステムを持っており、Modelを作成し、ブラウザからアクセスすると、Evolutionを実行するか確認する画面が表示されます。Evolutionを実行すると、自動的に作成されたDLLが実行されmessageテーブルを作成してくれます。DLLはconf/evolutions/default/1.sqlに作成され、messsegeテーブルのcreateは以下のように作成されます。

create table message (
  id                        bigint not null,
  name                      varchar(255),
  mail                      varchar(255),
  message                   varchar(255),
  postdate                  timestamp not null,
  constraint pk_message primary key (id))
;

デフォルトの状態では、モデル構成が変更される度にデータベースをクリーンする動作になっているため、この機能を停止するには1.sqlで以下の箇所を削除します。

# To stop Ebean DDL generation, remove this comment and start using Evolutions