hidemium's blog

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

VMware WorkstationにvSphere 6.5/Virtual SAN 6.5を構築する

この記事は NIFTY Advent Calendar 2016の 21 日目の記事です。

昨日は@licht110さんのserverspec+ansible+vagrant-vsphereでやるテスト駆動サーバー運用という記事でした。

ハイパーコンバージドインフラストラクチャが注目されていますが、VMwareでは、x86サーバを複数台束ねて仮想的なストレージを構築できるVirtual SANという機能を提供しています。
ただし、Virtual SANを試すためには、最低3台のホストを用意する必要があり、検証環境を用意するのも敷居が高くなっています。 今回は、自宅でもVirtual SANを試せるように、VMware Workstation上にオールフラッシュのVirtual SAN 6.5を構築する方法を紹介します。

構成

  • vCenter 6.5 (2vCPU、10GB MEM)
  • ESXi6.5 (Nested) (2vCPU、6GB MEM) x 4
  • Virtual SAN 6.5
  • VMware Workstation 11.1.4 *1
  • SSD (PC側)

インストール

仮想ネットワークの設定

Workstationの仮想ネットワーク エディタを開き、ホストオンリーのネットワーク(プライベートネットワーク)を2つ作成し、以下の用途とします。

名前 用途
VMnet1 Management Network、Virtual Machine Network、vMotion Network
VMnet2 VSAN Network

DNSサーバの構築

vCenterのデプロイで必須となるため、まずは、DNSサーバを用意します。
vSphere Web Clinentでの操作が必要となるので、今回はWindows Server 2012 R2をWorkstationにデプロイし、機能と役割からDNSサーバをインストールします。
DNSサーバの構築方法は省略しますが、vCenterのホスト名が名前解決できるように、DNSサーバに前方参照ゾーンと、Aレコードを追加しておきます。

Nested ESXiの構築

ESXi 6.5 Virtual Appliance is now availableにて、Nested_ESXi6.5のアプライアンスが提供されており、Nested_ESXi6.5_Appliance_Template_v1.ovaというファイルをダウンロードします。
アプライアンスの構成は以下のようになっています。

  • ESXi 6.5 OS
  • GuestType: ESXi 6.5
  • vHW 11
  • 2 vCPU
  • 6GB vMEM
  • 2 x VMXNET vNIC
  • 1 x 2GB HDD (ESXi Installation)
  • 1 x 4GB SSD (for use w/VSAN, empty by default)(SSDとしてラベル済み)
  • 1 x 8GB SSD (for use w/VSAN, empty by default)(SSDとしてラベル済み)

ダウンロードしたovaファイルをダブルクリックし、Workstationに追加します。
OSを起動する前に、VMの設定の変更を行います。VMの設定の変更は、VM>設定を開き、Intel VT-x/EPT または AMD-V/RVI を仮想化の有効化と、ネットワークアダプターにVMnet1、ネットワークアダプター2にVMnet2を設定します。

f:id:hidemium:20161220224503p:plain

VMの起動後に、DCUIにログインし、パスワードの追加(初期ではパスワードが空のため)と、Management NetworkのIPを追加します。
今回は、Virtual SANの検証をするため、Nested ESXiを4台用意します。

vCenter Server Appliance (VCSA)の構築

VCSAのisoファイル(VMware-VCSA-all-6.5.0-4602587.iso)をダウンロードし、isoファイルとしてマウントします。
マウントしたディスクのvcsa配下にVMware-vCenter-Server-Appliance-6.5.0.5100-4602587_OVF10.ovaというovaファイルがあるので、ovaファイルをダブルクリックし、Workstationに追加します。

OSを起動する前に、VCSAのVMのvmxファイルを開き、以下の内容を追記します。

guestinfo.cis.deployment.node.type = "embedded"
guestinfo.cis.appliance.net.addr.family = "ipv4"
guestinfo.cis.appliance.net.mode = "static"
guestinfo.cis.appliance.net.pnid = "vcenter65-1.lab.local"
guestinfo.cis.appliance.net.addr = "192.168.229.170"
guestinfo.cis.appliance.net.prefix = "24"
guestinfo.cis.appliance.net.gateway = "192.168.229.1"
guestinfo.cis.appliance.net.dns.servers = "192.168.229.10"
guestinfo.cis.appliance.root.passwd = "VMware1!"
guestinfo.cis.appliance.ssh.enabled = "True"
guestinfo.cis.deployment.autoconfig = "True"
guestinfo.cis.appliance.ntp.servers = "pool.ntp.org"
guestinfo.cis.vmdir.password = "VMware1!"
guestinfo.cis.vmdir.site-name = "default-site"
guestinfo.cis.vmdir.domain-name = "vsphere.local"
guestinfo.cis.ceip_enabled = "False"

VCSAのVMを起動すると、上記の設定が自動的に追加されます。VCSAの初回起動には時間がかかるので、しばらく待ちます。

なお、DNSサーバを用意しない場合、VCSAの初回起動時に以下のエラーが出ることがあり、はまりました。

