モデル - Model_Baseクラス

モデル - Model_Baseクラス

ここでは、モデルの基底クラスについて説明いたします

なぜフルスクラッチで書いたのか

FuelPHPには、Model_CrudOrm\Modelといったモデルクラスが提供されています

しかし、Model_Baseは、それらのクラスを継承しておりません

その理由をまず説明いたします

Model_CrudOrm\Modelは、

プロパティの取得や設定など、高頻度でアクセスされるプロパティにマジックメソッド(__get, __set)を使用しています

マジックメソッドは動作が遅い1ため、コードではなく「動作速度を」軽量にしたいことと、

後述のいくつかの制約や機能を加えたかったため、Model_CrudもOrmパッケージも継承しない基底クラスModel_Baseを実装しました

完全にゼロベースではなく、FuelのModel_Crudにあるメソッドを一部組み込んでおります


制約

Model_Baseクラスは、

モデルクラスの名前とテーブル名の関係に命名規約があるなど、いくつか制約を加えています

1. モデルクラス名とテーブル名の命名規約

Model_Baseを継承するクラスの名前と、DB上のテーブル名は対応させなければなりません

代わりに、命名規約に則りさえすれば、楽にモデルのコードを書くことができます

テーブル名とモデルクラス名の関係は、

  • テーブル名:複数形
  • モデルクラス名:単数形

とする必要がありますいくつか具体例を挙げます

テーブル名 単数形 モデルクラス名
users user Model_User
companies company Model_Company
people person Model_Person

この対応関係を作るために、内部でFuelPHPのInflector::pluralizeメソッド2を使用しています

単数形の単語を与えると、複数形にした単語を返すメソッドです

そのため、oil consoleコマンドで手軽に対応関係を確認することができます

$ cd /path/to/sample-fuelphp
$ php oil console
Fuel 1.7.1 - PHP 5.4.28 (cli) (Jun 19 2014 14:31:57) [Darwin][]
>>> Inflector::pluralize('man')
men         # この場合テーブル名は`Model_Men`とする
>>> Inflector::pluralize('monky')
monkies     # この場合テーブル名は`Model_Monkies`とする
>>> Inflector::pluralize('person')
people      # この場合テーブル名は`Model_People`とする

しかし、oil generate modelコマンドで生成されたマイグレーションが生成するテーブルは、

モデルクラス名の複数形になるため、さほど意識する必要はありません

※ Moel_Baseクラス自身をインスタンス化する必要がないため、abstractにしています

2. プロパティ名とカラム名の命名規約

DBのカラム名と、モデルクラスのプロパティ名にも命名規約があります

カラム名とプロパティ名は一致させる

モデルの設定や、Model_Baseの継承クラスの処理を簡潔するための規約です

DBのカラム名と、モデルクラスのプロパティ名は一致させる必要があります

また、モデルで定義されたプロパティは、publicで、かつDBに存在する必要があります

class Model_User extends Model_Base {
    public $company_id      = null;
    public $uid             = null;
    public $next_engine_id  = null;
    public $email           = null;
    public $access_token    = null;
    public $refresh_token   = null;
}

例えば、はじめに3で説明したusersテーブルを使用したModel_Userの場合ですと、

以下の様な定義になります

なお、id, created_at, updated_atはModel_Baseに定義されているため、再定義する必要はありません

これら3つのカラムは、全てのテーブルが持っていると制約しています

しかし、全てのプロパティが強制的に保存されてしまうと柔軟性に欠けるため、

保存対象から除外するカラムを指定する設定を設けています

保存処理から除外したいプロパティはignoreSaveKey

保存対象から除外するカラムを指定する、ignoreSaveKeyというプロパティがあります

class Model_Hoge extends Model_Base {
    public $hoge = null;
    public $foo  = null;
    public $bar  = null;

    protected $ignoreSaveKey = array('hoge', 'foo');
}

$model = new Model_Hoge();
$model->bar = 'fizz';

// hogeとfooは除外指定されているため、barのみが保存される
$model->save();     // INSERT INTO `hoges` SET `bar`='fizz'

上記のように、プロパティ名の配列を指定します

  • DBに存在しないカラムをプロパティとして使用したい、
  • PHPからDBに保存したくないプロパティ

などを保存対象から除外することができます

DBに存在するカラムとモデルのみに存在するプロパティを分ける

DBに存在しないカラムをプロパティとして定義することができるため、

第三者がコードを見た際に、DBに存在するプロパティなのか見間違うリスクが生まれました

そのため、DBに保存するカラム名(プロパティ名)と、

DBに保存しない、モデルクラスにのみ記述されたプロパティ名を分けるための命名規約を設けます

種類 命名規則
DBに保存するプロパティ スネークケース(hoge, hoge_foo_bar)
モデルにのみ存在するプロパティ キャメルケース(hoge, hogeFooBar)

上記の表のように、命名を分けて下さい

Model_Baseで言えば、id, created_at, updated_atがカラム名(スネークケース)

validationErrors, primaryKey, ignoreSaveKeyがモデルのプロパティ(キャメルケース)です


