2013年6月14日金曜日

VirtualBox で「なんちゃってステージング環境」のススメ

山本です。

負荷分散のある環境で試したいけれど、開発環境はマシン1台しかない!
でもステージング環境で試すのはまだ怖い…
そんなときは、 VirtualBox を使って手元にステージングもどき環境を作ってしまうといいかも知れません。

ポイント

  • 本番環境と似たサーバネットワークを、社内環境から隔離したかたちで手元に用意するのが目標
    ↑気軽に負荷分散環境での動作をシミュレート
  • 簡単にバーチャルマシンのコピーが作れるから、OSインストールなどの共通作業だけ先に済ませておける

事前設定

サーバネットワークに使うホストオンリーネットワークを作っておく。
Oracle VM VirtualBox マネージャー(システムツール/Oracle VM VirtualBox)から
「ファイル」→「環境設定」→「ネットワーク」タブ
→「+」(追加)アイコン
→「(ドライバーの絵)」(編集)アイコン
→IPアドレスとネットワークマスクを入力して「OK」
→「OK」

共通作業


OSインストール

(省略;ISOイメージを光学メディアとして直接接続できるので活用!)

事前ダウンロード

aptitude install -d パッケージ名… で、インストールする予定のあるパッケージをあらかじめダウンロードだけしておけるので、これも活用すると便利。

準備できたら

一旦仮想マシンをシャットダウンします。

個別マシン作成

元ネタの仮想マシンを右クリック→「clone」
  • 「Reinitialize the MAC address of all network cards」のチェックを忘れずに!
  • 「Full Clone」を選択←元ネタにクローン側で行った変更が伝染しないようにするため
クローン作成にはホストのディスクI/O性能によって相応の時間がかかります。
(山本のマシンだと、8Gのイメージをクローンするのに3分弱かかりました。)

起動する前にやること

ネットワーク設定(ハード?側)

できたクローンを右クリック→「設定」→「ネットワーク」タブ

  • 「アダプタ1」タブを選択 (こいつが仮想マシンの eth0; apt-get 用+ホストからのアクセス用)
    • 「割り当て」→「NAT」←ブリッジだと無線接続時うまく動かない
  • 「アダプタ2」タブを選択 (これは仮想マシンの eth1; 模擬サーバネットワークにつなげるため)
    • ネットワークアダプタを有効化
    • 「割り当て」→「ホストオンリー アダプタ」
    • 「名前」→事前準備で作ったホストオンリーネットワークを選択
      ↑これで模擬サーバネットワークにつなげる
    • 「高度」→「アダプタタイプ」→「準仮想化ネットワーク (virtio-net)」
      ↑パフォーマンス(10倍くらい違うらしいです)

いざ起動!

初回はネットワーク設定がされていないので、DHCPのタイムアウトで待たされる。
辛抱強く待ちましょう。

ネットワーク設定(ゲストOS側; Ubuntuゲストの例)

Ubuntu、というかいまどきのLinuxは、MACアドレスでインタフェース名を固定する機能があるが、クローンを作る場合はそれが障害になる。
初回起動時に待たされる原因のほとんどはこれが原因です。
  • /etc/udev/rules.d/70-persistent-net.rules 書き換え
    NAME="eth0" の行を削除
    NAME="eth1" の行を書き換え→ NAME="eth0" に
    NAME="eth2" の行を書き換え→ NAME="eth1" に
  • /etc/network/interfaces 追記
    auto eth1
    iface eth1 inet static
        address 10.0.0.11
        netmask 255.255.0.0
    ↑IPアドレスとネットワークマスクは適切なものに読み替えて
  • 再起動!

ミドルウェアその他のインストール&設定

個別にインストール and/or 設定が必要なものは各プロジェクトのインフラ情報を参考に。

その他

起動したままの状態で一時停止

仮想マシンのウィンドウを「閉じる」→「仮想マシンの状態を保存」を選択
→ハイバネーションに似た状態になります。

スナップショット

「在りし日の仮想マシン 」として、大規模変更前などに仮想マシンの状態を記録しておくことができます。
「Oracle VM VirtualBox マネージャー 」ウィンドウで、仮想マシンのアイコンを選択した状態で「スナップショット」タブを表示、カメラのようなアイコンを叩くとスナップショットが作成できます。
作ったスナップショットに仮想マシンの状態を巻き戻したり、クローンの仮想マシンを新たに起こしたりできます。

サーバネットワークのネットワーク経路が入らない?!

原因・再現性とも不明ながら、 eth1 のネットワークのための経路が入らない、もしくは何らかの理由で消えてしまうという現象がおこることがあるようです。
「ゲストOSからホストOSへの ping は通るのに、ゲストOS同士や、ホストからゲストOSへの ping が通らない」のような奇妙な現象として観測されることも。
手動でネットワーク経路を追加してやることで、とりあえず修復できます。
  • sudo route add -net 10.0.0.0/16 gw 10.0.0.11

2013年6月5日水曜日

Zend Framework 2とDependency Injection

高瀬です。

PHP歴約1年、まだYii Frameworkでしか開発出来ないので、少しは他の開発手法もやってみようと思う。そこで、ユニークビジョンでは馴染みがあるようなないようなZend Frameworkに注目。

Zend Frameworkについて


米Zend、PHPアプリフレームワーク「Zend Framework 2.0」をリリース (2012年9月7日)

バージョン1.0のリリースは2007年、2012年9月にバージョン2.0がリリースとのこと。2013年6月時点のバージョンは2.2。

数あるPHPフレームワークの中で、Zemd Frameworkの人気はどうだろうか。

PHPの4大フレームワークの人気を比較 (2013年1月21日)

4大フレームワークと呼ばれるものの中では、Zend Frameworkはいまひとつの様子。

さて、バージョンアップでどのように変わったのか。まずはZend Framework開発メンバーの方が作成されたこちらを参照。

Creating Re-usable Modules for Zend Framework 2 (2012年6月?)

これによると、3つのコンセプトがあるという。

Three core concepts
  • Decoupled and DI-driven (ServiceManager, Di)
  • Event-driven (EventManager)
  • HTTP handlers (DispatchableInterface)

英語で書かれているからということを差し引いても、よくわからない。では、少し観点が違うけれども、日本の方が書かれたこちらも参照。


結局よくわからないが、先ほどの資料と共通しているのは依存性の扱いが変わったことと、イベントという機能が追加されたことか。これは本家のFAQにも書かれている。


ここでは依存性の扱いについて見ていくことにする。

チュートリアルの実施


では、Zend Framework 2を使ってみよう。インストールは、スケルトンアプリケーションをダウンロードし、composerで関連モジュールを取り込む、という手順になる。チュートリアルのページを参照。

Getting started: A skeleton application

上記のページに、php composer.phar create-project --repository-url=... のコマンドでインストールするとあるが、LinuxとWindowsのどちらで試してもエラーが発生し、インストールができなかった。

しかたがないので、GithubからZendSkeletonApplication-master.zipをダウンロードし、解凍する。

解凍したら、composerで関連モジュールを取り込む。これにより、Zend Framework 2本体もインストールされる。

php composer.phar self-update
php composer.phar install

nginxの設定は以下のとおり。

server {
        root /usr/share/nginx/www/workspace/zend/public;
        index index.html index.htm index.php;
        server_name localhost;

        location / {
                try_files $uri $uri/ /index.php$is_args$args;
        }

        location ~ \.(php|phtml)?$ {
                fastcgi_param  PATH_INFO        $fastcgi_path_info;
                fastcgi_pass 127.0.0.1:9000;
                fastcgi_index index.php;
                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_param APPLICATION_ENV development;
        }
}

