ORMを自作したので、せっかくなので紹介してみます。
使ってもらいたい、と言うよりは、こんなことを考えて作ってみた、という話を書きたかったのです。
レポジトリはgithubにありますが、まだpackagist.orgに登録してないので、git clone
で取ってくる必要があります。まだアルファ段階。
なぜORMを自作するのか?
過去に何度かORM、というかDB関連のライブラリを自作してます。前回作ったのはデータマッパーのようなORM。エンティティのキャッシュや循環参照とか入ってきて、どんどんと複雑さを増してゆくコードと格闘しながら何とか動かしました。これでORM作るのは最後にしようと思ったのですが… また作ってしまったというわけです。
思うにコーディングの練習にはORM開発はもってこいではないでしょうか。もう二度と作らないと思っても、2年ぐらいすると作ってしまうわけです。するとコーディングが上達しているのがわかるので、最初は楽しいんですよね。
さて、今回のORMですが、前回の反省から出来るだけ仕様を抑えてシンプルに作ることにしました。どう作るかというより、何を作るかを考えながら開発しました。
今回のORMの特徴
結果できたのは、おそらくActive Recordの一種。ただしレポジトリ(厳密にはDAO(Data Access Object)に近い)、エンティティ、コレクション、それとDBクエリについて、それぞれのクラスを作ることで、神クラスができないようにしてあります。
特徴は
- 当たり前ですがエンティティの作成・保存・削除・読込、そしてリレーションが可能、
- 独自のレポジトリクラスとエンティティクラスが使える(インターフェース実装の必要あり)、
- 複合キーに完全対応(してるはず)、
- Lazy/Eager Loadingに対応。ただし別途で実装。
- テーブルが別DBにあってもリレーションを作成可能なはず。
- フレームワークに依存しない。
独自クラスが使えると言っても、それなりに複雑なのでアブストラクトクラスを継承することになると思います。
逆に対応しないのは、
- エンティティのキャッシュ。
- Unit of Workのような実装。
- アノテーション・XMLなどの設定。
- コードジェネレーション。
先に書いたように、複雑さとの兼ね合いで見送ってます。
使い方(サンプルコード)
簡単なサンプルコードです。
sample database
サンプル用のDBテーブルです。
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(64) NOT NULL
);
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
contents VARCHAR(256)
);
レポジトリ作成
users
とposts
用に、それぞれレポジトリクラスを作成します。
use WScore\Repository\Repository\AbstractRepository;
class Users extends AbstractRepository
{
protected $table = 'users'; // table name
protected $primaryKeys = ['id']; // primary keys in array
protected $useAutoInsertId = true; // use auto-incremented ID.
/**
* @return RelationInterface
*/
public function posts() {
return $this->repo->hasMany($this, 'posts', ['id' => 'user_id']);
}
}
class Posts extends AbstractRepository
{
protected $table = 'posts'; // table name
protected $primaryKeys = ['id']; // primary keys in array
protected $useAutoInsertId = true; // use auto-incremented ID.
}
Repo
コンテナ
レポジトリはRepo
クラスで管理しています。
$repo = new Repo();
$container->set(PDO::class, function () {
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $pdo;
});
$repo->set('users', function(Repo $repo) {
new new Users($repo);
});
$repo->set('posts', function(Repo $repo) {
new new Posts($repo);
});
すると、こんなコードでレポジトリをとってこれます。
$users = $repo->get('users');
$posts = $repo->get('posts');
エンティティの作成と保存
$user1 = $users->create(['name' => 'my name']);
$id = $users->save($user1); // 自動追加IDが戻るよ!
echo $user1->getIdValue(); // idが入ってくるよ!
読み込んで、修正して、保存
$user1 = $users->findByKey(1);
$user1->fill(['name' => 'your name']);
$users->save($user1);
リレーション
Users
クラスのposts()
メソッド名を使います。
$user1 = $users->findByKey(1);
// postsに関連エンティティが入ってくる。
echo count($user1->posts);
foreach($user1->posts as $post) {
echo $post->contents;
};
// 今度は新規Postsを関連付け
$newPost = $posts->create(['contents' => 'test relation'])
$user1->posts[] = $newPost; // ここで関連付け!
$newPost->save(); // 保存しなくちゃ。
Eager loading
関連するエンティティをEager Loadingで読み込むには、Collection
オブジェクトを作成するところから始めます。
$user12 = $users->collecFor(['user_id' => [1, 2]]); // コレクション=エンティティの集合
$user12->load('posts'); // Users::postsを使ってEager Load!
foreach($user12 as $user) {
echo $user->name;
foreach($user->posts as $post) {
echo $post->content;
}
}
このload
メソッドでは、レポジトリのメソッドを呼び出して、RelationInterface
オブジェクトを返してもらいます。このオブジェクトを使って、関連するエンティティを一回で読み込んでいます。
何を考えてたのか
EntityはPOPOじゃないの?
本当はEntityにはPOPO(Plain Ordinary PHP Object)にしたかったのですが、そのためにはDoctrineのようなアノテーションを使ったり、複雑な設定が必要だったり、あるいは変換オブジェクトを指定する必要が出てきます。当然、複雑なコードとなります。
たとえPOPOに対応したとしても、DBレコード≒エンティティとなります。自由にクラスを設計したくても、結局はレコードに引っ張られます。そうなると、余りPOPOにこだわる必要が無いのかなとも思ってました。
そんなとき、DDDパターンを活用した Laravelアプリケーション開発のスライドを見て、レポジトリパターンが最強だなと。
ならば、使い勝手のいいActive Recordを実装して、必要であればDDD用のモデルに変換する、と割り切ったほうがいいのかなと考えました。
LazyとEagerローディング
そもそも今回のORMを作る気になったのは、Atlas.ORMがEager Loadingのみ対応と聞いたのがきっかけでした。ならLazyだけでORM作ったら簡単になるなと。
結局、Eager Loadingにも対応してしまいました。ただ、Lazyはレポジトリで、Eagerはコレクションで、それぞれ実装することでクラスの肥大化を抑えてみました。
Entity/Repository複雑すぎない?
エンティティとレポジトリなど、あちこちでインターフェースを切ってますが、想定よりAPIが多くて複雑になってしまいました。
実はORMで使われるAPIは意外と少ないです。ただ最小限のAPIだと使い勝手が悪すぎたので、便利さを追いかけているうちにAPIが増えてしまいました。
Simpleさと便利と簡単と、バランスが難しい。
PHPにもGenericsみたいなのが導入されれば、最小限のインターフェースを切っておいて、利用するときに便利なクラス・インターフェースを指定できたりして便利なのでしょうけれど。
参考にしたORM
-
Eloquent (Laravel):
APIや命名など、非常に分かりやすいので、参考にしています。 -
Atlas.ORM:
Auraを作っているPaulさんが開発しているORMです。 -
CakePHP3 database:
TableとEntityを別クラスにしているところが似てます。
結論
ORM自作はつらいけど、プログラミングスキルが上達するのが分かって楽しい。
一方で、ORM開発している時間を別の技術覚えたほうがいいのかもとも思う。前回ORMを作ったときも、これで最後にしようと思った。で、また作ってる…