主キーの変更

主キーは、primaryKeyプロパティで指定します

デフォルトではidと指定されていますが、任意の値へ変更することができます

// 主キーを変更するには$primaryKeyプロパティの値を変更する
class Model_Map extends Model_Base {
    public $foo_id = null;
    public $hoge_id = null;

    protected static $primaryKey = 'foo_id';
}

// idではなく、カラムfoo_idの値で検索をかけることができる
$map = Model_Map::find('hoge-foo-bar');

現状、主キーは単一の指定のみ対応しております

複合主キーには対応しておりません


基本的な機能

次は、Model_Baseで使える基本的な機能について説明いたします

このドキュメントには、細かな仕様を記述しておりません

細かい挙動や仕様については、fuel/app/classes/model/base.phpを御覧下さい

インスタンスの生成

__construct(array $data = array())

引数
array $data プロパティに与える初期値を連想配列で指定
class Model_Test extends Model_Base {
    public $hoge = null;
}

$model = new Model_Test();
$model->hoge;   // null

$model = new Model_Test(array('hoge' => 1));
$model->hoge;   // 1

読み取り

データの読み取りには、find, findAll, findBy, findLikeを用意しています

find($id)

findは、主キーを用いて検索を行います

引数
mixed $id 取得したいモデルのIDを指定(文字列か数値を想定)
戻り値
指定されたIDに対応するデータが有れば、自身のインスタンス、なければnull
$model = Model_Test::find(1); // id値で検索

$exist = is_null($model) ? 'ない' : 'ある';

findAll()

引数
なし
戻り値
モデルクラスと対応するテーブル内の全件のデータ
$models = Model_Test::findAll(); // 全件取得

$found = (count($models) > 0) ? '1件以上ある' : '1件もない'

findBy($column, $value, $operator = '=')