設定できたら、http://lcoalhost/ でアクセスする。スケルトンアプリケーションの画面が表示されたら成功。なお、nginxの設定の問題ではないかと思うが、http://localhost/index.php でアクセスすると、「Page not found.」のエラーとなった。

チュートリアル自体は、ソースコードをコピー&ペーストしながら読み進めていくと、データベースへのデータの登録、更新、取得、削除が一通りできる内容になっている。

「Using ServiceManager to configure the table gateway and inject into the AlbumTable」の手順で、データベースへのアクセスを行うTableGatewayやAdapterのインスタンスを用意しているあたりがDependency Injectionの書き方になっている。

チュートリアルで作成したプログラムが動作したら、Dependency Injectionについて調べてみよう。


Dependency Injection


自分がDependency Injection(以下、DI)という言葉を知ったのは最近だが、これは別に新しい技術ではなく、検索すると2005年ころの記事が多く見つかる。

Java開発を変える最新の設計思想「Dependency Injection(DI)」とは (2005年2月18日)

上記のページから一部抜粋。
----------
DIを実現するメカニズムの概要は、オブジェクト相互の依存性をプログラム外部に記述して、実行時に結合することである。これにより、オブジェクト間の独立性が高まる。

----------


さて、何のことだかわからないが、自分の認識では以下のような内容。


「ソースコードの中にクラス名を直接書いていると、そのクラスを拡張した派生クラスに差し替えようと思うとあちこち修正しなければならなかったり、開発環境と本番環境でクラスを切り替えたり、というようなことがやりにくい。そこで、ソースコード中には仮のクラス名を書いておき、実際にインスタンスを作るクラスの名前は設定ファイルに書くことにする。」



Zend Framework 2ではこれを以下のページの説明のように実現している。


簡単な例が最初に示されている。A、Bという2つのクラスがあったとして、Bはコンストラクタの引数にAのインスタンスを受け取る。すなわち、Bのインスタンスを生成する構文は以下のようになる。

$b = new B(new A());

newが2回も登場するので、Aを差し替えるときも、Bを差し替えるときもソースコードの修正が面倒そうだ。そこで、newを使わずにそれぞれのインスタンスを生成させてみよう。

クラスBのコンストラクタの定義部分で、引数に型を書いておく。

class B {
        public function __construct(A $a) { ...

そのうえで、以下のように記述する。

$di = new Zend\Di\Di;
$b = $di->get('B');

こうすると、$bにはBのインスタンスが代入されるが、その際に自動的にAのインスタンスも生成される。Zend\Di\Diクラスが必要なインスタンスの準備を肩代わりしてくれるのだ。少なくともこれで、クラスAの登場場面が減ったので、Aの差し替えが楽になる。

newが消えたとはいえ、Bの方は相変わらず直接名前が指定されている。$bに代入されるインスタンスの型を設定ファイルへ追い出すことができるかどうかはまだ勉強中。


実行環境による型の切り替え


前述の例では設定ファイルが登場しなかったが、生成されるインスタンスの型を設定ファイルに追い出してみよう。

BookStoreというモジュールに、StoreControllerというコントローラがあるとする。本屋さんが仕入先の出版社から本を入荷する、と考えてみる。

<?php
namespace BookStore\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Zend\Di\Di;
use Zend\Di\Config;

class StoreController extends AbstractActionController
{
    public function indexAction()
    {
        $di = $this->getDi();
        $purchase = $di->get('BookStore\Model\Purchase');

        return new ViewModel(array(
            'purchase' => $purchase,
        ));
    }

    protected function getDi()
    {
        // http://stackoverflow.com/questions/8957274/access-to-module-config-in-zend-framework-2
        $config = $this->getServiceLocator()->get('Config');

        // http://www.eschrade.com/page/zf2-dependency-injection-managing-configuration/
        $diConfig = new Config($config['di']);

        $di = new Di;
        $di->configure($diConfig);
        return $di;
    }
}

Purchaseクラスは仕入先を表し、以下のように定義する。

<?php
namespace BookStore\Model;

class Purchase
{
    protected $publisher;

    public function setPublisher($publisher)
    {
        $this->publisher = $publisher;
    }

    public function getPublisher()
    {
        return $this->publisher->getName();
    }
}

コンストラクタの引数にPublisherクラスのインスタンスを受け取る。Publisherクラスは以下のとおり。ついでにほぼ同様の内容でPublisherMockクラスも作っておく。

<?php
namespace BookStore\Model;

class Publisher
{
    protected $name;

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }
}


ここで、このプログラムを本番環境で動作させる時はPublisherクラスを使用し、開発環境の場合はPublisherMockクラスを使用したいとする。nginxから受け取っている環境変数APPLICATION_ENVで実行環境は判定。設定は config/module.config.php と config/module.config.develop.php に書くことにする。

まず環境変数APPLICATION_ENVの値により読み取る設定ファイルを切り替えるようにしてみる。というか、マージさせてみる。

Module.phpに、設定ファイルのパスを記述している部分がある。

class Module
{
    public function getAutoloaderConfig()
    {
        省略
    }

    public function getConfig()
    {
        return include __DIR__ . '/config/module.config.php';
    }

これを、以下のように変更。config/module.config.phpを読み取ったうえで、環境変数の値に対応するファイルがあったらそれのファイルの内容をマージする。

