読者です 読者をやめる 読者になる 読者になる

FuelPHPでMongoDBをちょびっと便利に使う

php mongodb

MongoDB Advent Calendar 2013の14日目です。まぁ途中で一度途切れているので気楽に行きましょうw


さて、このエントリはここ連続で続いている FuelPHP ネタでもあります。

MongoDBでSQL的なシーケンスをどうするか

FuelPHPでMongoDBを使うには、Coreに含まれている Mongo_Dbクラスを使うのが普通だと思います。基本的なメソッドはひと通り用意されていて、通常使う文にはまあ困らないでしょう。ただしちょっと突っ込んだことをやろうとすると、そのままでは使えなくて何らかの拡張を自分ですることになるのもよくあることです。


MongoDBをアプリケーションデータの格納に使うとして、SQL的なシーケンス(SEQUENCE)が欲しいことがあります。MySQLでいうところのSERIAL属性と同様で、一意な値を自動採番していくれる仕組みです。むろんMongoDBでもちょっとした仕掛けで実現可能で、そのための手法はちょっとググれば簡単に見つかります。ただし FuelPHP の Mongo_Db クラスでは、PHP の Mongo オブジェクトがプライベート変数になっていて直接アクセスできるメソッドもないので、直接アクセスするには Mongo_Db クラスを継承したクラスを用意する必要があります。そうやって作ったのが、以下の Util_MongoPlus クラスです。

<?php

class Util_MongoPlus extends \Fuel\Core\Mongo_Db
{
    protected $seq_collection = 'sequences';

    public function getRawMongo()
    {
        return $this->db;
    }

    public function seqNext($name)
    {
        $query = array(
            '_id' => $name,
            );
        $update = array(
            '$inc' => array('seq' => 1),
            );
        $options = array(
            'new' => true,
            'upsert' => true,
            );

        $result = $this->db->{$this->seq_collection}->findAndModify(
            $query,
            $update,
            null,
            $options
            );
        return $result['seq'];
    }
} 

直接 Mongo オブジェクトにアクセスする getRawMongo メソッドと、シーケンスを実現する seqNext メソッドを持っています。シーケンスは sequences というコレクションを作り、そこにシーケンス名がキーのレコードを作成して、アトミックにインクリメントを行うことで実現しています。


※ とここまで書いていて、Mongo_Db::get_collection メソッドを用いれば任意のコレクションにアクセスできるので、直接 Mongo オブジェクトを使う必要がないことに気がつきました。まあいいやw

MongoDBをベースにしたモデルのベースクラスを作る

で、ここまでできたらもういっそ Model のベースクラスまで作ってしまえということで、できたのが以下です。

<?php
use Fuel\Core\Date;

class Model_Mongobase
{
    static protected $table_name = null;
    static protected $uniq_id = null;

    static protected $timestamp = null;

    static public function getMongo()
    {
        return \Util_MongoPlus::instance();
    }

    static public function getTable()
    {
        return static::$table_name;
    }

    static public function getSeqName()
    {
        // dummy
        return null;
    }

    static public function newId()
    {
        $mongo = static::getMongo();
        $seq = static::getSeqName();
        if (empty($seq)) {
            throw new Exception('empty sequence');
        }
        return $mongo->seqNext($seq);
    }

    // base methods
    static public function get($id)
    {
        if (static::$uniq_id === null) {
            throw new Exception('Unique key is not found');
        }

        $mongo = static::getMongo(); 
        $conds = array(
            static::$uniq_id => (int)$id,
            );
        $result = $mongo->get_where(static::getTable(), $conds);
        if (count($result) === 0) {
            throw new Exception('Item not found');
        }
        return $result[0];
    }

    static public function all($options = null)
    {
        $conds = array_val($options, 'conditions');
        $order_by = array_val($options, 'order_by');
        $use_cursor = array_val($options, 'use_cursor', false);

        $mongo = static::getMongo();
        if (!empty($conds)) {
            $mongo->where($conds);
        }
        if (!empty($order_by)) {
            $mongo->order_by($order_by);
        }
        if ($use_cursor) {
            $result = $mongo->get_cursor(static::getTable());
        } else {
            $result = $mongo->get(static::getTable());
        }
        return $result;
    }

    static public function save($data)
    {
        if (static::$uniq_id === null) {
            $result = static::saveWithNonuniq($data);
        } else {
            $result = static::saveWithUniq($data);
        }
        return $result;
    }
    static public function saveWithNonuniq($data)
    {
        // call hook
        static::beforeInsert($data);

        $mongo = static::getMongo();
        return $mongo->insert(static::getTable(), $data);
    }

    static public function saveWithUniq($data)
    {
        $mongo = static::getMongo();

        $id_name = static::$uniq_id;

        $id = array_val($data, $id_name);
        if (empty($id)) {
            $id = static::newId();
            $data[$id_name] = $id;

            static::beforeInsert($data);
            $result = $mongo->insert(static::getTable(), $data);
        } else {
            $conds = array(
                $id_name => (int)$id,
                );

            static::beforeUpdate($data);
            $result = $mongo->where($conds)->update(static::getTable(), $data);
        }
        return $result;
    }

    static public function deleteWithUniq($id)
    {
        $mongo = static::getMongo();

        if (static::$uniq_id === null) {
            $id_key = '_id';
        } else {
            $id_key = static::$uniq_id;
        }
        $conds = array(
            $id_key => (int)$id,
            );
        return $mongo->where($conds)->delete(static::getTable());
    }

    static public function deleteAll($conds)
    {
        $mongo = static::getMongo();

        return $mongo->where($conds)->delete_all(static::getTable());
    }

    static protected function beforeInsert(&$data)
    {
        $hook = array_val(static::$timestamp, 'before_insert');
        if (empty($hook)) {
            return;
        }
        $key = array_val($hook, 'key');
        static::handleTimestamp($data, $key);
    }

    static protected function beforeUpdate(&$data)
    {
        $hook = array_val(static::$timestamp, 'before_update');
        if (empty($hook)) {
            return;
        }
        $key = array_val($hook, 'key');
        static::handleTimestamp($data, $key);
    }

    static public function handleTimestamp(&$data, $key)
    {
        foreach ($key as $k) {
            $data[$k] = Date::forge()->format('mysql');
        }
    }
}

あー、いつものarray_val()を使ってますね。ここで紹介している自作の便利関数です。


使用するには、例えば以下の様な Model_Mongobase を継承したクラスを作って

<?php

class Model_Item extends Model_Mongobase
{
    static protected $table_name = 'item';
    static protected $sequence = 'item';
    static protected $uniq_id = 'id';

    static protected $timestamp = array(
        'before_insert' => array(
            'key' => array('created_at', 'updated_at'),
            ),
        'before_update' => array(
            'key' => array('updated_at'),
            ),
        );

    static public function getSeqName()
    {
        return static::$sequence;
    }
}

あとは任意のデータを格納するだけです。

<?php
  :
    Model_Item::save(array('fuga' => 'hoge'));

継承したクラスで

static protected $uniq_id = 'id';

のように $uniq_id プロパティが定義されていれば、それに対してシーケンスを適用してくれます。


いつもなら Package にまとめて github で公開したりするんですが、これに関してはちょっとどういうまとめ方が良いのか悩んでいるところもあって、とりあえず生ソースをブログで公開することにしました。


明日のアドカレは@さんです。