Laravelのデータ操作方法について
今回調べてみようと思ったきっかけはドメイン駆動設計
やアーキテクチャ
について、学んでいる最中なのですが、「EloaquentはActiveRecord型のためDDDには不向き
」的な意見が多く、Laravelで利用できるデータ操作方法が取得する側のクラスにどう影響を与えるのか整理しておきたいと思ったため。
また、Eloaquentを使わない場合に代替案としてどんな方法があるのか(メリデメを知りたい)と思ったため。
DoctrineはLaravelで標準で使えるものではないがライブラリで簡単に導入できることと、SymfonyはDDDに向いているという話を聞いたことがあったので、Symfonyで標準利用できるDoctrineも検証内容に含ました。
結論をまとめておく
後で見返せるように各取得方法を羅列して行ったところ、かなり冗長になったので最初に纏めておきます。
自身の整理用の基本的な内容が続くので、考察とか諸々
の段落まではサラッと読み流して大丈夫だと思います。
全件取得 | id指定(find) | 検索 | リレーション | |
---|---|---|---|---|
PDO | 配列[※連想配列] | - | 配列[※1 連想配列] | 配列[※1 連想配列] |
QueryBuilder | Collectionオブジェクト[stdClassのオブジェクト] | stdClassのオブジェクト | Collectionオブジェクト[stdClass] | Collectionオブジェクト[stdClassのオブジェクト] |
Eloaquent | Collectionオブジェクト[※2 Modelオブジェクト] | ※2 Modelオブジェクト | Collectionオブジェクト[※2 Modelオブジェクト] | ※2 Modelオブジェクト |
Doctrine | 配列[※4 Entityオブジェクト] | Entityオブジェクト | Entityオブジェクト | Entityオブジェクト[PersistentCollection] |
※1 連想配列を指定した場合
※2 ModelオブジェクトはEloaquent由来
※3 PersistentCollectionのオブジェクトはDoctrine由来
※4 Entityクラスのメンバは自由に操作可能
使いやすい便利機能を利用すると依存度は高くなる
特定の技術を用いた便利機能をいろんな箇所で使っていると依存度は高くなります。
Eloaquentはデータベース構造と密結合のModelオブジェクトを返していることがわかります。
実際に取得方法とそのデータについて見ていきます。
PDO
PDO(PHP Data Object)とは、PHP標準(5.1.0以降)で利用することのできるデータベース接続クラスのことです。
今回の記事では本筋ではないのでサラッと流しますが、詳しくおさらいしておきたい方はこちらの記事をどうぞ→【PHP超入門】クラス~例外処理~PDOの基礎
Laravelで利用する場合はDBクラスを用いることで簡単に利用できます。
$pdo = DB::connection()->getPdo();
$users = $pdo->query('select * from users');
PDOにquery()を実行すると返り値はPDOStatement クラス
となります。(ドキュメンはこちら)
一つずつ結果を見ていきます。
[PDO] 全件取得
//全件取得
$pdo = DB::connection()->getPdo();
$users = $pdo->query('select * from users')->fetchAll(PDO::FETCH_ASSOC); //キーを連想配列に指定した場合
-
FETCH_ASSOC
を指定しているため3件分のユーザーの一覧があり、その中に連想配列という形になっています。
[PDO] id指定(find)
省略
[PDO] 検索
//検索
$pdo = DB::connection()->getPdo();
$users = $pdo->query('select * from users where name = "モーリー"')
->fetchAll(PDO::FETCH_ASSOC);
- 検索に該当したusersテーブルのデータが配列形式で取得(今回は1件分)でその中に連想配列が入っています。
[PDO]リレーション(posts)
//リレーション(posts)
$pdo = DB::connection()->getPdo();
$users = $pdo->query('select * from users inner join posts on posts.user_id = users.id where users.name = "モーリー"')
->fetchAll(PDO::FETCH_ASSOC);
- SQLの実行結果が配列データとなるので、同じユーザーのデータで計2件の場合は後述するORMで取得した場合よりも、データ取得方法よりも冗長な形で取得しています
- usersテーブルのカラムとpostsテーブルのカラムが区別なく同列に扱われていることも実際のデータを見てみることでわかるかと思います。
QueryBuilder
クエリビルダはLaravelがサポートするすべてのデータベースシステムで機能します。つまりデータベースがMySQL・PostgreSQLといった種類が変わっても抽象化してくれます。
またPDOパラメーターバインディングを利用することでSQLインジェクション攻撃の対策も行ってくれます。
クエリビルダのドキュメントはこちら
PDOを利用した記述に比べると、記述が直感的になっています。発行されるSQLの条件を増やしたい場合はチェーンメソッドを増やしていく形になるため、SQLが組める人にとっては複雑な条件の場合にもイメージしやすい取得方法になります。
$users = DB::table('users');
まずテーブルの情報を取得してみます。
こちらの中身を見てみるとIlluminate\Database\Query\Builder
クラスであるとこがわかります。こちらに用意されている便利なメソッドを使って、冒頭の一覧にあるデータを取得していきます。(各メソッドについてはこちらのドキュメントをどうぞ)
[クエリビルダ] 全件取得
// 全件取得
$users = DB::table('users')->get();
-
ユーザーテーブルにある3レコード分が配列となっていますが、
Illuminate\Support\Collection
で囲っているインスタンスが帰っていることがわかります。 -
Collectionは配列データを操作しやすくするためのラッパーです。(Collectionのドキュメントはこちら)
-
さらに配列の中の1レコード分のデータに着目してみるとオブジェクトになっています。(PDOの際は配列の中に連想配列が入っていました。)
-
クエリビルダを用いた場合はオブジェクトの中身は構造をしていて、これをPHPのstdClassと言って、
プロパティやメソッドを一切持たない標準クラス
になります。(stdClassの説明は【PHP】stdClassについて) -
型として扱える他にオブジェクトにすることで、
$obj->hoge
のように表記することができます。
[クエリビルダ] id指定(find)
// id指定(find)
$users = DB::table('users')->find(1);
- PDOの時と違い、クエリビルダの
find()
を利用することで該当する1つのデータのみをstdClass
のオブジェクトとして取得できることになります - 型を持つデータとして扱いやすくなりました。
[クエリビルダ] 検索
//検索
$users = DB::table('users')->where('name', 'モーリー')->get();
- データの形式としては全件取得の時と同じ形式で、該当しないレコード分が取り除かれた状態になる
[クエリビルダ] リレーション(posts)
//リレーション(posts)
$users = DB::table('users')->join('posts', 'users.id', '=', 'posts.user_id')->where('users.name', 'モーリー')->get();
- データの形としては検索の時の状態に、子テーブルであるpostsのカラムが追加された形。
- postsテーブルのカラムにアクセスする際は
$users[0]->content
として取得することになる。
Eloquent
EloquentはLaravelで標準的に備えられているオブジェクトリレーショナルマッパー(ORM)
です。
モデルクラスを利用して各データベーステーブルのデータの扱いを簡単にする機能です。
Eloquentについて仕組みを詳しく知りたい方はこちらの記事を呼んでみるのがいいと思います。(バージョンによる違いはあるかもしれないので最新のソースを追いつつ読むのがいいかと)→ 【Laravel】 第1回 Eloquent ソースコードリーディング - モデルの取得
[Eloaquent]全件取得
// 全件取得
$user = User::all();
- これまでのクエリビルダやPDOで取得できるデータに比べるとかなり多くの情報が付与されていることがわかります。
- Collectionオブジェクトとして取得した中のデータに関してはEloquent由来のモデルクラスである
Illuminate\Database\Eloquent\Model
を継承するApp\Models\Post
のオブジェクトが返りました。 - これにより、Eloquentの持つ多機能かつ強力な機能を利用することができるようになります(Eloquentのドキュメントはこちら)
- 後述しますが、ドキュメントに記載のある便利な機能をControllerやServiceクラスで利用した場合に激しくこの機能をもつクラスに依存することになります。
[Eloaquent] id指定(find)
// id指定(find)
$users = User::find(1);