    public function getConfig()
    {
        $basePath = __DIR__ . '/config/';
        $config = include $basePath . '/module.config.php';
      $mode = $_ENV['APPLICATION_ENV'];

        if (!empty($mode)) {
            $filePath = $basePath . "/module.config.{$mode}.php";

            if (file_exists($filePath)) {
                $overwrite = include $filePath;
                $config = array_merge($config, $overwrite);
            }
        }
        return $config;
    }


それから、config/module.config.phpに以下の内容を追加する。

<?php
return array(
    'di' => array(
        'instance' => array(
            'BookStore\Model\Purchase' => array(
                'parameters' => array(
                    'publisher' => 'BookStore\Model\Publisher',
                ),
            ),
            'BookStore\Model\Publisher' => array(
                'parameters' => array(
                    'name' => 'Amazon',
                ),
            ),
        ),
    ),
    :

さらに、config/module.config.development.php には以下の内容を書いておく。

<?php
return array(
   'di' => array(
        'instance' => array(
            'BookStore\Model\Purchase' => array(
                'parameters' => array(
                    'publisher' => 'BookStore\Model\PublisherMock',
                ),
            ),
            'BookStore\Model\PublisherMock' => array(
                'parameters' => array(
                    'name' => 'Amazon',
                ),
            ),
        ),
    ),
);


以上のコードで、Purchaseクラスのインスタンスが作成されるとき、環境変数APPLICATION_ENVの値がdevelopmentでないときはコンストラクタの引数$publisherにPublisherクラスのインスタンスが注入され、developmentの場合はPublisherMockクラスのインスタンスが注入される。

ソースコードを1行も書き換えずに、環境変数の値だけで使用されるクラスを切り替えることができるようになった。

Zend Frameworkのサイトの説明だけでは分かりにくかったが、以下のページが参考になった。



終わり


正直なところ、DIがどのような場面で活躍するのか今ひとつ想像できていない。逆にソースコードの流れが読み取りにくくなりそうで少々心配だが、それは自分が大規模なシステム開発をしたことがないためか。

Zend Frameworkはまだチュートリアルくらいしか触っていないのでなんとも言えないが、DIやREST用コントローラ、コントローラの単体テストなどYiiでは提供されていなかったり、標準機能ではなかったりするものがあって、いろいろ勉強できそう。

またPHPの4大フレームワークと呼ばれるものを一つも知らないし、最近はFuelPHPやLaravelなどの方が有名らしいので、順番に見ていき、比較ができるようになれればよいと思う。

2013年5月1日水曜日

Slim Frameworkについて

青柳です。
Slim Frameworkについて紹介します。

○機能

ホームページで紹介されている機能は以下の通りです。
■強力なルーター
・標準と拡張できるHTTPメソッド
・ワイルドカードや条件と一緒に使えるルートパラメーター
・リダイレクト、パス、停止
・ルートミドルウェア
■拡張できるビューとテンプレートの表示
■フラッシュメッセージ
■AES256暗号化した安全なクッキー
■HTTPキャッシュ
■拡張できるログライター
■エラーハンドリングとデバッグ
■ミドルウェアとフックアーキテクチャ
■簡単な設定

○インストール
・フォルダー構成
app
  - composer.json
  - composer.lock
  - lib
    - Controller
    - Model
  - public
    - index.php
  - templates
  - vendor

・composer
vi composer.json
{
  "require": {
    "slim/slim": "2.*"
  }
}
php composer.phar install

・index.php
mkdir public
cd public
vi index.php
<?php
require 'vender/autoload.php';
$app = new \Slim\Slim();
$app->get('/hello/:name', function ($name) {
  echo "Hello, $name";
});
$app->run()
・nginx

server {
  server_name slim.example.com;
  listen 80;
  root /var/www/app/slim/public;
  index index.php index.html index.htm;
  location / {
    try_files $uri $uri/ /index.php?$args;
  }
  location ~ \.php$ {
    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    include fastcgi_params;
    fastcgi_param  PATH_INFO        $fastcgi_path_info;
    fastcgi_index index.php;
    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    fastcgi_pass 127.0.0.1:9000;
  }
}

○拡張
素のSlimにはあまり機能がありません。
目的に応じて機能を追加します。

・ビュー
HTMLを構築することが多いようならSmartyがおすすめ。
以下のパッケージを追加します。

"slim/extras": "2.0.*",
"smarty/smarty": "3.1.*",


vi index.php

\Slim\Extras\Views\Smarty::$smartyDirectory = '../vendor/smarty/smarty/distribution/libs';
\Slim\Extras\Views\Smarty::$smartyCompileDirectory = '../templates_c';
\Slim\Extras\Views\Smarty::$smartyTemplatesDirectory = '../templates';
\Slim\Extras\Views\Smarty::getInstance()->escape_html = true;
$app = new \Slim\Slim([
    "templates.path" => "../templates",
    'view' => new \Slim\Extras\Views\Smarty,
]);

・ログ
fluentdにログを書くにはこれ。
"aoyagikouhei/slim-fluent-logwriter": "0.0.*",

vi index.php

$writer = new \Slim\FluentLogwriter(['tag' => 'mongodb.system_log', 'level' => \Slim\Log::INFO]);
$writer->addFluent(['tag' => 'mail.system_log', 'level' => \Slim\Log::WARN]);
$app = new \Slim\Slim([
    'log.writer' => $writer,
]);

・セッション
MongoDBにセッションを保存します。
"aoyagikouhei/mongo-session-handler": "0.0.*",

vi index.php

\MongoSession\Handler::initSession([
  'server' => 'mongodb://127.0.0.1:27017'
  ,'db_name' => 'test'
  ,'write_options' => ['w' => 1]
]);

○コントローラー
規模が大きくなってくると、index.phpに全てのコードを書くと量が多くなり読みにくいコードになります。共通化するのも関数でしかできなくて不便です。
そこでコントローラークラスを導入することで、ベースクラスやポリモルフィズムやtraitなどの共通化行いやすくなります。

まずindex.phpの先頭でコントローラークラスを自動で読み込んでもらえるようにAutoloaderに登録します。

<?php
$loader = require "../vendor/autoload.php";
$loader->add('Controller', '../lib');
$loader->register();

次にBaseControllerです。
<?php
namespace Controller;
class BaseController
{
    protected $app;
    public function __construct($app)
    {
        $this->app = $app;
    }
}

各コントローラーを定義します。
<?php
namespace Controller;
class Member extends \Controller\BaseController
{
    public function __construct($app)
    {
        parent::_construct($app);
        $this->app->get('/member/login'), function () {
            $this->login();
        });
    }
    private function login() {
        echo "hi";
    }
}
ルート設定はコンストラクターで行います。無名関数では宣言無しで$thisが使えるのが便利です。

○モデル
モデルもあると便利です。
自前で容易するのもいいですがMongoDBを使うのならmandangoがおすすめです。

"mandango/mondator": "dev-master",
"mandango/mandango": "dev-master",

ジェネレーションギャップパターンで空の派生クラスが生成されるので、追加のコードはそこに書けば再生成し直しても問題ありません。



ØMQ(zeromq)について

青柳です。
メッセージキューを使おうかと思って調べていたら面白そうなキューのライブラリがあったので試してみました。

○メッセージキューとは
メッセージキュー
メッセージキュー(英: Message queue)は、プロセス間通信や同一プロセス内のスレッド間通信に使われるソフトウェアコンポーネントである。制御やデータを伝達するメッセージのキューである。
[Wikipeida]

MQ【メッセージキューイング】
アプリケーションソフト間でデータを交換して連携動作させる際に、送信するデータをいったん保管しておき、相手の処理の完了を待つことなく次の処理を行う方式。
[e-Word]

メッセージ指向ミドルウェア (MOM)
イラスト参考
[Oracle]

MOMとは
MOMとは、異なるプラットフォーム間でアプリケーション同士が双方向に情報をやり取りするためのソフトウェアのことである。
[Binary]

MOM(メッセージ指向ミドルウェア)の存在意義って何?
日本ではMOMがあまり活用されていないようですが、MOMを使う理由はどこにあって、どうしてあまりはやらないのでしょうか。
[togetter]

○ØMQとは
本家
The Intelligent Transport Layer
[ØMQ]

ØMQ (ZeroMQ) 序論
異なったソケットタイプ,接続処理,フレーミング,さらにはルーティングといった低レベルな詳細事項を,いくらかでも抽象化できたら素晴らしいとは思わないでしょうか? ZeroMQ (ØMQ/ZMQ) ネットワークライブラリは,まさにそのためのものです。"このライブラリはメッセージ全体をインプロセスや IPC,TCP,マルチキャストなど,さまざまなトランスポートを越えて送信できるソケットを提供します。ファンアウト,PubSub,タスク分散,要求/応答などのパターンによる N 対 N の接続が,ソケットを使って可能になるのです。"
[InfoQ]

ØMQ(zeromq)について調査する。

N-N通信を実現する、socket API風軽量メッセージングライブラリ。
自動的な再接続や、メッセージのキューイングを行ってくれる。
複数のメッセージングパターンと呼ばれるものを組み合わせることによって、柔軟なメッセージ配信を行うことができる。
[グニャラくんのwktk運営日記]


ActiveMQ or RabbitMQ or ZeroMQ or ...
どうやら ZeroMQ はシンプルで高速、RabbitMQ は割と高速でスケーラビリティが高い、ActiveMQ は遅いけど機能豊富といった感じらしい。ま、ActiveMQ は JMS 実装だしね……ESB 向きなんだろうなぁ。
[wivlog]

システム間連携 その4:ZeroMQ
ZeroMQを用いる事により、Berkeley socketsと同様のコーディングでありながら、Berkeley socketsで提供されずユーザーが実装しなければならなかった障害対応等の実アプリケーションで必須な機能が使用でき、簡単にシステム間連携が実現できます。
[TeckSketch]

○Rubyでの実装
■request/response
response.rb
#coding: utf-8
require 'ffi-rzmq'
context = ZMQ::Context.new(1)
# Socket to talk to server
socket = context.socket(ZMQ::REP)
socket.bind("tcp://127.0.0.1:5555")
while true
  msg = ''
  res = socket.recv_string(msg)
  puts "recive message " + msg
  socket.send_string(msg + " World")
end
request.rb
#coding: utf-8
require 'rubygems'
require 'ffi-rzmq'
context = ZMQ::Context.new(1)
# Socket to talk to server
puts "Connecting to hello world server…"
requester = context.socket(ZMQ::REQ)
requester.connect("tcp://127.0.0.1:5555")
0.upto(9) do |request_nbr|
  puts "Sending request #{request_nbr}…"
  requester.send_string ARGV[0]
  reply = ''
  rc = requester.recv_string(reply)

