Eloquent ORM@Laravel¶
はじめに¶
本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。
01. Eloquent ORMとは¶
Laravelに組み込まれているORM。
Active Recordパターンで実装されている。
内部にはPDOが使用されており、Laravelクエリビルダーよりも抽象度が高い。
01-02. Active Recordパターン¶
Active Recordパターンとは¶
テーブルとモデルが一対一の関係になるデザインパターンのこと。
加えて、テーブル間のリレーションシップがそのままモデル間の依存関係にも反映される。
ビジネスロジックが複雑でないアプリの開発に適している。
メリット/デメリット¶
項目 | メリット | デメリット |
---|---|---|
保守性 | テーブル間のリレーションが、そのままモデル間の依存関係になるため、モデル間の依存関係を考える必要がなく、開発が早い。そのため、ビジネスロジックが複雑でないアプリの開発に適している。 | ・反対に、モデル間の依存関係によってテーブル間のリレーションが決まる。そのため、複雑な業務ロジックでモデル間が複雑な依存関係を持つと、テーブル間のリレーションも複雑になっていってしまう。 ・モデルに対応するテーブルに関して、必要なカラムのみでなく、全てのカラムから取得するため、アプリに余分な負荷がかかる。 |
拡張性 | テーブル間のリレーションがモデル間の依存関係によって定義されており、JOIN句を使用せずに、各テーブルから必要なレコードを取得できる。そのため、テーブルを増やすやすい。 | |
可読性 | ・モデルとこれのプロパティがそのままテーブルになるため、モデルを作成するためにどのテーブルからレコードを取得するのかを推測しやすい (Userモデル ⇄ usersテーブル) 。 ・リレーションを理解する必要がなく、複数のテーブルに対して無秩序にSQLを発行するような設計実装になりにくい。 |
03. Eloquentモデル¶
テーブル設計に基づくEloquentモデル¶
▼ Eloquentモデルの継承¶
Eloquentモデルを継承したクラスは、INSERT
文やUPDATE
文などのデータアクセスロジックを使用できるようになる。
*実装例*
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
class Foo extends Model
{
// クラスチェーンによって、データアクセスロジックをコール
}
▼ テーブルの定義¶
テーブルを定義するため、table
プロパティにテーブル名を割り当てる。
ただし、table
プロパティにテーブル名を代入する必要はない。
Eloquentがクラス名の複数形をテーブル名と見なし、これをスネークケースにした文字列をtable
プロパティに自動的に代入する。
また、テーブル名を自前で命名したい場合は、代入によるOverrideを行っても良い。
*実装例*
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
class Foo extends Model
{
/**
* @var string
*/
protected $table = "foos"; // Eloquentモデルと関連しているテーブル
}
▼ テーブル間リレーションシップの定義¶
ER図における各テーブルのリレーションシップを元に、モデル間の関連性を定義する。
hasOne
メソッド、hasMany
メソッド、belongsTo
メソッドを使用して表す。
*実装例*
Departmentモデルで、hasMany
メソッドを使用して、Departmentモデル (親) とEmployeesモデル (子) のテーブル関係を定義する。
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Department extends Model
{
/**
* @var string
*/
protected $primaryKey = "department_id"; // 主キーとするカラム
/**
* @return HasMany
*/
public function employees(): HasMany
{
// 一対多の関係を定義します。
// デフォルトではemployee_idに紐付けます。
return $this->hasMany(Employee::class);
}
}
また、Employeesモデルでは、belongsTo
メソッドを使用して、Departmentモデル (親) とEmployeesモデル (子) のテーブル関係を定義する。
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Employee extends Model
{
/**
* @var string
*/
protected $primaryKey = "employee_id"; // 主キーとするカラム
/**
* @return BelongsTo
*/
public function department(): BelongsTo
{
// 多対一の関係を定義します。
// デフォルトではdepartment_idに紐付けます。
return $this->belongsTo(Department::class);
}
}
リレーションを基にJOIN句のSQLを発行するために、Departmentモデル (親) のhasMany
メソッドを実行する。
これにより、DepartmentモデルのIDに紐付くEmployeesモデル (子) を配列で参照できる。
<?php
// Departmentオブジェクトを取得
$department = Department::find(1);
// 部署ID=1に紐付く全てのemployeeオブジェクトをarray型で取得
$employees = $department->employees()
▼ 主キーカラムの定義¶
Eloquentは、primaryKey
プロパティの値を主キーのカラム名と見なす。
keyType
プロパティで主キーのデータ型、またincrementing
プロパティで主キーの自動増分を有効化するか否か、を設定できる。
*実装例*
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
class Foo extends Model
{
/**
* @var string
*/
protected $primaryKey = "foo_id"; // 主キーとするカラム (デフォルトではidが主キー)
/**
* @var string
*/
protected $keyType = "int"; // 主キーのデータ型
/**
* @var bool
*/
public $incrementing = true; // 主キーの自動増分の有効化します。
}
▼ TIMESTAMP型カラムの定義¶
Eloquentは、timestamps
プロパティの値がtrue
の時に、Eloquentモデルに紐付くテーブルのcreated_at
カラムとupdated_at
カラムを自動的に更新する。
また、TIMESTAMP型カラム名を自前で命名したい場合は、代入によるOverideを行っても良い。
*実装例*
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
class Foo extends Model
{
const CREATED_AT = "created_date_time";
const UPDATED_AT = "updated_data_time";
/**
* @var bool
*/
protected $timestamps = true; // Eloquentモデルのタイムスタンプを更新するかの指示します。
}
▼ TIMESTAMP型カラム読み出し時のデータ型変換¶
DBからタイムスタンプ型カラムを読み出すと同時に、CarbonのDateTimeクラスに変換したい場合、data
プロパティにて、カラム名を設定する。
*実装例*
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* CarbonのDateTimeクラスに自動変換したいカラム名
*
* @var array
*/
protected $dates = [
"created_at",
"updated_at",
"deleted_at"
];
}
▼ カラムデフォルト値の定義¶
特定のカラムのデフォルト値を設定したい場合、attributes
プロパティにて、カラム名と値を定義する。
*実装例*
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
class Foo extends Model
{
/**
* カラム名とデフォルト値
*
* @var array
*/
protected $attributes = [
"is_deleted" => false,
];
}
▼ 変更できる/できないカラムの定義¶
変更できるカラム名をfillable
プロパティを使用して定義する。
カラムが増えるたびに、実装する必要がある。
*実装例*
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
class Foo extends Model
{
/**
* カラム名
*
* @var array
*/
protected $fillable = [
"name",
];
}
もしくは、変更できないカラム名をguarded
プロパティで定義する。
これらのいずれかの設定は、Eloquentモデルで必須である。
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
class Foo extends Model
{
/**
* カラム名
*
* @var array
*/
protected $guarded = [
"bar",
];
}
使用に注意する機能¶
▼ セッター¶
Laravelでは、プロパティを定義しなくても、Eloquentモデルからプロパティをコールすれば、処理の度に動的にプロパティを定義できる。
しかし、この機能はプロパティがpublicアクセスである必要があるため、オブジェクト機能のメリットを享受できない。
そのため、こを使用せずに、constructor
メソッドを使用したコンストラクタインジェクション、またはセッターインジェクションを使用するようにする。
*実装例*
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
class Foo extends Model
{
/**
* @var FooName
*/
private FooName $fooName;
/**
* 名前を取得します。
*
* @return string
*/
public function __construct(FooName $fooName)
{
$this->fooName = $fooName;
}
}
▼ ゲッター¶
Laravelでは、getFooBarAttribute
という名前のメソッドを、foo_bar
という名前でコールできる。
一見、プロパティをコールしているように見えるため、注意が必要である。
*実装例*
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
class Foo extends Model
{
/**
* @var FooName
*/
private FooName $fooName;
/**
* 名前を取得します。
*
* @return string
*/
public function getNameAttribute()
{
return $this->fooName . "です。";
}
}
<?php
$foo = Foo::find(1);
// nameプロパティを取得しているわけでなく、getNameAttributeメソッドを実行している。
$fooName = $foo->name;
データ型変換¶
▼ シリアライズ¶
フロントエンドとバックエンド間、またバックエンドとDB間のデータ送信のために、配列型オブジェクトをJSONに変換する処理はシリアライズである。
*実装例*
<?php
$collection = collect([
[
"user_id" => 1,
"name" => "佐藤太郎",
],
[
"user_id" => 2,
"name" => "山田次郎",
],
]);
// Array型に変換する
$collection->toArray();
<?php
$users = App\User::all();
// Array型に変換する
return $users->toArray();
▼ デシリアライズ¶
フロントエンドとバックエンド間、またバックエンドとDB間のデータ送信のために、JSONを配列型オブジェクトに変換する処理はデシリアライズである。
フィルタリング¶
▼ filter
メソッド¶
コールバック関数の返却値がtrue
であった要素を全て抽出する。
*実装例*
$collection = collect([1, 2, 3, 4]);
// trueを返却した要素を全て抽出する
$filtered = $collection->filter(function ($value, $key) {
return $value > 2;
});
$filtered->all();
// [3, 4]
補足として、複数の条件を設定したい時は、早期リターンを使用する必要がある。
*実装例*
$collection = collect([1, 2, 3, 4, "yes"]);
// 複数の条件で抽出する。
$filtered = $collection->filter(function ($value, $key) {
// まずはyesを検証する。
if($value == "yes") {
return true;
}
return $value > 2;
});
$filtered->all();
// [3, 4, "yes"]
▼ first
メソッド¶
コールバック関数の返却値がtrue
であった最初の要素のみを抽出する。
*実装例*
$collection = collect([1, 2, 3, 4]);
// trueを返却した要素のみ抽出する
$filtered = $collection->first(function ($value, $key) {
return $value > 2;
});
// 3
03-02. EloquentモデルとビルダーによるCRUD¶
CRUDメソッドの返却値型と返却値¶
▼ CRUDメソッドを持つクラス¶
Eloquentモデルを継承すると、以下のクラスからメソッドをコールできるようになる。
Eloquentモデルにはより上位のメソッドが定義されていないことがあり、もし定義されていないものがコールされた場合、__callStatic
メソッド (静的コールによる) や__call
メソッド (非静的コールによる) が代わりにコールされ、より上位クラスのメソッドをコールできる。
どちらの方法でコールしても同じである。
クラス | 名前空間 | __call メソッドを経由してコールできるクラス |
---|---|---|
Queryビルダー | Illuminate\Database\Query\Builder |
なし |
Eloquentビルダー | Illuminate\Database\Eloquent\Builder |
Queryビルダー、 |
Eloquentリレーション | Illuminate\Database\Eloquent\Relations\Relation |
Queryビルダー、Eloquentビルダー、 |
Eloquentモデル | Illuminate\Database\Eloquent\Model |
Queryビルダー、Eloquentビルダー、Eloquentリレーション |
▼ Eloquentビルダー¶
Eloquentビルダーが持つcrudを実行するメソッドの返却値型と返却値は以下の通りである。
その他のメソッドについては、以下のリンクを参考にせよ。
CRUDメソッドの種類 | 返却値型 | 返却値 | 返却値の説明 |
---|---|---|---|
create | collection/$this | {id:1, name: テスト} |
作成したオブジェクト |
find | collection/Builder/Model | {id:1, name:テスト} |
取得したオブジェクト |
update | mixed | 0 、1 、2 、3 |
変更したレコード数 |
delete | mixed | 0 、1 、2 、3 |
変更したレコード数 |
▼ Eloquentモデル¶
Eloquentモデルが持つcrudを実行するメソッドの返却値型と返却値は以下の通りである。
その他のメソッドについては、以下のリンクを参考にせよ。
CRUDメソッドの種類 | 返却値型 | 返却値 | 返却値の説明 |
---|---|---|---|
update | bool | true 、false |
結果のboolean値 |
save | bool | true 、false |
結果のboolean値 |
delete | bool | true 、false |
結果のboolean値 |
CREATE¶
▼ create
メソッド¶
INSERT文を実行する。
Eloquentモデルにはcreate
メソッドがないため、代わりにEloquentビルダーが持つcreate
メソッドがコールされる。
create
メソッドに挿入先のカラムと値を渡し、これを実行する。
別の方法として、Eloquentビルダーのfill
メソッドで挿入先のカラムと値を設定し、save
メソッドを実行しても良い。
save
メソッドはUPDATE
処理も実行できるが、fill
メソッドでID値を割り当てない場合は、CREATE
処理が実行される。
create
メソッドまたはsave
メソッドによるCREATE
処理では、レコードの挿入後に、lastInsertId
メソッドに相当する処理が実行される。
これにより、挿入されたレコードのプライマリーキーが取得され、EloquentモデルのID値のプロパティに保持される。
*実装例*
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Foo;
use Illuminate\Http\Request;
class FooController extends Controller
{
/**
* @param Request $request
*/
public function createFoo(Request $request)
{
$foo = new Foo();
// INSERT文を実行する。また同時にIDを取得する。
$foo->create($request->all());
// 以下の実装でもよい
// $foo->fill($request->all())->save();
// 処理後にはEloquentモデルにID値が保持されている。
$foo->id();
// 続きの処理
}
}
Eloquentモデルにはfillable
プロパティを設定しておく。
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
class FooDTO extends Model
{
// 更新できるカラム
protected $fillable = [
"name",
"age",
];
}
READ¶
▼ all
メソッド¶
レコードを全て取得するSELECT句を発行する。
MySQLを含むDBエンジンでは、取得結果に標準の並び順が存在しないため、プライマリーキーの昇順で取得したい場合は、orderBy
メソッドを使用して、明示的に並び替えるようにする。
Eloquentモデルにはall
メソッドがないため、代わりにEloquentビルダーが持つall
メソッドがコールされる。
全てのプライマリーキーのCollection型を配列型として返却する。
toArray
メソッドで配列型に再帰的に変換できる。
*実装例*
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Foo;
use Illuminate\Database\Eloquent\Collection;
class FooController extends Controller
{
/**
* @return Collection
*/
public function findAll(): Collection
{
$foo = new Foo();
return $foo->all();
}
}
▼ find
メソッド¶
レコードを1つ取得するSELECT句を発行する。
Eloquentモデルにはfind
メソッドがないため、代わりにEloquentビルダーが持つfind
メソッドがコールされる。
引数としてプライマリーキーを渡した場合、指定したプライマリーキーを持つEloquentモデルを返却する。
toArray
メソッドで配列型に変換できる。
*実装例*
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Foo;
use Illuminate\Database\Eloquent\Collection;
class FooController extends Controller
{
/**
* @param int $id
* @return Collection
*/
public function findById(int $id): Collection
{
$foo = new Foo();
return $foo->find($id);
}
}
▼ first
メソッド¶
取得されたコレクション型データの1つ目の要素の値を取得する。
ユニーク制約の課せられたカラムをwhere
メソッドの対象とする場合、コレクションとして取得されるが、コレクションが持つEloquentモデルは1つである。
foreachを使用してコレクションからEloquentモデルを取り出しても良いが、無駄が多い。
そこで、first
メソッドを使用して、Eloquentモデルを直接的に取得する。
*実装例*
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Foo;
use Illuminate\Database\Eloquent\Collection;
class FooController extends Controller
{
/**
* @param string $emailAddress
* @return Foo
*/
public function findByEmail(string $emailAddress): Foo
{
$foo = new Foo();
return $foo->where('foo_email', $emailAddress)->first();
}
}
▼ limit
メソッド、offset
メソッド¶
開始地点から指定した件数のレコードを全て取得するSELECT句を発行する。
これにより、ページネーションで、1ページ当たりのレコード数 (limit
) と、次のページの開始レコード (offset
) を定義できる。
これらのパラメーターはクエリパラメーターとして渡すと良い。
*実装例*
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Foo;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
class FooController extends Controller
{
/**
* @param Request $request
* @return Collection
*/
public function findAllByPagination(Request $request): Collection
{
$foo = new Foo();
return $foo->offset($request->offset)
->limit($request->limit)
->get();
}
}
▼ orderBy
メソッド¶
指定したカラムの昇順/降順でレコードを並び替えるSELECT句を発行する。
並び替えた結果を取得するためには、get
メソッドを使用する。
プライマリーキーの昇順で取得する場合、all
メソッドではなく、orderBy
メソッドを使用して、プライマリーキーの昇順を明示的に設定する。
*実装例*
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Foo;
use Illuminate\Database\Eloquent\Collection;
class FooController extends Controller
{
/**
* @return Collection
*/
public function findAllByAsc(): Collection
{
$foo = new Foo();
// 昇順
return $foo->orderBy('foo_id', 'asc')->get();
}
/**
* @return Collection
*/
public function findAllByDesc(): Collection
{
$foo = new Foo();
// 降順
return $foo->orderBy('foo_id', 'desc')->get();
}
}
▼ sortBy
メソッド¶
指定したカラムの昇順でレコードを並び替えるSELECT句を発行する。
*実装例*
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Foo;
use Illuminate\Database\Eloquent\Collection;
class FooController extends Controller
{
/**
* @return Collection
*/
public function findAllByAsc(): Collection
{
$foo = new Foo();
return $foo->all()->sortBy('foo_id');
}
}
▼ sortByDesc
メソッド¶
指定したカラムの降順でレコードを並び替えるSELECT句を発行する。
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Foo;
use Illuminate\Database\Eloquent\Collection;
class FooController extends Controller
{
/**
* @return Collection
*/
public function findAllByDesc(): Collection
{
$foo = new Foo();
return $foo->all()->sortByDesc('foo_id');
}
}
▼ with
メソッド¶
親テーブルにアクセスして全てのデータを取得し、親テーブルのEloquentモデルのプロパティに子テーブルのレコードを保持する。
この仕組みをEagerロードという。
Eloquentモデルにはwith
メソッドがないため、代わりにEloquentビルダーが持つwith
メソッドがコールされる。
テーブル間に一対多 (親子) のリレーションシップがある場合に使用する。
N+1問題を防げる。
ただし、with
メソッドに他のメソッドをチェーンしてしまうと、Eagerロードの後にSQLを発行されてしまうため、Eagerロードの恩恵を得られなくなることに注意する。
*実装例*
コントローラーにて、Department (親) と、これに紐付くEmployee (子) を読み出す。
これらのモデルの間では、hasMany
メソッドとbelongsTo
メソッドを使用して、テーブルにおける一対多のリレーションを定義しておく。
<?php
namespace App\Http\Controllers;
use App\Models\Department;
class EmployeeController
{
public function getEmployeesByDepartment()
{
$department = new Department();
// Departmentに属するEmployeesを全て読み出します。
// (departments : employees = 1 : 多)
$employees = $department->with("employees")->get();
foreach ($employees as $employee) {
// ここではDBアクセスはせずに、プロパティに保持された値を取得するだけ。
$name = $employee->name;
}
// 続きの処理
}
}
Department (親) に、departmentsテーブルとemployeesテーブルの間に、一対多の関係を定義する。
<?php
namespace App\Models\Department;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Department extends Model
{
/**
* 主キーとするカラム
*
* @var string
*/
protected $primaryKey = "department_id";
/**
* 一対多の関係を定義します。
* (デフォルトではemployee_idに紐付けます)
*
* @return HasMany
*/
public function employees(): HasMany
{
return $this->hasMany(Employee::class);
}
}
また、Employee (子) に、反対の多対一の関係を定義する。
<?php
namespace App\Models\Department;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Employee extends Model
{
/**
* 主キーとするカラム
*
* @var string
*/
protected $primaryKey = "employee_id";
/**
* 多対一の関係を定義します。
* (デフォルトではdepartment_idに紐付けます)
*
* @return BelongsTo
*/
public function department(): BelongsTo
{
return $this->belongsTo(Department::class);
}
}
UPDATE¶
▼ save
メソッド¶
UPDATE文を実行する。
Eloquentビルダーのfill
メソッドで挿入先のカラムと値を設定し、save
メソッドを実行する。
save
メソッドはCREATE
処理も実行できるが、fill
メソッドでID値を割り当てた場合は、UPDATE
処理が実行される。
*実装例*
<?php
namespace App\Infrastructure\Repositories;
use App\Http\Controllers\Controller;
use App\Domain\Foo\Entities;
use Illuminate\Http\Request;
class FooController extends Controller
{
/**
* @param Request $request
*/
public function updateFoo(Request $request)
{
$foo = new Foo();
// UPDATE文を実行する。
$foo->fill($request->all())->save();
// 続きの処理
}
}
Eloquentモデルにはfillable
プロパティを設定しておく。
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
class FooDTO extends Model
{
// 更新できるカラム
protected $fillable = [
"name",
"age",
];
}
DELETE¶
▼ destroy
/delete
メソッド (物理削除)¶
DELETE文を実行する。
Eloquentモデルのdestroy
/delete
メソッドを使用する。
手順として、Eloquentビルダーのfind
メソッドで削除先のModelを検索する。
返却されたEloquentビルダーのdestroy
/delete
メソッドをコールし、自身を削除する。
▼ SoftDeletesの有効化 (論理削除)¶
削除フラグを更新するUPDATE文を実行する。
Eloquentモデルのdestroy
/delete
メソッドを使用する。
手順として、テーブルに対応するModelにて、SoftDeletesのTraitを読み込む。
DBマイグレーション時に追加されるdelete_at
カラムをSQLで取得する時に、DataTimeクラスに変換できるようにしておく。
*実装例*
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class FooDTO extends Model
{
/**
* Traitの読み出し
*/
use SoftDeletes;
/**
* 読み出し時にCarbonのDateTimeクラスへ自動変換するカラム
*
* @var array
*/
protected $dates = [
"deleted_at"
];
}
DBマイグレーションファイルにてsoftDeletes
メソッドを使用すると、削除フラグとしてdeleted_at
カラムが追加されるようになる。
deleted_at
カラムのデフォルト値はNULL
である。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateFooTable extends Migration
{
/**
* マイグレート
*
* @return void
*/
public function up()
{
Schema::create("foo", function (Blueprint $table) {
...
// deleted_atカラムを追加する。
$table->softDeletes();
...
});
}
/**
* ロールバック
*
* @return void
*/
public function down()
{
Schema::drop("foo");
}
}
上記の状態で、同様にdestroy
/delete
メソッドを使用して、自身を削除する。
物理削除ではなく、deleled_at
カラムが更新されるようになる。
find
メソッドは、deleled_at
カラムがNULL
でないレコードを読み出さないため、論理削除を実現できる。
N+1問題の解決¶
▼ N+1問題とは¶
親テーブルを経由して子テーブルにアクセスする時に、親テーブルのレコード数分のSQLを発行してしまうアンチパターンのこと。
▼ 問題が起こる実装¶
反復処理の中で子テーブルのレコードにアクセスしてしまう場合、N+1問題が起こる。
内部的には、親テーブルへのSQLと、Where句を持つSQLが親テーブルのレコード数分だけ発行される。
<?php
$departments = Department::all(); // 親テーブルにSQLを発行 (1回)
foreach($departments as $department) {
$department->employees; // 親テーブルのレコード数分のWhere句SQLが発行される (N回)
}
# 1回
select * from `departments`
# N回
select * from `employees` where `department_id` = 1
select * from `employees` where `department_id` = 2
select * from `employees` where `department_id` = 3
...
▼ 解決方法¶
反復処理の前に小テーブルにアクセスしておく。
データアクセス時にwith
メソッドを使用すると、親テーブルへのアクセスに加えて、親テーブルのEloquentモデルのプロパティに子テーブルのレコードを保持するように処理する。
そのため、反復処理ではプロパティからデータを取り出すだけになる。
内部的には、親テーブルへのSQLと、In句を使用したSQLが発行される。
<?php
$departments = Department::with('employees')->get(); // SQL発行 (2回)
foreach($departments as $department) {
$department->employees; // キャッシュを使用するのでSQLの発行はされない (0回)
}
# 2回
select * from `departments`
select * from `employees` where `department_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ... 100)
04. Laravelへのリポジトリパターン導入¶
背景¶
LaravelはActive Recordパターンを採用しており、これはビジネスロジックが複雑でないアプリに適している。
ただし、ビジネスロジックが複雑なアプリに対しても、Laravelを使用したい場面がある。
その場合、Laravelにリポジトリパターンを導入することが選択肢の1つになる。
リポジトリパターンについては、以下のリンクを参考にせよ。
工夫¶
▼ DTOクラスの導入¶
ビジネスロジック用ドメインモデルと、Eloquentモデルを継承した詰め替えモデル (例:DTOクラス) を用意する。
詰め替えモデルをドメインモデルに変換する処理をメソッドとして切り分けておくと便利である。
ドメインモデルとDTOクラスの間でデータを詰め替えるようにすると、DTOクラスがドメインモデルとDBの間でレコードのやり取りを仲介し、これらを疎結合にしてくれる。
そのため、Repositoryパターンを実現できる。
<?php
declare(strict_types=1);
namespace App\Infrastructure\Foo\DTOs;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
final class FooDTO extends Model
{
use HasFactory;
/**
* @var array
*/
protected $dates = [
'created_at',
'updated_at',
'deleted_at',
];
/**
* @var array
*/
protected $fillable = [
'name',
'age',
];
/**
* @var int
*/
private int $id;
/**
* @var string
*/
private string $name;
/**
* @var int
*/
private int $age;
/**
* @var string
*/
private string $email;
/**
* @return Foo
*/
public function toFoo(): Foo
{
return new Foo(
new FooId($this->id),
new FooName($this->name),
new FooAge($this->age),
);
}
}
CREATE¶
▼ create
メソッド¶
*実装例*
<?php
namespace App\Infrastructure\Repositories;
use App\Domain\Foo\Entities\Foo;
use App\Domain\Foo\Repositories\FooRepository as DomainFooRepository;
use App\Infrastructure\Foo\DTO\FooDTO;
class FooRepository extends Repository implements DomainFooRepository
{
/**
* @var FooDTO
*/
private FooDTO $fooDTO;
public function __construct(FooDTO $fooDTO)
{
$this->fooDTO = $fooDTO;
}
/**
* @param Foo $foo
* @return void
*/
public function create(Foo $foo): void
{
$this->fooDTO
// INSERT文を実行する。
->create([
// ドメインモデルのデータをDTOに詰め替える。
"name" => $foo->name(),
"age" => $foo->age(),
]);
// 以下の実装でも良い。
// $this->fooDTO
// ->fill([
// "name" => $foo->name(),
// "age" => $foo->age(),
// ])
// ->save();
}
}
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
class FooDTO extends Model
{
// 更新できるカラム
protected $fillable = [
"name",
"age",
];
}
READ¶
▼ find
メソッド¶
*実装例*
<?php
namespace App\Infrastructure\Foo\Repositories;
use App\Domain\Foo\Entities\Foo;
use App\Domain\Foo\Ids\FooId;
use App\Domain\Foo\Repositories\FooRepository as DomainFooRepository;
use App\Infrastructure\Foo\DTOs\FooDTO;
class FooRepository extends Repository implements DomainFooRepository
{
/**
* @var FooDTO
*/
private FooDTO $fooDTO;
public function __construct(FooDTO $fooDTO)
{
$this->fooDTO = $fooDTO;
}
/**
* @param FooId $fooId
* @return Foo
*/
public function findById(FooId $fooId): Foo
{
$fooDTO = $this->fooDTO
->find($fooId->id());
// DBアクセス処理後のDTOをドメインモデルに変換する。
return new Foo(
$fooDTO->id(),
$fooDTO->name(),
$fooDTO->age(),
$fooDTO->email()
);
}
}
▼ all
メソッド¶
*実装例*
<?php
namespace App\Infrastructure\Foo\Repositories;
use App\Domain\Foo\Entities\Foo;
use App\Domain\Foo\Repositories\FooRepository as DomainFooRepository;
use App\Infrastructure\Foo\DTOs\FooDTO;
class FooRepository extends Repository implements DomainFooRepository
{
/**
* @var FooDTO
*/
private FooDTO $fooDTO;
public function __construct(FooDTO $fooDTO)
{
$this->fooDTO = $fooDTO;
}
/**
* @return array
*/
public function findAll(): array
{
$fooDTOs = $this->fooDTO
->all();
$foos = [];
foreach ($fooDTOs as $fooDTO)
// DBアクセス後のDTOをドメインモデルに変換する。
$foos = new Foo(
$fooDTO->id(),
$fooDTO->name(),
$fooDTO->age(),
$fooDTO->email(),
);
return $foos;
}
}
▼ with
メソッド¶
UPDATE¶
▼ save
メソッド¶
*実装例*
<?php
namespace App\Infrastructure\Foo\Repositories;
use App\Domain\Foo\Entities\Foo;
use App\Domain\Foo\Repositories\FooRepository as DomainFooRepository;
use App\Infrastructure\Foo\DTOs\FooDTO;
class FooRepository extends Repository implements DomainFooRepository
{
/**
* @var FooDTO
*/
private FooDTO $fooDTO;
public function __construct(FooDTO $fooDTO)
{
$this->fooDTO = $fooDTO;
}
/**
* @param Foo $foo
* @return void
*/
public function save(Foo $foo): void
{
$this->fooDTO
// ドメインモデルのデータをDTOに詰め替える。
->fill([
"name" => $foo->name(),
"age" => $foo->age(),
])
// UPDATE文を実行する。
->save();
}
}
<?php
namespace App\Domain\DTO;
use Illuminate\Database\Eloquent\Model;
class FooDTO extends Model
{
// 更新できるカラム
protected $fillable = [
"name",
"age",
];
}
DELETE¶
▼ destroy
/delete
メソッド¶
*実装例*
<?php
namespace App\Infrastructure\Repositories;
use App\Domain\Foo\Entities\Foo;
use App\Domain\Foo\Ids\FooId;
use App\Domain\Foo\Repositories\FooRepository as DomainFooRepository;
use App\Infrastructure\Foo\DTOs\FooDTO;
class FooRepository extends Repository implements DomainFooRepository
{
/**
* @var FooDTO
*/
private FooDTO $fooDTO;
public function __construct(FooDTO $fooDTO)
{
$this->fooDTO = $fooDTO;
}
/**
* @param FooId $fooId
* @return void
*/
public function delete(FooId $fooId): void
{
// destroyメソッドでレコードを削除する。
$this->fooDTO->destroy($fooId->id());
// deleteメソッドを使用しても良い。
// $this->fooDTO->find($fooId->id())->delete();
}
}
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class FooController extends Controller
{
public function __construct(FooRepository $fooRepository)
{
$this->fooRepository = $fooRepository;
}
/**
* @param FooId $fooId
* @return mixed
*/
public function delete(FooId $fooId)
{
$this->fooRepository
->delete($fooId);
return response()->view("foo")
->setStatusCode(200);
}
}