既存のコードをかえずにAdapterをかませることでMySQLからDynamoDBに移行したときの話

はじめまして、こんにちわ。
AWS Advent Calendar 12月16日分を担当させていただきます。
mehosoです。

さて、いきなりですが、DynamoDBって便利ですよね！

* 安い
* 速い
* 何と言っても負荷を気にしなくてもいい ←これに尽きます

そんなこんなで
 *既存のコードをかえずにAdapterをかませることでDynamoDBに移行したときの話*
をしたいと思います。

### 経緯

あるところに、データベース負荷がボトルネックのPHPxMySQLのプロジェクトがありました。
そしてその横には「負荷対策をしろ」と突然言い渡された一人の男がおりました。

男はせっせとデータベースのチューニングをしてみたり、処理をキューイングにしてみたり、
しまいにはスケールアップをするべきだ！と嘆いてみたり...

当然MySQLは泣き止みません。

そこで男は DynamoDB化 を試みます。
男はちょっと横着で、既存のコードは変えたくない！と思いました。

### 下準備

①まずは AWS SDK for PHP をとりにいく
http://aws.amazon.com/jp/sdkforphp/

②適当なDynamoDBWrapperを拝借させていただく（ありがたやですね）
https://github.com/masayuki0812/dynamodb-php-wrapper

### DynamoDBの準備

##### テーブル名：user_counts

| KEY  | NAME                 | TYPE   |
|:-----|:---------------------|:-------|
| HASH | user_id              | Number |
| -    | a_count        | Number |
| -    | b_count    | Number |
| -    | c_count  | Number |
| -    | d_count           | Number |
| -    | e_count       | Number |
| -    | f_count     | Number |
| -    | g_count | Number |
| -    | last_update | String | 

すみません今回至って簡単すぎる構成で、
プライマリーキーにuser_idを持つ各項目のカウントに利用するようなものを想定します。

### 既存コードの確認

```php:user_counts_model.php

class User_counts_model extends CI_Model
{
    public function set($user_id, $data)
    {
        $this->db->insert('user_counts', $data)
    }

    public function get($user_id)
    {
        $this->db
            ->where('user_id', $user_id)
            ->get('user_counts')
        ->row();
    }

    public function countUp($column, $data)
    {
        $this->db
            ->set($column, $column.' + 1', false)
            ->where('user_id', $user_id)
            ->update('user_counts');
    }

    // 受け取ったカラムの値をリセットする
    public function reset_count($user_id, $columns)
    {
        foreach($columns as $column){
            if($column == 'last_update'){
                $this->db->set($column, date('Y-m-d'));
            }else{
                $this->db->set($column, 0);
            }
            $this->db
                ->where('user_id', $user_id)
                ->update('user_counts');
        }
    }

    // 複数のユーザーのカラムを一度にカウントアップする
    public function countUpUsers($column, $users){
        $this->db->set($column, $column.' + 1', false)
            ->where_in('user_id', $users)
            ->update('user_counts');
    }
}
```

かなり簡易的にまとめましたがざっくりこんな感じのプログラムがあったとします。
ここで思ったんです。
フレームワークのデータベースライブラリの内部処理を
MySQLからDynamoDBへスイッチしてあげればいいんだと...
（果たしてこれが良い判断だったのかどうか...笑）

### DynamoDB用DAOを作成