  puts "Received reply #{request_nbr}: [#{reply}]"
end 
■publish/subscribe
publish.rb
#coding: utf-8
require 'ffi-rzmq'
context = ZMQ::Context.new(1)
pub = context.socket(ZMQ::PUB)
pub.bind("tcp://127.0.0.1:5555")
0.upto(9) do |i|
  puts "Sending #{i}…"
  pub.send_string i.to_s
  sleep(1)
end
pub.close
context.terminate 
susbcribe.rb
#coding: utf-8
require 'ffi-rzmq'
context = ZMQ::Context.new(1)
sub = context.socket(ZMQ::SUB)
sub.setsockopt(ZMQ::SUBSCRIBE, '')
sub.connect("tcp://127.0.0.1:5555")
while true
  msg = ''
  res = sub.recv_string(msg)
  puts ARGV[0] + " " + msg
end
■push/pull
push.rb
#coding: utf-8
require 'ffi-rzmq'
context = ZMQ::Context.new(1)
push = context.socket(ZMQ::PUSH)
push.bind("tcp://127.0.0.1:5555")
0.upto(9) do |i|
  puts "Sending #{i}…"
  push.send_string i.to_s
  sleep(1)
end
push.close
context.terminate
pull.rb
#coding: utf-8
require 'ffi-rzmq'
context = ZMQ::Context.new(1)
pull = context.socket(ZMQ::PULL)
pull.connect("tcp://127.0.0.1:5555")
while true
  msg = ''
  res = pull.recv_string(msg)
  puts ARGV[0] + " " + msg
end
■pipeline
task.rb
#coding: utf-8
require 'ffi-rzmq'
context = ZMQ::Context.new(1)
push = context.socket(ZMQ::PUSH)
push.bind("tcp://127.0.0.1:5555")
pull = context.socket(ZMQ::PULL)
pull.bind("tcp://127.0.0.1:5556")
th_push = Thread.new do
  0.upto(9) do |i|
    puts "Sending #{i}…"
    push.send_string i.to_s
    sleep(1)
  end
  push.close
end
th_pull = Thread.new do
  while true
    msg = ''
    res = pull.recv_string(msg)
    puts "Recv " + msg
  end
end
th_push.join
th_pull.join
context.terminate
worker.rb
#coding: utf-8
require 'ffi-rzmq'
context = ZMQ::Context.new(1)
pull = context.socket(ZMQ::PULL)
pull.connect("tcp://127.0.0.1:5555")
push = context.socket(ZMQ::PUSH)
push.connect("tcp://127.0.0.1:5556")
while true
  msg = ''
  res = pull.recv_string(msg)
  back = ARGV[0] + " " + msg
  puts back
  push.send_string(back)
end
■まとめ
非同期の仕組みが驚くほど簡単に実装できます。
大量のクロールを走らせるような仕組みを構築するのに使えそうです。
ただし、キューはメモリ上で行われるためメッセージが確実に処理されたかどうかの保証がありません。
クロールを作ろうとすると実際にクロールされたかどうか、現在クロール中なのかどうかをDBなどで管理する必要がありそうです。


2013年4月24日水曜日

AWS whitepaper “Using AWS for Disaster Recovery” を読み解く

山本です。
AWS のホワイトペーパー Using AWS for Disaster Recovery を読み解いてみます。
(注:このホワイトペーパーは既存のクラウド上でないシステムの存在を前提にしているフシがある)

キーワード

一般用語

Recovery Time Objective, RTO
災害から回復までの所要時間とサービスレベルの目標。 objective(目標)であって mandate(指令)ではないところがポイント。 RTOを満足しない作戦をとることもありうるし、その場合でもRTOは目標として堅持すべき。
Recovery point objective, RPO
許容可能なデータロス期間の上限目標。 例えば、RPOとして4時間を設定したとすると、日次バックアップは適切な手段ではないことになる。

AWS用語

リージョン
国くらいの粒度の地域単位。AWSでは往々にしてリージョンをまたぐような操作に壁がある。
アベイラビリティ・ゾーン, AZ
リージョンの中に複数個存在。地理的に離れていて独立性が高い、ということになっている。 普段目にするのは論理AZであって物理AZではないため、アカウントをまたぐとゾーン名が一緒でも物理的には違うなどということがあるらしい。 (ソース)
CloudFormation
インスタンスの作成などをテンプレート化して自動化するための仕掛け。(今のところ、UVでこれを活用したことはない)

災害復旧シナリオ例

Backup and Restore

昔なら定期的にテープにフルダンプをとってオフサイト送り、となるところ。
S3がテープメディアの代わりに。EBSのスナップショットも有効。

バックアップで話は終わりじゃない!

リストアまでがシナリオです。
  • バックアップに使うツールは適切か?
  • データ保持(期間)のポリシーは適切か?
  • セキュリティ対策は?
  • 作ったバックアップ からの復元のテストを定期的に!

Pilot Light(種火)

最重要なコア要素をAWSで動かすようにする。
災害時にはこのコア要素に取り巻きを立て直すことで復旧とする。

準備

データはパイロットライト系統にレプリケーション。
OSのようなあまり更新頻度の高くないようなものは定期的にAMI(マシンイメージ)を更新しておく。

復旧

水平展開によるスケーリングの方がオススメ。インスタンスタイプを上位に変更してのスケールアップ手法もとれないことはない。
復旧が一段落したら、冗長性を速やかに取り戻すべし。

キーポイント

  • アプリケーションサーバのAMIを作って、そこからすぐ立ち上げられるように
  • 必要に応じてスケールアップ
  • 災害時はDNSをいじってAWS側を向くように
  • AMIベースでない要素の構成を忘れずに、理想的には自動化

Warm Standby

Pilot Light 方式の拡張。 インスタンスタイプその他を必要最小限に抑えた一式をAWSに用意する方式。
災害時にはこちらをスケールアップして負荷を捌く。

Multi-Site

平時はオンプレミスとAWS上のシステムを同時に動かしておく方式。

データレプリケーションの方式

