CakePHPでDynamoDBを参照してみる

## 1.DynamoDBとは？
　AmazonDynamoDBは、AWSの提供するNoSQLデータベースエンジンです。RDBMSとして有名なAmazonRDSのNoSQL版ですが、「フルマネージドサービス」を謳っていることからも分かる通り、DynamoDBはDynamoDBであり、単体のNoSQLエンジンです。RDSのようにMySQLやOracleやSQLServerといった好きなエンジンを載せたインスタンスを起動する形式ではなく、Table単位で作成し使用する形になります。
　従って、RDSの時のような細かいチューニングはできませんし、実際不要になっているのが長所であり短所でもあります。

## 2.DynamoDBの準備
　DynamoDBの作り方の詳細は省略しますが、RDS同様AWSのManagement ConsoleからDynamoDBを選択し、「create table」から必要事項を入力すればすぐにtableが作成できます。
　RDSのようにインスタンスの概念はありませんが、スループットプロビジョニングとリージョンは設定可能です（スループットプロビジョニングについては[こちら](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ProvisionedThroughputIntro.html)を参照してください）。

## 3.検証環境の準備
　今回は以下の環境を用いてDynamoDBにアクセスを試みてみたいと思います。

* EC2 m1.small instance
* CentOS 6.5 64bit
* PHP 5.4
* AWS SDK for PHP 2.6
* CakePHP 2.5.10

DynamoDBのtableは東京リージョンにてとりあえずデフォルトのままのプロビジョニング設定のまま作成しています。
TableのHashKeyとしては、とりあえずイメージが湧きづらいので通し番号のint型の「id」を準備しました。
AWS SDK for PHPについては、今回はpearでCakePHPのレポジトリ内ではなくサーバの共有include_path内に設置しています。
AWS SDK for PHPのインストール方法については後述の参考URLを参照してください。
amazonのチャンネルを指定すればpear install awsで問題なくインストールできると思います。

## 4. CakePHPでの実装内容概要
　CakePHPでDynamoDBを参照するにあたり、必須の実装項目は以下の通りです。

* AWS SDK向け設定
* DynamoDB SDK向け設定
* DynamoDB専用モデルファイル
* テストページ

このうち上から2つは、1つの設定ファイルとモデルファイル内での初期化にて実装します。
テストページについては、今回はTopControllerを作成し、TopController::indexのページを/としてルーティングし、/にアクセスするとDynamoDBのデータが表示されるよう実装していきます。

## 5. AWS SDK及びDynamoDB SDK向け設定
　基本的に設定ファイルは1つのPHPファイルにPHP配列で格納する、標準ドキュメント通りの実装そのままで問題ありません。

```lang:app/Config/aws.php
$awsConfig = array(
			"key" => "ACCESS_KEY",
			"secret" => "SECRET_ACCESS_KEY",
			"region" => "REGION_NAME"
	);
```

全てを削ぎ落した超単純な書き方で、必要最低限のものしか書いていません。しかし、これだけあれば大体のAWS SDKの認証は問題なく通るようになっています。DynamoDBも同様です。

## 6. モデルファイルの作成
　まずはapp/Model/DynamoDb.phpを作り、aws.phpの内容を使って初期化（認証）をしてみましょう。

```lang:app/Model/DynamoDb.php
<?php
App::uses("Model", "Model");
require "AWSSDKforPHP/aws.phar";
use Aws\Common\Aws;
use Aws\Common\Enum\Region;
use Aws\S3\Enum\CannedAcl;
use Aws\S3\Exception\S3Exception;
use Guzzle\Http\EntityBody;
class DynamoDb extends Model
{
	public $Connection;
	public function __construct()
	{
		parent::__construct();
		require APP . "/Config/aws.php";
		$Aws = Aws::factory($awsConfig);
		$this->Connection = $Aws->get("dynamodb");
		return $this->Connection;
	}
}
```

先程もチラと触れていましたが、AWS SDK自体はpearでインスタンス自体にインストールし、php.iniのinclude_pathを通して読み込んでいます。そのため、CakePHPですが、requireとしています。
　例えば、pearの中身をcakephp/Vendor/以下にインストールしてしまえば、requireの代わりにApp::import()を使用することもできます（今回の環境ではpearのpathの変更等ちょっとインストールに一手間かかるので見送りました）。
　さて、この状態でテストページを以下のように実装してみましょう。

