Category: Ethna
久々のEthnaネタです。
まだいくつかネタがあることが判明しましたので、徐々に公開していこうと思います。

AppObjectでRailsのActiveRecordでいうBelongsTo/HasOne/HasMany/HasAndBelongsToMany/HasManyThroughを実装してみました。
HasManyThroughはすごく不完全のようです。
AppObject再編が騒がれているこのタイミングというのも何かの縁でしょう。完全にその再編で思い出しただけなんですが。

ただ、だいぶ昔のバージョンでしか試していないのが若干不安です、しかし一応世の中に出しているサービスで稼働中の実績もあります。
この辺の名前は出していいのかわからないので、いったん自重しておきます。

Source

ついに、こういうライブラリもsvn管理になりました。
http://bobpp.as-roma.com/svn/ethnaLibs/trunk/projBop_AppObject.php
こちらになります。ほかの機能もいくつか混ざっているようですが、それは追って紹介していこうと思います。

使い方は続きをどうぞ。

How to Load.

libsディレクトリなどに置いてください。include_onceできれば別にどこでもかまわないです。
また、利用するすべてのAppObject/AppManagerをこのクラスのサブクラスにしてください。

AppManager/AppObjectをフル活用しているエンジニアの大半は <AppID>_AppObjectや<AppID>_AppManagerを持っているという噂なので、そのアプリケーション固有の基底クラスをprojBop_AppObject, projBop_AppManagerに置き換えるだけです。

ということで、ありがちな例としては、
include_once 'libs/projBop_AppObject.php';
class AppId_AppObject extends projBop_AppObject { ... }
class AppId_AppManager extends projBop_AppManager { ... }
このような形でしょうか。

has_one/belongs_to

早速has_one/belongs_toから。

例として、「ユーザはプロフィールを持つ」という関係を書いてみようと思います。
class AppID_User extends projBop_AppObject {
  var $has_one = array(
    'profile' => array(
      'key' => 'user_id',
    ),
  );

  ...
}

class AppID_Profile extends projBop_AppObject {
  var $belongs_to = array(
    'user' => array(
      'key' => 'user_id',
    ),
  );

  ...
}
このような宣言をする必要がありますが、Rails風の命名規則、外部キーはリレーション先のテーブル名_id の規則に則っていれば省略可能なので、上記の例は、
class AppID_User extends projBop_AppObject {
  var $has_one = array(
    'profile' => null,
  );

  ...
}

class AppID_Profile extends projBop_AppObject {
  var $belongs_to = array(
    'user' => null,
  );

  ...
}
とすることもできます。この命名規則はここから先のすべての例について適用されます。

get

ではさっそく関連オブジェクトの取得を行ってみましょう。
まずは、User#id=1のプロフィールオブジェクトを取得する例です。
$user = &$this->backend->getObject('AppID_User''id', 1);
$profile = $user->profile->get();
このようにすればOKです。「profile」はリレーションの定義に用いたキー名をそのまま利用しています。
そのgetメソッドで関連オブジェクトが取得できます。

では、Profileからユーザを取得する場合は?
$profile = &$this->backend->getObject('AppID_Profile''id', 1);
$user = $profile->user->get();
これだけです。非常に簡単ですね。

set

取得できたら保存も。
$profile = &$this->backend->getObject('AppID_Profile');
$profile->set('description''Ethna-AppObject-Relations');

$user = &$this->backend->getObject('AppID_User''id', 1);
$user->profile->set($profile);
これで $profileのuser_id は1として保存されます。また、$profileの保存も自動的に行います。

こちらも逆にProfileからUserをセットする場合は?
$profile->user->set($user); このようにかわるだけです。自動的な保存も同様に行われます。

has_many/belongs_to

続いてHasMany/BelongsTo。

今回は、「ユーザの出身都道府県」という関係を。まずは定義から。
class AppID_Pref extends projBop_AppObject {
  var $has_many = array(
    'user' => array(
      'key' => 'pref_id',
    )
  );

  ...
}

class AppID_User extends projBop_AppObject {
  var $belongs_to = array(
    'pref' => array(
      'key' => 'pref_id',
    ),
  );

  ...
}
このようになります。先の命名規則による省略を用いると、
class AppID_Pref extends projBop_AppObject {
  var $has_many = array(
    'user' => null,
  );