-
find()
を利用した場合はApp\Models\User
のオブジェクトが取得できます。 - こちらもEloaquent由来なので操作しやすい便利な機能を利用できますね。
[Eloaquent] 検索
//検索
$users = User::where('name', 'モーリー')->get();
- 該当したusersテーブルのレコード分がCollection形式で取得できます。
- 各レコード単位では
App\Models\User
のオブジェクトが取得できます。
リレーション(posts)
//リレーション(posts)
$user = User::find(1);
$posts = $user->posts;
- usersの
id=1
に紐つく、postsテーブルのレコードがCollection形式で取得できます。 - 各postsレコード単位では
App\Model\Post
が取得できます。 - これはモデルにusersとposts間でリレーションを定義しているからであり、関係を定義すると、Eloquentの動的プロパティを使用して関連レコードを取得できます。(Laravel 8.x Eloquent:リレーション)
Doctrine
DoctrineはSymfonyでデフォルト導入されているデータマッパー型のORMでEntityクラスとRepositoryクラスを用いてデータを取得しオブジェクト化します
Entityとは、データベースにおける表の個別の行に対応するオブジェクトでメンバを自由にカスタムすることができます(データベース構造に依存しないオブジェクトを生成できる)
ちなみにDDDのEntityやクリーンアーキテクチャのEntityとは名前が被っているだけで、役割としてはいずれも異なる。
Repositoryとはデータの永続化や取得を担うクラスです。
Laravelでは標準的に使うことはできませんがライブラリを用いると簡単に導入できるため、比較対象として個人的に気になったので、今回は取り上げました。
Laravelでの取り入れ方はこちらの記事を参考にしました→laravelでdoctrine
全件取得
$users = $repository->findAll();
- Userテーブルの各レコードが配列に格納されている形
- レコード単位では
APP\Entities\User
のクラス由来のオブジェクトが返る - Entities以下は自分で定義しているのでクラスのメンバーは自由に編集可能(findAll()はdoctrineの機能だけどデータベース構造由来のオブジェクトが返るわけではない)
id指定(find)
$user = $repository->find(1);
-
APP\Entities\User
のクラス由来のオブジェクトが返る
検索
$users = $repository->findBy(["name" => 'モーリー']);
- Userテーブルの検索条件に該当する各レコードが配列に格納されている形
- レコード単位では
APP\Entities\User
のクラス由来のオブジェクトが返る - データの構造は全件取得と同じ
リレーション
$user = $repository->find(1);
// ...
/**
* @OneToMany(targetEntity="Post", mappedBy="user")
*/
protected $posts;
// ...
-
App\Entities\User
が返るがEntities
にアノテーションでリレーションを表現しUserクラスのメンバーに$postsを加えることでPostクラス由来のオブジェクトをメンバーのように扱うことができるようになる。アソシエーションの詳しいマッピング方法はこちら - postsのレコード単位では
PersistentCollection
というDoctrineに依存したクラスのオブジェクトが返るために、データベースの関連情報が利用箇所に漏れる
考察とか諸々
- 当たり前ですが多機能なORMを使うとその技術に対する依存度は高くなる
- 他のレイヤーとドメインモデルを疎結合な状態に保つためRepository層でデータ操作について隠蔽するドメイン駆動設計と、データベース設計と戦略的に密結合なオブジェクトを使うことでコードの記述量を減らし開発速度を上げるActiveRecord型のFWはアプローチが真逆なので不向きという文脈が理解できた。「結局妥協するなら落とし所がどこなのか」という問題と「厳密にやろうとするとRepository層でEloaquent由来のオブジェクトを詰め替える必要があり高コスト」という点。
- Doctrineは単一のオブジェクトの場合、Entityは自由に設定できるのでデータベース構造に依存しない。しかし複数のテーブルの関係をアノテーションで表現するとデータベースの関連情報にオブジェクトが引きずられる。
- xmlでマッピングすると純粋なEntityの親子関係が表現できる模様
- Laravel x Doctrineでクリーンアーキテクチャを実践されている企業さんを見つけた
- Laravel x Doctrineにすることでお行儀のいいRepositoryパターンを実装できる
- 実際にプロジェクト導入したことないので細かい問題がでてくるのかはわからない
- しかしRepositoryクラスで完全に隠蔽しようとすると結局それなりに詰め替えや記述コストがかかる。
まとめ
Laravelを技術選定するということは便利なEloaquentを使いたいということだと思うので、ORM依存はある程度受け入れるのが吉なのかなと思いました。(PHPを選定している時点で、ある程度開発速度が求められるプロジェクトだと思いますし...)
EloaquentをRepository層でのみ利用し、詰め替えするのはコストが大きいと感じました。
RDB以外(API経由など)からデータリソースを受け取ることになったならば、大人しく別のクラスとして定義する方がプロジェクト全体のコスト感は低いのかなとも思いました。
リプレイスのフェーズだけど、何某の事情で「データベースの設計は変えられず、その設計自体が微妙」みたいな特殊事情があれば、データリソースに依存せず実装できる意味でDoctrineを用いてもいいのかと思いました。
開発に関わるメンバーがSymfonyに精通しているけれど、Laravelの機能を使いたい事情があるみたいな時もLaravel x Doctrineを選択肢に入れてもいい気はします。
実際にプロジェクトで用いたことがないのでわかりませんがLaravel x DoctrineはSymfonyベースの開発だけれど、Laravelの機能を足したみたいなイメージになるのかなと思いました。
プロジェクトメンバーの入れ替わりが激しいと、独自ルールを浸透させるのが難しそうだなと思いました。
PS
調べながら書いたので間違っているところが結構あるかもしれません。