  • 同期(Multi-AZな AWS RDB はこの構成になる)
  • 非同期(バックアップ用や参照系のユースケースならこれで充分なことが多い)
レプリケーション方式について理解しておくことが推奨されている。

災害復旧プランを向上するには

テスト!

いざ障害という時に復旧プロセスが可能な限りシンプルにできるように、復旧マニュアルが不足ないかを見る。

モニタリングとアラート(監視系)

バックアップは継続的に

アクセス制御

自動化

2013年4月6日土曜日

AndroidでのRESTクライアント(GETのみ)


高瀬です。

今回はAndroidアプリを作ってみる。目指すものは、Webサーバからデータを取得し、それを画面に表示する、という機能の実現とする。

アプリとWebサーバとの通信はRESTの形で行う。アプリ側でのRESTの実装にはSpring for Androidを利用。このSpring for Androidを試してみることが本題なのだが、RESTのサーバを準備したり、JUnitでAndroidアプリのテストなどもやってみる。

ソースコードはGitHubのandrestリポジトリを参照。


RESTについて


自分はRESTが何なのか分かっていないので、まず用語辞典を読んでみる。

RESTとは
http://e-words.jp/w/REST.html

やっぱり何だかよく分からないが、自分の認識を書くと以下のとおり。

  • URLが一つのリソースを表す。
  • リソースへの操作の種類をHTTPメソッド(GET=取得、POST=新規追加、PUT=更新、DELETE=削除)で表す。
  • レスポンスはJSON形式で送信する。

上記の用語辞典のページに書かれているとおり、RESTは別にHTTP通信に限ったものでもないし、レスポンスがJSONでなくてはいけないわけでもないが、今のところはこのくらいの内容で理解しておくことにする。

ここではサーバからのデータの取得のみを実装する。


サーバ側


サーバ側はPHPのYiiフレームワークで用意する。RESTでの実装を補助してくれるエクステンションがあるので、これを利用してみる。

RESTFullYii
http://www.yiiframework.com/extension/restfullyii/

このエクステンションでは、ERestControllerクラスの派生クラスとしてコントローラを作成することで、GETやPOSTに対応する処理を書くことができるようになっている。また、URLとリソースを結びつけるURLフォーマットも用意されている。

作成したのはPostControllerクラス。エクステンションのソースコードに含まれているREADME.mdの説明にしたがって、doRestList()、doRestView()メソッドを記述した。

doRestList()は、ID指定なしでGETメソッドによりアクセスされた場合に呼び出される。ここでは8件の記事のデータを返すようにした。

  $data = array(
    array(
      "post_id" => 1,
      "time" => "3月3日 12時42分",
      "title" => "Evernoteが不正アクセス被害"
    ),
    array(
      "post_id" => 2,
      "time" => "3月4日 18時3分",
      "title" => "警戒区域で初 ストビュー撮影"
    ),
    :
  );

  $this->renderJson($data);

記事の内容は2013年3月4日のYahoo!JAPANのニュースから拝借。

doRestView()は、ID指定ありでGETメソッドによりアクセスされた場合に呼び出される。ここでは、8件の記事のうち、IDが指している番号の記事の内容を返すようにした。上記の一覧取得では各記事の内容をpost_id、time、titleとしているが、こちらではこれらに加えてcontentを返すようにしている。例えば、1が指定された場合は以下の内容を返す。

  array(
    "post_id" => 1,
    "time" => "3月3日 12時42分",
    "title" => "Evernoteが不正アクセス被害、全ユーザーのパスワードをリセット",
    "content" => "米Evernoteは2日、同社のシステムに何者かが不正アクセスしたことを公表した。 ..."
  ),

アプリ側ではこれらのデータをJSON形式で受け取り、解析する。

なお、この後登場するSpring for AndroidのRestTemplateが、HTTPレスポンスのタイプがapplication/jsonであることを期待しているので、HTTPヘッダのContent-Typeにapplication/jsonをセットしている。


アプリで通信をする際の準備


通信を行うアプリを開発する場合、以下の2点に注意する必要がある。

  1. パーミッションandroid.permission.INTERNETを許可する。
  2. 通信処理はUIスレッド(メインスレッド)で実行してはならない。

参考:
Androidアプリでインターネット接続する為に必要な設定(android.permission.INTERNET)
http://feedyomi.blog32.fc2.com/blog-entry-181.html

android.os.NetworkOnMainThreadExceptionエラーへの対応方法
http://garnote.com/2012/10/android-os-networkonmainthreadexception.html

パーミッションの許可を指定しないと、実行時に以下の例外が発生した。

I/O error: socket failed: EACCES (Permission denied);
nested exception is java.net.SocketException: socket failed: EACCES (Permission denied)

UIスレッドで通信をしようとすると、上記参考ページのとおりNetworkOnMainThreadExceptionが発生する。

パーミッションについては、参考ページのとおりAndroidManifest.xmlでパーミッションの指定をすればよい。

通信処理を行うスレッドについては、AsyncTaskクラスなどを使用してワーカースレッドを作成する必要がある。

参考:
AsyncTask を利用した非同期処理
http://android.keicode.com/basics/async-asynctask.php

AsyncTaskLoaderを利用した非同期処理を行う
http://techbooster.org/android/application/13492/

時代は AsyncTask より AsyncTaskLoader
http://archive.guma.jp/2011/11/-asynctask-asynctaskloader.html

AsyncTaskLoaderの方が便利だが、対象OSバージョンがAndroid 3.0以降であること、コールバックメソッドが必ずActivityになければならないことから、少々使いにくい印象がある。そこで、AsyncTaskを使用して、非同期処理完了時に呼び出されるハンドラを指定できるようにしてみた。

ハンドラ用にOnFetchListenerクラスを定義。インスタンスを生成したら、ハンドラとなるメソッドを定義する。そして、AsyncTaskクラスのインスタンス生成時にハンドラを渡しておく。

これなら、一つのActivityで複数の非同期処理を実行したい場合に、ハンドラを個別に指定することができる。

非同期処理の準備ができたら、いよいよ通信処理を実装していく。


Spring for Android


ここからが本題。AndroidでのHTTP通信はorg.apache.http.client.HttpClientを使用してもよいが、RESTを簡単に実装できるという、Spring for Androidを使ってみることにする。

Spring for Android | SpringSource.org
http://www.springsource.org/spring-android

Jackson JSON Processor
http://jackson.codehaus.org/

Spring for Androidでは、HTTPメソッドGET、POST、PUT、DELETEにそれぞれ対応する、getForObject()、postForObject()、put()、delete()というメソッドが用意されている。使い方は、delete()ならURLのみ、それ以外はURLとパラメータを指定して呼び出すだけである。

マニュアルにしたがって、まずはインストールから。

Spring for Androidのライブラリ(spring-android-rest-template-{version}.jarとspring-android-core-{version}.jar)をダウンロードしたら、以下の手順(Spring for Android Reference Documentation から抜粋)でプロジェクトに組み込む。

  1. Refresh the project in Eclipse so the libs/ folder and jars display in the Package Explorer.
  2. Right-Click (Command-Click) the first jar.
  3. Select the BuildPath submenu.
  4. Select Add to Build Path from the context menu.
  5. Repeat these steps for each jar.

libsフォルダに各jarファイルをコピーしたら、Eclipse上で右クリックし、「Add to Build Path」でビルドパスに追加。これでRestTemplateが使用できるようになる。

ついでに、JSONの解析にJacksonというライブラリを使用するので、同様にプロジェクトに組み込んでおく。ダウンロードは上記の参照ページから。


一覧表示


記事の一覧を取得する。記事データの構造は前述のとおり、post_id、time、title、contentの4つの要素で構成されている。まずはこの構造に合わせたクラスを用意しておく。一覧取得ではcontentは使用しないが、単一記事と共用で使用できるクラスとするために含めている。


public class Post {
  private Long post_id;
  private String time;
  private String title;
  private String content;