  ...
}

class AppID_User extends projBop_AppObject {
  var $belongs_to = array(
    'pref' => null,
  );

  ...
}
このように省略できます。

get/getsAll

ある都道府県にいる人全員を取得してみましょう。
$pref = &$this->backend->getObject('AppID_Pref''id', 1);
$users = $pref->user->getAll();
たったこれだけです。Pref#id=1にいるUser#id=2だけを取得する際は、
$user = $pref->user->get(2); こうなります。

逆に、User#id=2の都道府県を取得するには、
$user = &$this->backend->getObject('AppID_User''id', 2);
$pref = $user->pref->get();
例によってこれだけです。

find

都道府県からみたユーザはたくさんありますが、条件を指定して検索をかけることもfindメソッドで可能です。
今回は「性別が男 (gender = M) であるユーザを生年月日若い順(ORDER BY birthday ASC)に10件(LIMIT 10)」という検索条件を加えてみます。

書き方としては、AppManager#getObjectListのfilter等と同じなので取っ付きやすいと思います。
$pref = &$this->backend->getObject('AppID_Pref''id', 1);
$filter = array(
  'gender' => 'M',
);
$order = array(
  'birthday' => OBJECT_SORT_ASC,
);
$limit = 10;
$users = $pref->user->find($filter$order$limit);
このようにすることで検索も容易に行えます。

set

保存をしてみましょう。
新しく作ったユーザをPref#id=2に所属させてみましょう。
$user = &$this->backend->getObject('AppID_User');
$user->set('name''BoBpp');

$pref = &$this->backend->getObject('AppID_Pref''id', 2);
$user->pref->set($pref);
このようになります。$userは pref->set した時点で、外部キーの設定がされて自動的に保存されます。

逆のやり方としては、
$pref->user->set($user); このようにかわるだけです。こちらも自動的に保存がされます。

HasAndBelongsToMany

HABTMにも対応します。

「ユーザへのタグ付け」という例を。まずは、定義からですが、
ユーザとタグの関係を表現するテーブルを tag_user テーブルとして、
  • id
  • tag_id
  • user_id
このようなテーブルだとします。
class AppID_User extends projBop_AppObject {
  var $has_and_belongs_to_many = array(
    'tag' => array(
      'relation' => 'tag_user',
      'from_key' => 'user_id',
      'dest_key' => 'tag_id',
    ),
  );

  ...
}

class AppID_Tag extends projBop_AppObject {
  var $has_and_belongs_to_many = array(
    'user' => array(
      'relation' => 'tag_user',
      'from_key' => 'tag_id',
      'dest_key' => 'user_id',
    ),
  );

  ...
}
このようになります。
relationはこの二つのリレーションを表すテーブルです。[id, user_id, tag_id]のみのテーブルとなります。
from_keyはrelationテーブルのカラムの中で、自身の外部キーが入るカラム名を指定してください。
dest_keyはrelationテーブルのカラムの中で、相手の外部キーが入るカラム名を指定してください。

なお、上記三つも省略できます。from_key, dest_keyはBelongsTo,HasOne,HasManyのkeyと同じ命名規則です。
relationは、二つのモデル名を比較して小さい方を前方に置いた、モデル名_モデル名 となります。
よって、上の例も省略記法を用いると、
class AppID_User extends projBop_AppObject {
  var $has_and_belongs_to_many = array(
    'tag' => null,
  );

  ...
}

class AppID_Tag extends projBop_AppObject {
  var $has_and_belongs_to_many = array(
    'user' => null,
  );

  ...
}
このように省略できます。

get/getsAll

使い方は基本的にHasManyの項目と同じですが、、、
Tag#id=1がついたユーザ群を取得するには、
$tag = &$this->backend->getObject('AppID_Tag''id', 1);
$users = $tag->user->getAll();
こうなります。
Tag#id=1がついているUser#id=2を取得する場合は、 $user = $tag->user->get(2); こう変わります。

set

保存もしてみましょう。
User#id=1にTag#id=2をつけてみましょう。
$user = &$this->backend->getObject('AppID_User''id', 1);
$tag = &$this->backend->getObject('AppID_Tag''id', 2);