```lang:app/Controller/TopController.php
<?php
App::uses("AppController", "Controller");
class TopController extends AppController
{
	public $uses = array("DynamoDb");
	public function index()
	{
		$this->set("DynamoDb", $this->DynamoDb);
		return $this->render();
	}
}
```
```lang:app/View/index.ctp
<?php
var_dump($DynamoDb);
```
```lang:app/Config/routes.php
<?php
	Router::connect("/", array("controller" => "top", "action" => "index"));
	CakePlugin::routes();
	require CAKE . "Config" . DS . "routes.php";
```

これで吐き出したvar_dump()の内容がDynamoDbへの接続インスタンスであれば、初期化（認証）に成功しています。失敗した場合は、失敗した旨が表示されているはずです。

## 7. CountとItemを取得してみる
　いよいよDynamoDbの中身を取得してみたいと思います。以下のように各ファイルを変更します。

```lang:app/Model/DynamoDb.php
<?php
App::uses("Model", "Model");
require "AWSSDKforPHP/aws.phar";
use Aws\Common\Aws;
use Aws\Common\Enum\Region;
use Aws\S3\Enum\CannedAcl;
use Aws\S3\Exception\S3Exception;
use Guzzle\Http\EntityBody;
class DynamoDb extends Model
{
	public $Connection;
	public function __construct()
	{
		parent::__construct();
		require APP . "/Config/aws.php";
		$Aws = Aws::factory($awsConfig);
		$this->Connection = $Aws->get("dynamodb");
		return $this->Connection;
	}
	
	private function __getAllItemCount()
	{
		return $this->Connection->describeTable(array("TableName" => "TABLE_NAME"));
	}
	
	private function __getItemCollection($startId = 1, $count = null)
	{
		$getLimitCount = (!empty($count))?$count:$this->__getAllItemCount();
		$result = array();
		$keys = array();
		if ($startId <= $getLimitCount)
		{
			for($id=$startId;$id<=$getLimitCount;$id++)
			{
				$keys[] = array(
					"id" => array("N" => $id)
				);
			}
		}
		else
		{
			return false;
		}
		$result = $this->Connection->batchGetItem(
				array(
					"RequestItems" => array(
						"TABLE_NAME" => array(
							"Keys" => $keys,
							"ConsistentRead" => true
						)
					)
				)
			);
		return $result->getPath("Responses/TABLE_NAME");
	}

	public function getAllItemCount()
	{
		return $this->__getAllItemCount();
	}
	
	public function getSomeItems($startId, $count)
	{
		return $this->__getItemCollection(0,50);
	}
}
```
```lang:app/Controller/TopController.php
<?php
App::uses("AppController", "Controller");
class TopController extends AppController
{
	public $uses = array("DynamoDb");
	public function index()
	{
		$this->set("allCount", $this->DynamoDb->getAllCount());
		$this->set("someItems", $this->DynamoDb->getSomeItems());
		return $this->render();
	}
}
```
```lang:app/View/index.ctp
<?php
var_dump($allCount);
var_dump($someItems);
```
TABLE_NAMEには適宜作成時のテーブルの名前を入れてください。
　もし問題が起こっていなければ、1つ目のvar_dump()にテーブル内のアイテム総数が、2つ目のvar_dump()にid=0から50件のアイテムがそれぞれ表示されるはずです。

　ちなみに、DynamoDbは標準でソートする機能はないようです。あるといえばあると言えますが、「HashKeyとRangeが必ず決まっている」「Queryを使用する（Scan操作では不可）」「Rangeのみのソート（HashKeyソートは不可）」という苦行のような制約を潜る必要があります（この苦行を潜った上でscan_index_forward => trueという条件を加えてQueryを実行するとソートされます）。MongoDBならsortもあるしOrderByという親切なものまであるのになぁ・・・と思ってしまいました。

## 8.最後に・・・特にDynamoDBさんに！
　DynamoDBは、Webサービスを作る際に中核技術として使うにはちょっと男気がありすぎるというか、色々シンプルすぎるかもしれません。
　そして、サービス（エンジン）自体に男気がありすぎるせいで、アプリケーション側がそれを受け入れる寛大さ、すなわち高機能性が求められてしまうのではないかと感じました。MySQLやMongoDBは母性が強すぎて、気をつけておかないとプログラムがグズなヒモ状態になってしまいます。それとは逆に、DynamoDBは男気がありすぎて、ついていこうと躍起になっているうちに、気がついた時にはプログラムがどんどん複雑で高機能になりがちだと思います。ちょっと僕の手には負えない気もします・・・。
　ただ、容量による従量課金ではなくスループットによる従量課金である点や、スループットがプロビジョニングによって保証してもらえる点は非常に興味があります。
　ちなみに、僕自身がどちらかと言うとインフラ屋さんなので、こんな男気あるDBをお手手の上でコロコロできるようなPGさんを尊敬してしまいます。