```php:dynamodb_dao.php

class Dynamodb_dao extends DynamoDBWrapper
{
    // テーブル名
    protected $tableName = 'user_count';
    // テーブルスキーマ
    protected $tableData = array(
        'HASH' => 'user_id::N',
        'COLUMNS' => array(
            'a_count::N',
            'b_count::N',
            'c_count::N',
            'd_count::N',
            'e_count::N',
            'f_count::N',
            'g_count::N',
            'last_update::S',
        )
    );
    private $updateData;
    private $userIds;

    /**
     * データをDynamoDBに格納する
     * @param object $data
     */
    public function putItem($data)
    {
        $params[$this->tableData['HASH']] = (int)$data->user_id;
        foreach($this->tableData['COLUMNS'] as $val){
            $column = $this->convertColumn($val);
            $name = $column[0];
            if(!empty($data->{$name})){
                $params[$val] = $this->_cast($column[1], $data->{$name});
            }
        }
        parent::putItem($params);
    }

    /**
     * DynamoDBからデータを取得する
     * (データがある場合はカラムを全てセットする)
     * @return array notification_count DATA
     */
    public function getItem()
    {
        $res = $this->getByKey($this->userIds[0], null, 1);
        if(empty($res[0])){
            return array();
        }
        $data = $res[0];
        foreach($this->tableData['COLUMNS'] as $val){
            $column = $this->convertColumn($val);
            $name = $column[0];
            if(!isset($data[$name])){
                $data[$name] = 0;
            }
        }
        return $data;
    }

    // updateする項目をセットする
    public function setUpdateData($key, $value)
    {
        $v = explode(' ', $value);
        if(count($v) == 3){
            $type = 'N';
            $action = 'ADD';
            $value = (int)$v[2];
        }else{
            $type = ($key == 'last_update') ? 'S' : 'N';
            $action = 'PUT';
            $value = $this->_cast($type, $value);
        }
        $this->updateData[$key.'::'.$type] = array($action, $value);
    }

    /**
     * user_idをセットする
     * @param array $ids (user_id)
     */
    public function setUserIds(array $ids)
    {
        $this->userIds = $ids;
    }

    /**
     * 事前にセットした条件でレコードのアップデートを実行する
     */
    public function updateItem()
    {
        $userIds = array_unique($this->userIds);
        foreach($userIds as $id){
            $key = array($this->tableData['HASH'] => (int)$id);
            parent::updateItem($key, $this->updateData);
        }

        $this->_reset();
    }

    private function _cast($type, $data)
    {
        return ($type == 'S') ? (string)$data : (int)$data;
    }

    private function _reset()
    {
        $this->updateData = array();
        $this->userIds = array();
    }
}
```

はじめにテーブル定義を書いといてあげることで、
次他のやつをDynamoDB化したいとなったときに汎用的に使えると思います。
また`setUpdateData`という関数ではMySQLのカラムの直接加算or減算などをうまく考慮してDynamoDBの仕様にあてはめています。

### Adapterをかませる！

お待たせしました、これを書きたかったのですが、上のコードを変に改変しすぎて、かなり質素なものになってしまった予感です。

```php:user_counts_adapter.php

class user_counts_adapter extends CI_Model
{
    public $ddb;

    public function __construct()
    {
        parent::__construct();
        $this->ddb = new Notification_Count_DDB();
    }

    public function insert($table, $data)
    {
        $this->ddb->putItem($data);
    }

    public function set($key, $value)
    {
        $this->ddb->setUpdateData($key, $value);
        return $this->_chain();
    }

    public function where($key, $value)
    {
        $this->ddb->setUserIds(array($value));
        return $this->_chain();
    }

    public function where_in($key,array $values)
    {
        $this->ddb->setUserIds($values);
        return $this->_chain();
    }

    public function update($table = null)
    {
        $this->ddb->updateItem();
    }

    public function row()
    {
        $res = $this->ddb->getItem();
        return empty($res) ? array() : (object)$res;
    }

    public function row_array()
    {
        return $this->ddb->getItem();
    }

    private function _chain()
    {
        return $this;
    }

    public function select($column = null)
    {
        return $this->_chain();
    }

    public function get($table = null)
    {
        return $this->_chain();
    }
}
```

これだけで既存のコードを変えずにMySQLに無駄に入れていたデータがなくなり、
無駄に走っていたUPDATEもなくなり、負荷もなくなり、やっと一息つけました。

Joinなどの処理がないMySQLのテーブルはほとんどこのパターンでDynamoDBに移行しちゃいました。

### まとめ

本来なら設計の段階でDynamoDBの特性を活かしたベストなアーキテクトを組むべきなのですが、
ちょっと無理矢理にでも工数かけずに、MySQLネックなプログラムを改善したい。
そんな時には今回のようなやり方も良いのかもしれませんね！