  public String getPost_id() {
    return this.post_id.toString();
  }

  public void setPost_id(Long post_id) {
    this.post_id = post_id;
  }

  public String getTime() {
    return this.time;
  }

  public void setTime(String time) {
    this.time = time;
  }

  public String getTitle() {
    return this.title;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public String getContent() {
    return this.content;
  }

  public void setContent(String content) {
    this.content = content;
  }
}


そして、AsyncTaskを継承したクラスPostListを作成。以下が記事一覧を取得する部分。

  private ArrayList<HashMap<String, String>> data;
    :

  protected Long doInBackground(String... params) {
    // TODO Auto-generated method stub
    RestTemplate rest;
    String url = this.context.getString(R.string.post_url);

    // Create a new RestTemplate instance
    rest = new RestTemplate();

    try {
      Log.i("App", "start");
      // Add the String message converter
      rest.getMessageConverters().add(new MappingJackson2HttpMessageConverter());

      Post posts[] = rest.getForObject(url,  Post[].class);

      this.data.removeAll(null);
      for (int i=0; i<posts.length; i++) {
        HashMap<String, String> map = new HashMap<String, String>();
        map.put("post_id", posts[i].getPost_id());
        map.put("time", posts[i].getTime());
        map.put("title", posts[i].getTitle());
        this.data.add(map);
      }
      Log.i("App", "end");
    }
    catch (Exception e) {
      Log.e("App", e.getMessage());
    }
    return null;
  }

getForObject()でサーバへ記事の一覧を要求すると、結果がPostクラスの配列で得られる。これをListViewにセットできる形にするため、メンバー変数dataに保存した。


アクセスするURLはres/values/params.xmlファイルに定義している。


<resources>
  <string name="post_url">http://hostname/path/index.php/api/post/</string>
</resources>

api/post/ にGETでアクセスすることで、サーバ側のPostController、doRestList()が呼び出され、記事の一覧取得が行われる。


単一記事表示


単一記事を取得する場合も、一覧取得とほぼ同様の処理となる。ソースはPostDetailクラスを参照。

getForObject()の結果は、取得される記事は1件だけなので、Postクラスのインスタンスが1つだけ返される。

またアクセスするURLは api/post/1 などのように記事のIDを付加している。これによりサーバ側ではdoRestView()メソッドが呼び出され、該当の記事の取得が行われる。


JUnit


サーバからのデータ取得と画面表示ができたら、テストコードも書いてみる。Androidでのテストについては以下のページを参照。

Androidアプリ開発でテストを始めるための基礎知識
http://www.atmarkit.co.jp/fsmart/articles/androidtest01/01.html

Testing Fundamentals
http://developer.android.com/tools/testing/testing_android.html

さらに、非同期処理の部分については以下も参照。

AsyncTaskをJUnitでテストする方法
http://wavetalker.blog134.fc2.com/blog-entry-68.html

テストはプロジェクトを「Android Test Project」で作成する。ActivityのテストをするにはActivityInstrumentationTestCase2の派生クラスを作成する。

上記@ITの記事では見つけられなかったが、ActivityInstrumentationTestCase2の派生クラスを作る際、引数なしのデフォルトコンストラクタがないとテストが実行されなかった。デフォルトコンストラクタは自分で記述する必要がある。

AndrestcliActivityTestクラスを作成。以下のテストを行う。


  • 記事の一覧が取得できること。
  • 取得した記事の件数が一致すること。
  • リストビューのアイテム数が記事の数と一致すること。