引数
string $column 比較するカラム名
mixed $value 比較する値
string $operator 比較に用いる演算子(デフォルトは=
戻り値
比較条件にマッチしたモデルのインスタンスの配列、1件もマッチしなければ空配列
$models = Model_Test::findBy('hoge', 'foo');    // カラムhogeの値がfooなモデルを取得
$models = Model_Test::findBy('age' 10, '>=');   // 比較に>=を使用

$found = (count($models) > 0) ? '1件以上ある' : '1件もない'

findLike($column, $value)

引数
string $column 検索に使用するカラム
mixed $value 検索に使用する値
戻り値
比較条件にマッチしたモデルのインスタンスの配列、1件もマッチしなければ空配列
$models = Model_Test::findLike('description', 'Geek');  // LIKEを使用して部分一致を検索

$found = (count($models) > 0) ? '1件以上ある' : '1件もない'

上記のような、汎用的で簡素な検索・取得メソッドしか用意しておりません

複雑な検索クエリを書かなければならない場合は、検索用のメソッドを実装して下さい

また、件数のカウントにはcountを用意しています

countについてはModel_Crudのcountメソッドをそのまま移植しています

使い方はそちらを御覧下さい

Model_Crud - クラス - FuelPHP ドキュメント

http://fuelphp.jp/docs/1.7/classes/model_crud/methods.html#/method_count

$row_count = Model_Test::count();   // テーブルの全行数をカウント

保存処理

保存系の処理には、insert, update, save, createを用意しています

insert($insert_ignore = false)

insertメソッドは、第一引数にINSERT IGNORE...を使用するか否かのフラグを受け取ります

しかし、INSERT IGNOREを使用するとINSERTに使用する処理時間が若干増えます

デフォルトの動作はなるべく早く軽くしておきたいため、引数を省略した場合は通常のINSERTを行います

引数
boolean $insert_ignore trueが渡されるとINSERT IGNORE句を使用する(デフォルトは使用しない)
戻り値
挿入に成功すればtrue

挿入に失敗(INSERT IGNOREで挿入が起きなかった場合も)したらfalse

また、保存に成功した場合、挿入されたidがidプロパティに反映されます

// まだDBに未挿入のデータはinsertを用いる
$model = new Model_Test(array('hoge' => 'foo'));
$model->insert();       // DBに挿入される
$model->id;             // 挿入されたIDが格納されている

$model->insert();       // そのまま再INSERTしようとするとキー重複で例外発生
$model->insert(true);   // trueを渡すとINSERT IGNOREになり、キー重複の例外が発生しない

update()

引数
なし
戻り値
更新に成功したらtrue、更新に失敗したらfalse
// DBから取得したデータならupdateを用いる
$model = Model_Test::find(1);
$model->content = 'rewrite from PHP';
$model->update();       // DBの値が更新される

save()

引数
なし
戻り値
保存に成功すればtrue、失敗すればfalse

saveメソッドはINSERT ... ON DUPLICATE KEY UPDATE ...を使用します

DBにUNIQUE等の制約がかかっているカラムがあるとしたときに、

重複する値がDBに存在している場合は、モデルの値でDBを更新します

重複する値がDBに存在していない場合は、挿入を行います

    // fuel/packages/nextengine/classes/nextengine/api/client/router.phpの内部

    private function _createUser($company_id) {
        $user_info = parent::apiExecute('/api_v1_login_user/info');
        $user_info = $user_info['data'][0];

        $user = new \Model_User();
        $user->company_id     = $company_id;
        $user->uid            = $user_info['uid'];
        $user->next_engine_id = $user_info['pic_ne_id'];
        $user->email          = $user_info['pic_mail_address'];
        $user->access_token   = $this->_access_token;
        $user->refresh_token  = $this->_refresh_token;
        $user->created_at     = \DB::expr('NOW()');

        $user->save();  // INSERT or UPDATE

        return $user;
    }

create(array $columns, array $values) {

createメソッドはstaticメソッドとして提供されており、

引数に特定の形式で配列を与えると、DBに挿入を行い、そのデータをモデルクラスのインスタンスとして返します

引数
string[] $columns 挿入するカラム名の配列
array[] $values 値の配列、指定する順序は$columnsの順序と対応している必要がある
戻り値
挿入されたデータのインスタンスの配列
Model_Test::create(
    array(
        'username',
        'password',
    ),
    array(
        array('hogehoge', 'hugahuga', 'foooooooooo'),   // usernameに挿入する値の配列
        array('foobar',   'hige',     'fizzbuzz'),      // passwordに挿入する値の配列
    ));

削除処理

削除処理は、deleteを用意しています

delete()

deleteメソッドは物理削除を行います

論理削除は今のところ実装していません

引数
なし
戻り値
削除に成功すればtrue、失敗したらfalse
$models = Model_Test::findAll();
foreach($models as $model) {
    $model->delete();       // DBから物理削除する(findAllと組み合わせて全件削除)
}

バルクインサート

大量のデータを外部から取得し、素早く一度にDBへ挿入する際に便利ですが、

現状、実装する予定はありません


モデルを扱う上での注意点

created_atとupdated_atについて

MySQLの仕様

ネクストエンジンアプリ基盤ではcreated_atのデフォルト値にCURRENT_TIMESTAMP

updated_atのデフォルト値にCURRENT_TIMESTAMP on UPDATE CURRENT_TIMESTAMPが指定されています

これらの指定が前提となっているため、ネクストエンジンアプリ基盤(PHPコード)からこれらのカラムについて一切操作を行いません

ここで気をつけるべき事項は、

既存のDBの内容と全く同じ内容でupdateを実行した場合、成功はするがupdated_atは更新されないことです

何か値を変更して更新した場合にはupdated_atは更新されます

この挙動にご注意下さい

insert, update, save時のプロパティの更新

挿入や保存をした際に、

DBにはその日時が反映されていますが、

モデルのプロパティのcreated_atupdated_atは更新されません

これらの情報をモデルのプロパティにも反映させたい場合、

挿入や更新後に、一度SELECT文を発行せざるを得ないため、パフォーマンスの観点から実装していません

具体的なコードを上げます

<?php
$model = Model_Hoge::find(1);
$model->hoge = 1;
$model->save();

$model->updated_at; // 更新されてない

/////////////////////////////////////////

$model = new Model_Hoge();
$model->hoge = 1;

$model->save();

$model->created_at; // 更新されてない
$model->updated_at; // 更新されてない

created_atupdated_atを処理に使用したい場合には、挿入および保存の挙動にご注意下さい

これら日時に関するプロパティを更新したいケースのためにフックを用意しています

そちらに日付更新の処理を実装して下さい


フック

各種DBとの通信するメソッドの前後に挟める各種フックを用意しています

フック名 フックが呼び出されるタイミング メソッドに渡される引数
before_save save(), insert(), update() before_save()
after_save save(), insert(), update() after_save(boolean $success)
before_insert insert() before_insert()
after_insert insert() after_insert(boolean $success)
before_update update() before_update()
after_update update() after_update(boolean $success)
before_delete delete() before_delete()
after_delete delete() after_delete(boolean $success)
before_validate validate() before_validate()
after_validate validate() after_validate(boolean $success)
after_find find(), findAll(), findBy(), findLike() after_find(array $record)

ほぼすべてのメソッドがインスタンスメソッドです

メソッド内部で$thisを用いればインスタンスにアクセスすることができます

ですが、find系のメソッドは全てstaticメソッドです

そのためafter_findのみstaticメソッドとなっていますご注意下さい

具体的なコードを挙げます

class Model_Admin extends Model_Base {
    public $username;
    public $password;

    // 挿入前にパスワードをハッシュ化
    // @override
    public function before_insert($query) {
        $this->password = password_hash($this->password);
    }

    // ユーザ名とパスワードを渡し、それにマッチするユーザが居ればtrueを、いなければfalseを返す
    public static function verify($username, $raw_password) {
        $admin = self::findBy('username', $username);
        if(is_null($admin)) return false;

        $admin = $admin[0];
        if(password_verify($raw_password, $admin->password)) {
            return true;
        } else {
            return false;
        }
    }
}

更新履歴
  • 2015/02/17: モデル - Model_Baseクラスページ作成