Encountered an internal error. Traceback (most recent call last): File "/usr/lib/vmidentity/firstboot/vmidentity-firstboot.py", line 2017, in main vmidentityFB.boot() File "/usr/lib/vmidentity/firstboot/vmidentity-firstboot.py", line 349, in boot self.configureSTS(self.__stsRetryCount, self.__stsRetryInterval) File "/usr/lib/vmidentity/firstboot/vmidentity-firstboot.py", line 1478, in configureSTS self.startSTSService() File "/usr/lib/vmidentity/firstboot/vmidentity-firstboot.py", line 1140, in startSTSService returnCode = self.startService(self.__sts_service_name, self.__stsRetryCount * self.__stsRetryInterval) File "/usr/lib/vmidentity/firstboot/vmidentity-firstboot.py", line 88, in startService return service_start(svc_name, wait_time) File "/usr/lib/vmware/site-packages/cis/utils.py", line 784, in service_start raise ServiceStartException(svc_name) ServiceStartException: { "resolution": null, "detail": [ { "args": [ "vmware-stsd" ], "id": "install.ciscommon.service.failstart", "localized": "An error occurred while starting service 'vmware-stsd'", "translatable": "An error occurred while starting service '%(0)s'" } ], "componentKey": null, "problemId": null }


This is an unrecoverable error, please retry install.
If you run into this error again, please collect a support bundle and opne a support request.

起動した後で、VCSAにログインし、VCSAにNested ESXiを追加します。ホストを追加するとこのようになります。vSphere 6.5では、以前のバージョンと比べてWeb Clinetのナビゲーションの構成が変わっています。

f:id:hidemium:20161220230826p:plain

Virtual SANクラスタの構築

Virtual SANネットワークの設定

ESXiに2本NICが刺さっているので、vmnic0をvSwitch、vmnic1をVirtual SAN用のvDSとして構成します。

f:id:hidemium:20161220232442p:plain

さらに、各ESXiにVirtual SAN用のVMkernel Adapterを作成し、Virtual SAN用のvDSに所属させます。

f:id:hidemium:20161220232638p:plain

Virtual SANクラスタの有効化

Virtual SAN用クラスタのConfigure>Generalから、Configureをクリックし、Virtual SANの構成ウィザードを開きます。ストレージへのディスクの追加モードや、クラスタでデデュープおよび圧縮を有効にするオプション、クラスタのフォールトトレランスのモードがありますが、まずは初期設定のまま進みます。

f:id:hidemium:20161220233913p:plain

次に、Virtual SAN用ネットワークの検証が行われます。

f:id:hidemium:20161220234406p:plain

手動モードとしたので、クラスタごとに使用するディスクを確認します。4GB SSDはキャッシュ用、8GB SSDはキャパシティ用として利用します。

f:id:hidemium:20161220234435p:plain

構成ウィザードが終わると、Virtual SANの構成が開始されるので、タスクが完了するまで少し待ちます。

ストレージポリシーの作成

Virtual SANの設定が完了したので、次はストレージポリシーを作成していきます。Virtual SANでは、ミラー数やストレイピング数などをストレージポリシーによって定義し、VMに適用することで、VMごとのサービスレベルを設定することができます。

Policies and Profiles>VM Storage Prolicies>Virtual SAN Default Storage Policyを選択します。Manage>Rule-set1: VSANから、Editをクリックし、ストレージポリシーの設定画面を開きます。

f:id:hidemium:20161221000203p:plain

許容障害数(failures to tolerate = FTT)で、何台の物理ホスト障害に耐えらるか設定でき、今回は1としておきます。また、Add-ruleから障害の許可方法を追加し、RAID-5/6 Erasure Codingを選択します。イレージャコーディングを使用すると、ミラーリング (RAID 1) と同じレベルのデータ保護が可能であるのに加えて、使用するストレージ容量が少なくて済みます。

仮想マシンへのストレージポリシーの適用

ストレージポリシーが用意できると、仮想マシンを作成する段階で、ストレージポリシーを選択することができます。ここで先ほど作成した、ストレージポリシーを適用することで、ストレージポリシーに従ってFTTやRAIDが決定されます。

f:id:hidemium:20161221001114p:plain

Virtual SANの稼働状態の確認

最後に、Virtual SANで実際にどのように仮想マシンのデータが管理されているか確認しておきます。
Cluster>Monitor>Virtual SAN>Capacityを見ると、データの使用量を確認できます。クラスタでデデュープおよび圧縮を有効にした場合、下のようにDeduplication and Compression Overviewに圧縮しなかった場合と圧縮した場合のデータの使用量が見え、デデュープおよび圧縮により6GB->4GBとストレージの効率が良くなっていることが分かります。

f:id:hidemium:20161221001823p:plain

また、仮想マシン>Monitor>Policiesを見ると、仮想マシンのvmdkファイルがRAID-5で各ホストに分散されて管理されていることが分かります。

f:id:hidemium:20161221002624p:plain

最後に

WorkstationにvSphere 6.5 & Virtual SAN 6.5を構築してみましたが、一通りVirtual SANの設定ができ、VCSAの操作ももたつくことなく等の問題はありませんでした。Virtual SANの操作感や設定を試してみる方法として良いのではと思います。(ただ、メモリがとても必要なので潤沢にメモリを用意しましょう。)
今後は、障害時の動作など、色々試してみようと思います。

明日は@tsubasaogawaさんの記事です。お楽しみに。

参考

*1:ただし、メモリは32GB程度必要になります。

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))
;