  public void testGetPostList() throws Exception {
    // 非同期処理完了時のハンドラーを定義
    OnFetchListener handlerForTest = new OnFetchListener() {
      @Override
      public void onFetch(Context context, AsyncBase asyncTask) {
        // unlock testcase's thread.
        signal.countDown();
      }
    };
    // create subclass of test target asynctask.
    PostList posts = new PostList(activity, handlerForTest);
    // execute asynctask.
    // AndrestcliActivityのOnCreate()で実行されているものとは別にもう一度実行する
    posts.execute("");
    // wait for asynctask.
    signal.await(30, TimeUnit.SECONDS);
    PostList result = activity.getPostList();
    assertNotNull("記事の一覧が取得できること。", result);
    assertEquals("取得した記事の件数が一致すること。", 8, result.getData().size());
    ListView listView1 = (ListView)activity.findViewById(R.id.listView1);
    assertNotNull("リストビューが取得できること。", listView1);
    assertEquals("リストビューのアイテム数が記事の数と一致すること。", 8, listView1.getCount());
  }

サーバのモックをしていないので、サーバが動作していないとテストに失敗してしまうが、とりあえずこれでテストの実行を確認することができる。実施しているのは、記事の一覧が取得できることと、それをListViewで表示できていること、としている。

実は、テスト対象のAndrestcliActivityはOnCreate()で記事一覧の取得を行っており、テストコードでも記事一覧の取得を行っているので、一覧取得が2回動いてしまっている。テストをするには都合の悪い作りだっただろうか。


おわり


通信処理をUIスレッドで行ってはいけない、という制限がなんとも面倒だ。画面に記事を表示するというだけの機能しかないのに、ずいぶんと手間がかかった印象がある。

しかし、もっと多機能なアプリを作るにはどのみち非同期の処理は必要になってくるだろうし、一度非同期の処理を作ってしまえば後は使いまわすだけなので、実質的には取っ掛かりが少々面倒、というところだろうか。

Spring for Androidはそれほど苦労もなく使えたので、便利なライブラリだと思う。GET以外のメソッドについては別の機会に試してみる。

アプリが通信機能を持つことは多いだろう、覚えておいて損はないはず。

2013年1月18日金曜日

make のお話

山本です。

今回は古くて旧い make を取り上げてみます。
昔と違って、今ならビルド自動化ツールの選択肢として rake も ant も grunt も xbuild もありますが、それでも敢えて make です。

make とは

make とは、あらかじめ Makefile に生成物 とその材料との間の依存関係を書いておくことで、生成物を得るのに必要十分なプロセスを自動的に行うツールです。
特に、中間生成物が存在するケースで再生成漏れを防ぐのに有用です。
make の特徴
  • 基本的に、何かを生成するために外部のプログラム(コンパイラ etc.)を起動する。シェルスクリプトに思想が似ています。
  • 自力でソースを解析してまで依存関係を判断することはない。ただし、外部ツール(mkdep 等)の支援のもとで依存関係の記述を自動化させる事例はあります。
make の種類
make には、主に2種類のメジャーな方言があります。 基本的な記述方法は一緒ですが、条件分岐やinclude等の特殊な構文や、変数展開時に使えるオプション指定に違いがみられます。
  • GNU make
  • BSD make (a.k.a. pmake)

make 基礎篇

依存関係(ルール)
ターゲット:依存物
        レシピ
ターゲット、依存物ともに複数個指定ができます。(その場合はスペース区切り)
レシピは行頭にタブ文字。複数行指定ができます。行頭にタブ文字以外が出現すると、そこでルールの記述が終わります。
 ターゲット以外はいずれも省略可能。
レシピを省略した場合は、そのターゲットが指定された依存物に依存している事実だけが記述されたことになり、 具体的なレシピは他の記述か拡張子をベースにしたデフォルト(推論規則)になります。
変数展開
$V
もしくは
${VARIABLE}
ただし、{}で括らない書式は、変数名が1文字な場合に限られます。
変数定義
普通に代入する
VAR = the value to be set
 行頭に空白は書けません。そのかわり、等号の前後に空白文字を入れることは可能です。
なお、変数に別の変数の値を代入することもできますが、代入された変数の値が実際に展開されるのは後になります。したがって、
A=$B
B=hoge
dummy:
        echo $A
は、 hoge が出力されます。
その場で変数展開したものを代入する
前述した挙動が望ましくなく、その時点での値を代入したい場合は、
A:=$B
とします。
未定義な場合に代入する
A ?= some default value
とすると、コマンドライン(後述)や環境変数などで値が与えられていない場合に限って代入が行われます。
追加する
 A+= append to A
とすると、変数の値に追加することができます。例えば、
A= Unique
A+= Vision
とした場合、 $A は Unique Vision と展開されます。(Unique と Vision の間に空白が入っている点に注目)
コマンドラインから
make VAR=value
のように、コマンドラインから変数を指定することもできます。

make 応用篇

記述量を減らそう!
自動変数
make が勝手に設定してくれる変数がいくつかあります。
主なものを挙げてみます。
  • $< 依存物
  • $@ 生成物
注意点としては、これらの変数はレシピ実行中に今まさに生成しようとしているターゲットに関する値として展開されます。
つまり、中間生成物の生成レシピを実行している最中では、それぞれ中間生成物の依存物と中間生成物そのものの名前に展開されるということです。
パターンによるルール
all: foo.css bar.css
foo.css: foo.styl
        @stylus foo.styl
bar.css: bar.styl
        @stylus bar.styl
と書く代わりに、
all: foo.css bar.css
%.css: %.styl
        @stylus $<
と書けます。($< は依存物を表す自動変数)
変数の展開時パターン置換
SRCS= foo.styl bar.styl
と書いてあるとすると、
${SRCS:%.styl=%.css}

foo.css bar.css
に展開されます。

共通部分の別ファイルへの切り出し
変数置換を駆使するなどして、ルールから具体的な値が排除できたら、ルールを別ファイルに切り出して、本体 Makefile から include させることで Makefile をまたいだ共通化が図れます。
all: foo.css bar.css
%.css: %.styl
        @stylus $<
の例を取り上げると、
  • Makefile:
    STYLES= foo.styl bar.styl
    include stylus-common.mk
  • stylus-common.mk:
    all: ${STYLES:%.styl=%.css}
    %.css: %.styl
            @stylus $<
のような2ファイルに分割できます。(GNU make の場合)
この例ではあまりありがたみはありませんが、もう少し複雑なルール集合を、いろいろな Makefile で使いたくなってきた時に威力を発揮します。
(なお、 BSD make 系では、 include filename.mk の代わりに .include "filename.mk" などとします)
風変わりなレシピ
常に失敗するレシピ
例えば、仕様書が更新されたらテストを更新しなければいけないことを明確化することを考えましょう。
具体的にテストをコマンドで更新することは一般的には不可能ですが、更新の必要があることを開発者が知る必要もあるわけです。
その場合、
testcase.js: specification.doc
        @echo 仕様が更新されています。テストケースも更新しましょう。 >&2
        @false
 のように、最後に false を実行させることで無理やり失敗させる手が有効です。

2013年1月9日水曜日

Backbone.jsとYiiフレームワークの連携


高瀬です。

Backbone.jsとYiiフレームワークを使用してアプリケーションを開発する場合の、ソースコードの配置や書き方について検討してみる。

サンプルアプリケーションのソースコードはこちらを参照。


サンプルアプリケーションの概要

Webブラウザ上でデータベースのER図が書けないものかと思い立ったので、それをテーマにアプリケーションを作成する。

アプリケーションはプロジェクトを1つの単位として、その中にスキーマやテーブルの定義を書いていく、という形にする。ここで、プロジェクトにリビジョンを付けて、編集履歴を残せるようにしたい。リビジョンはプロジェクトの作成直後を1とし、DB管理者のレビューを経て確定。その後さらに編集を開始する時に2に更新する、というサイクルを想定する。

画面構成や機能はまだまだ未検討で、今回はひとまずプロジェクトの作成、編集、削除ができるところまでを実装する。

画面は以下のとおり。まず、作成済みプロジェクトの一覧が画面の左上に表示される。


プロジェクト名にカーソルを重ねると、そのプロジェクト名の右側にメニューが表示される。メニュー項目は「編集」「プロパティ」「削除」とあり、それぞれER図の編集画面への遷移、プロジェクト名などのプロパティ編集、プロジェクトの削除、を行う。「レビュー」というメニュー項目を追加することを考えているが、今回は未実装。


プロジェクト一覧にカーソルを重ねた時、メニューと同時に、一覧の下側に「新しいプロジェクト」というリンクが表示される。「新しいプロジェクト」をクリックすると、画面の中央に「プロジェクトの作成」というダイアログを表示する。ダイアログにプロジェクト名を入力し、「作成」をクリックするとプロジェクトが作成される。作成されたプロジェクトは自動的にプロジェクト一覧に表示される。



プロジェクトのメニュー項目「編集」は現在未実装。「プロパティ」は「プロジェクトの作成」とほぼ同様のダイアログが表示され、プロジェクト名を変更できる。プロジェクト名が変更されると自動的にプロジェクト一覧に反映される。

メニュー項目「削除」をクリックすると、画面中央に確認メッセージが表示される。「OK」をクリックするとプロジェクトの削除が行われる。削除されたプロジェクトは自動的にプロジェクト一覧から消える。


画面には表示されないが、プロジェクトを作成すると、自動的にリビジョン1を表すデータが作成される。プロジェクトを削除するとリビジョンのデータがすべて削除される。現時点ではリビジョンは1固定。

開発するアプリケーションの名前はtprefix(ティー・プレフィックス)とする。以下、この名前で記述する。

tprefixは以下のデータベース、開発言語、フレームワークで開発する。

構成 DB/言語 フレームワーク
データベース PostgreSQL 9.x -
サーバ側 PHP 5.4 Yii
ブラウザ側 JavaScript Backbone.js

なお、JavaScriptのHTMLテンプレートエンジンとして、Handlebars.jsを使用する。



データベース

tprefixが記憶、管理するデータにはプロジェクトやリビジョン、スキーマ、テーブル、カラムなどが考えられる。その他にもユーザやカラム定義などを予定しているが、今回は「プロジェクト」「プロジェクトリビジョン」のテーブルのみを用意する。その他のテーブルの内容は検討中。




プロジェクトテーブルにはプロジェクトの名前を登録する。
プロジェクトリビジョンテーブルは、プロジェクトのリビジョンを登録する。今回はリビジョンIDは前述のとおり1固定とする。説明内容、レビュー日時、レビュー結果内容は未使用。

どちらのテーブルにもある「削除日時」は、レコードの削除が行われた日時を登録する。すなわち、「レコードの削除」操作はテーブルからレコードを消してしまうのではなく、「削除日時」を登録することにより、削除済みであるとみなすようにする。



ソースコードの配置

ソースコードはYiiフレームワークのディレクトリ構造を元に配置した。

サーバ側(PHP) ブラウザ側(JavaScript)

上図において、tprefixがルートを表す。protected以下にサーバ側のPHP、js以下にブラウザ側のJavaScriptが配置されている。

protected以下はMVCに従ってmodels、controllers、viewsにそれぞれソースコードを作成。これに対して、js以下はBackbone.jsのクラスに従って、models、collections、viewsを作成。

tprefixでプロジェクト一覧が表示されるページとなるのは、projectコントローラのindexアクション。URLでは http://(サーバ名)/index.php?r=project/index となる。



サーバ側

サーバ側はMVCフレームワークに従って開発する。

■モデル

モデルでは、今回はプロジェクトとプロジェクトのリビジョンを扱うので、Project.phpProjectRevision.phpを作成。これらは特にBackbone.jsとの関連は少ない。データベースへのアクセスにはCActiveRecordを使用。

Project.phpのsave()メソッドでトランザクションを使用しているが、トランザクション内の処理はtry/catchで例外を捕らえ、例外発生時にrollbackされるようにするべき。

■コントローラ

コントローラは、2種類のアクションを用意する。1つはブラウザ側で必要になるJavaScript、CSS、HTMLテンプレートを用意するアクション。もう一つは、ブラウザ側からのAjaxでの要求に対応する応答を返すアクション。

ProjectControllerでは、JavaScriptなどを用意するアクションはIndexAction.phpが担当。JavaScript、CSSのファイルを配列で定義する。このソースコード内にあるregisterPackage()メソッドはController.phpに定義しているメソッドで、こちらの記事を参照。

HTMLテンプレートも配列で定義する。これらはYiiのパーシャルビューとして、Controller::renderPartial()で描画させる。

よって、このproject/indexアクションを呼び出すと、表示する内容が何もない、YiiのレイアウトのみのHTMLが生成される。生成されたHTMLのheadタグ内には配列で指定したJavaScriptとCSSのファイルが含まれ、bodyタグ内にはHTMLテンプレートが含まれる。レイアウト部分以外の画面の描画はJavaScriptで行う。

※レイアウト部分と言うのは、前述「サンプルアプリケーションの概要」の図にある、「My Web Application」や「Home About ...」などが表示されている部分。それ以外の部分というのは、プロジェクトの一覧が表示されている部分。

もう一つのAjax要求に対応する応答を返すアクションでは、ListAction.phpのようにモデルを使ってデータを抽出・登録し、結果をJSON形式で出力する。

JavaScriptのファイルをYii::app()->clientScript->registerPackage()でHTMLに反映させるようにしており、この場合headタグ内にscriptタグが列挙される。しかし、headタグ内でJavaScriptを読み込むと、ページ全体の描画が始まるまでに時間がかかるため、ページの表示が遅い。bodyの終了タグの直前でJavaScriptが読み込まれるようにするとページの表示が速くなる。表示を速くするためにはもう少し工夫が必要。

■ビュー

ビューではHTMLテンプレートのみを定義した。このテンプレートはHandlebars.jsによって実際のDOMに変換される。Handlebars.jsのテンプレートはscriptタグ内に記述するようになっているので、それぞれのパーシャルビューは_project_delete_dialog.phpのようにscriptタグで囲まれた内容となっている。

■例外の出力

Yiiではtry/catchによってハンドリングされなかった例外はsiteコントローラのerrorアクションで処理される。ここで、SiteController.phpのとおり、もともとこのアクションは要求がAjaxであればエラー内容をechoで出力し、そうでなければrender()で描画するようになっている。この仕組みをそのまま使ってみようと思う。

Project.phpのprepare()メソッドで、更新または削除対象のレコードが見つからなかった場合は例外とした。この例外はsite/errorアクションに到達するが、NotFoundException.php内で表示するべきエラーメッセージを定めており、要求がAjaxだった場合はJSON形式、そうでない場合は文字列のみとしている。

これにより、JavaScriptからの要求は通常Ajaxで行うようにするので、エラーが発生した場合でもJSON形式の応答がある、という前提でブラウザ側の開発ができるはず。

JavaScriptからの要求がすべてAjaxであるとして開発をするのであれば、NotFoundExceptionクラス内でJSON形式か文字列かを決めるのではなく、site/errorアクション内でやってしまった方がよいだろう。



ブラウザ側

■名前空間とプログラムの開始

Backbone.jsやUnderscore.jsのファイルはmain.phpに記述しており、Ajax用ではないアクションで共通に読み込まれるようにしている。app.jsというファイルも共通としており、これは名前空間の定義用。

プログラムの開始は、プロジェクト一覧を表示する画面の場合、startupディレクトリ内にあるs_project.jsとなる。Webアプリケーション全体をシングルページとして開発する場合はわざわざstartupというサブディレクトリにこのファイルを配置する必要はないが、実験として、プロジェクト一覧の画面と、各プロジェクト内のER図を編集する画面を別のページとしてみようと考えており、起点となるソースコードを配置するディレクトリを用意した。

■コレクション

c_Project.jsでプロジェクトの一覧を扱う。urlに、サーバ側のproject/listアクションを指定。これにより、fetch()メソッドを実行するとプロジェクト一覧が取得される。一覧取得が完了すると自動的にresetイベントが発生するので、v_ProjectList.jsでハンドリングし、画面に描画する。

■モデル

今回扱うモデルはプロジェクトのみ。m_Project.jsで定義している。ここで、困ったことが2点あった。

1点目は、プロジェクトを新規作成する時、サーバ側のproject/saveアクションへプロジェクト名をPOSTしたいのだが、Backbone.Modelクラスのデフォルトのsave()メソッドを呼び出すと、サーバ側(PHP)の$_POST変数に送信しているはずの値が入らない。仕方がないので、jQueryのpost()で送信している。

$_POSTで受け取れないのは、Backbone.sync()により送信されるデータのエンコードがデフォルトではapplication/jsonになっており、これをPHPが受け取れていないため。Backbone.emulateJSON = true を指定することにより、$_POSTで受け取れるようになるかも、とのこと。

2点目は、Yiiとの連携とは関係ないが、プロジェクトの新規作成・編集・削除を行った際に自動的にプロジェクト一覧を再描画したいが、モデルが更新されたというイベントをどのようにしてコレクションに伝えればいいのかが分からなかった。無理やりな感じだが、プロジェクト一覧を描画しているビューのelが参照している要素に対してchangeイベントを発火して伝えている。

と、これを書きながら思ったが、もしかしてコレクションがモデルのaddイベントをハンドリングしていればいいだけの話だろうか。うむむ、気づくのが遅かった。

オブジェクト同士に関連がなく、ハンドリングの指定ができないような場合のために、前述のapp.jsで定義しているappオブジェクトを、Backbone.Eventの派生クラスにしておき、appオブジェクトにtrigger()させる、という方法もある。


■ビュー

ビューはv_ProjectItem.jsのようにモデルやコレクションに対応するものと、v_ProjectEditDialog.jsのようにダイアログなどの表示する部品に対応するものを作成した。ダイアログなどはサーバ側のビューで定義されているHTMLテンプレートから生成する。

ビュー同士や、別のビューが持っているモデルへのイベントの伝達方法が分からず、至らない点が多くあるので、追々直していくことにする。



おわり

ER図の編集画面くらいまでは作りたかったのに、勉強不足でいろいろ苦戦してようやくここまで、といったところ。

バリデーションをしていなかったり、エラー処理が不十分だったりと、実用レベルではない部分もあるので、そのあたりも身に付けていきたい。

tprefixの開発はまだまだ続く。はず。