コンテンツにスキップ

Eloquent ORM@Laravel

はじめに

本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。


01. Eloquent ORMとは

Laravelに組み込まれているORM。

Active Recordパターンで実装されている。

内部にはPDOが使用されており、Laravelクエリビルダーよりも抽象度が高い。


01-02. Active Recordパターン

Active Recordパターンとは

テーブルとモデルが一対一の関係になるデザインパターンのこと。

加えて、テーブル間のリレーションシップがそのままモデル間の依存関係にも反映される。

ビジネスロジックが複雑でないアプリの開発に適している。

ActiveRecord


メリット/デメリット

項目 メリット デメリット
保守性 テーブル間のリレーションが、そのままモデル間の依存関係になるため、モデル間の依存関係を考える必要がなく、開発が早い。そのため、ビジネスロジックが複雑でないアプリの開発に適している。 ・反対に、モデル間の依存関係によってテーブル間のリレーションが決まる。そのため、複雑な業務ロジックでモデル間が複雑な依存関係を持つと、テーブル間のリレーションも複雑になっていってしまう。
・モデルに対応するテーブルに関して、必要なカラムのみでなく、全てのカラムから取得するため、アプリに余分な負荷がかかる。
拡張性 テーブル間のリレーションがモデル間の依存関係によって定義されており、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 0123 変更したレコード数
delete mixed 0123 変更したレコード数

▼ Eloquentモデル

Eloquentモデルが持つcrudを実行するメソッドの返却値型と返却値は以下の通りである。

その他のメソッドについては、以下のリンクを参考にせよ。

CRUDメソッドの種類 返却値型 返却値 返却値の説明
update bool truefalse 結果のboolean値
save bool truefalse 結果のboolean値
delete bool truefalse 結果の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);
    }
}