$user->tag->set($tag);
このようにします。今回は$user,$tagともに保存済みであることが求められます。ここは要注意です。

HasAndBelongsToManyにはfindがありません。いずれ実装されるのかもしれません。。。?

HasManyThrough

不完全ではありますが、一応あります。

では、「ユーザは複数のグループに所属する」という関係を扱ってみましょう。まずは定義ですが、
所属する関係をmembershipテーブルで表現し、
  • id
  • user_id
  • group_id
  • joined_at
  • level
このようなテーブルとします。
class AppID_Group extends projBop_AppObject {
  var $has_many_through = array(
    'user' => array(
      'relation' => 'membership',
      'from_key' => 'group_id',
      'dest_key' => 'user_id',
    ),
  );

  ...
}

class AppID_User extends projBop_AppObject {
  var $has_many_through = array(
    'group' => array(
      'relation' => 'membership',
      'from_key' => 'user_id',
      'dest_key' => 'group_id',
    ),
  );

  ...
}

class AppID_Membership extends projBop_AppObject {
  var $belongs_to = array(
    'group' => array(
      'key' => 'group_id',
    ),
    'user' => array(
      'key' => 'user_id',
    ),
  );
  
  ...
}
こうなります。HasManyThroughの場合はrelationだけ省略不可です。そのため、省略を行うと、、、
class AppID_Group extends projBop_AppObject {
  var $has_many_through = array(
    'user' => array(
      'relation' => 'membership',
    ),
  );

  ...
}

class AppID_User extends projBop_AppObject {
  var $has_many_through = array(
    'group' => array(
      'relation' => 'membership',
    ),
  );

  ...
}

class AppID_Membership extends projBop_AppObject {
  var $belongs_to = array(
    'group' => null,
    'user'  => null,
  );
  
  ...
}
このようになります。

get/getsAll

get/getsAllもちょっと特殊です。

単純にUser#id=1が所属するグループを取得するだけであれば、
$user = &$this->backend->getObject('AppID_User''id', 1);
$groups = $user->group->getsAll();
$group  = $user->group->get(2); // User#id=1が所属しているGroup#id=2を取得
とHABTMと同じように扱えるのですが、Relationのオブジェクト ここではMembershipも欲しい場合は、
$groups = $user->group->getsAll(TRUE);
foreach ($groups AS $g) {
  $membership = $g->RelationObject;
}

// Group#id=3だけの場合は、
$group = $user->group->get(3, TRUE);
$membership = $group->RelationObject;
と、get/getsAllの引数にTrueを追加するとリレーションのリレーションのオブジェクトを取得することができます。
RelationObjectというプロパティ名は、現在は固定です。

set

では保存もしてみましょう。
User#id=1をGroup#id=4に保存してみましょう。
$user = &$this->backend->getObject('AppID_User''id', 1);
$group = &$this->backend->getObject('AppID_Group''id', 4);

$user->group->set($group);
これだけです。しかし、Membershipオブジェクトのjoined_at, levelもセットしたいですね。そんなときは、
$user = &$this->backend->getObject('AppID_User''id', 1);
$group = &$this->backend->getObject('AppID_Group''id', 4);
$membership = &$this->backend->getObject('AppID_Membership');
$membership->set('joined_at'time());
$membership->set('level', 10);

$user->group->set($group$membership);
このようにsetの第2引数にリレーションオブジェクトを設定すれば同時に保存されます。

最後に

今回は、過去最大級のライブラリ?をこんなタイミングでリリースした訳ですが、AppObjectもこれくらいはできるぜ。
これでAppObjectでサクサク開発していただければと思います。

バグとかもしございましたら、ご連絡ください。
Posted by BoBpp at 22:33:50 |はてなブックマークに追加

Trackback

Trackback
There are currently no trackbacks for this item.
Use this TrackBack url to ping this item (right-click, copy link target). If your blog does not support Trackbacks you can manually add your trackback by using this form.

Comment

なんだかんだいってちょっと手を加えればActiveRecord になるんですよね。今のEthna_AppObjectって。

僕もちょっと足りないだけじゃん、と思っていましたが、まあPEAR依存の排除もどうにかしないといけませぬ(*´~`)
Posted by mumumu | 04/03/09 14:41:27

Add Comment

:

:
: