PHPフレームワークのORMを徹底比較！Eloquent(Laravel) vs Model/Entity(CodeIgniter4)

## はじめに

私は現在(2022/11時点)社内システム開発をメインに担当しています。

執筆時点で社内システムは10個以上存在し、各システムのフレームワークもバラバラです。
その中でも比較的採用数が多いのが **Slim4+Eloquent** と **CodeIgniter(3 or 4)** になります。

**Eloquent** はもともとLaravelフレームワークのORMとして提供されているサービスであり、多機能であることが特徴です。
一方、**CodeIgniter** はLaravelに比べて軽量でパフォーマンスが良いのが特徴です。
バージョン4になってからは様々な機能が追加され、3以前でも存在していた **Model** に加え、取得レコードをエンティティとして扱うことに特化した **Entity** が新たに導入されました。[^1]
2022年現在も機能が追加されており、バージョンが上がるたびに使いやすくなってきています。

そこで、**Eloquent** と CodeIgniter(4に限定します)の **Model/Entity** とを機能ごとに比較し、それぞれの使い勝手を見てみることにしました！
今はLaravel使ってるけど、機能多すぎだしCodeIgniterも少し気になるから使ってみたい！という方の参考になれば幸いです。

### モデルの作成

どちらもコマンドで作成可能です。
CodeIgniter4のほうがデフォルトで生成されるコード量は多いです。

#### Eloquent

空のモデルが作成されます。
ただしそのままだと`App`直下に作成されるため、名前空間を付けたい場合はコマンド引数に `Entities/User` のような形で指定する必要があります。

```console:生成コマンド
$ php artisan make:model User
```

```User.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    //
}
```

#### Model/Entity(CodeIgniter4)

各プロパティにデフォルト値がセットされたモデルが作成されます。
名前空間は `App\Models` となります。

```console:生成コマンド
$ php spark make:model UserModel
$ php spark make:entity User
```

```php:UserModel.php
<?php

namespace App\Models;

use CodeIgniter\Model;

class UserModel extends Model
{
    protected $DBGroup          = 'default';
    protected $table            = 'users';
    protected $primaryKey       = 'id';
    protected $useAutoIncrement = true;
    protected $insertID         = 0;
    protected $returnType       = 'array';
    protected $useSoftDeletes   = false;
    protected $protectFields    = true;
    protected $allowedFields    = [];

    // Dates
    protected $useTimestamps = false;
    protected $dateFormat    = 'datetime';
    protected $createdField  = 'created_at';
    protected $updatedField  = 'updated_at';
    protected $deletedField  = 'deleted_at';

    // Validation
    protected $validationRules      = [];
    protected $validationMessages   = [];
    protected $skipValidation       = false;
    protected $cleanValidationRules = true;

    // Callbacks
    protected $allowCallbacks = true;
    protected $beforeInsert   = [];
    protected $afterInsert    = [];
    protected $beforeUpdate   = [];
    protected $afterUpdate    = [];
    protected $beforeFind     = [];
    protected $afterFind      = [];
    protected $beforeDelete   = [];
    protected $afterDelete    = [];
}
```

```php:User.php
<?php

namespace App\Entities;

use CodeIgniter\Entity\Entity;

class User extends Entity
{
    protected $datamap = [];
    protected $dates   = ['created_at', 'updated_at', 'deleted_at'];
    protected $casts   = [];
}
```

### モデルにセットできる主なプロパティ

CodeIgniter4でプロパティにセットできるものが全てEloquentでもセットできるわけではなく、トレイトや定数、カスタム関数などでセットするものもあるようです。
イベントはEloquentのほうが豊富です。

||Eloquent|Model(CodeIgniter4)|
|:--|:--|:--|
|DB接続先|○($connection)|○($DBGroup)|
|テーブル名|○($table)<br>デフォルトはModel名スネークケースの複数形|○($table)<br>Eloquentと異なり必須|
|主キー|○($primaryKey)|○($primaryKey)<br>複合キーは指定できない|
|主キーのオートインクリメント|○($incrementing)|○($useAutoIncrement)|
|主キーのデータ型|○($keyType)|×(intのみ)|
|結果オブジェクトの型|×(Modelに固定)|○($returnType)<br>Entityを指定可|
|論理削除フラグ|△(SoftDeletesトレイトを使用)|○($useSoftDeletes)|
|更新許可フィールド|○($fillable)|○($allowedFields)|
|更新禁止フィールド|○($guarded)|×|
|表示フィールド|○($visible)|×|
|非表示フィールド|○($hidden)|×|
|現在日時の自動設定|○($timestamps)|○($useTimestamps)|
|DBに入れる日付値の型|○($dateFormat)|○($dateFormat)|
|作成日時フィールド|△(CREATED_AT定数)|○($createdField)|
|更新日時フィールド|△(UPDATED_AT定数)|○($updatedField)|
|削除日時フィールド|△(DELETED_AT定数)|○($deletedField)|
|属性値のキャスト|○($casts)|○(Entityの$casts)|
|デフォルト属性値|○($attributes)|×|
|バリデーション|×|○<br>`$validateRules` にルールをセットする <br>`$validationMessages` にエラーメッセージをセットする<br>`$skipValidation = true`でバリデーションをスキップ可|
|イベント|○($dispatchesEvents)<br>以下のイベントを連想配列にして登録可<br>retrieved<br>creating<br>created<br>updating<br>updated<br>saving<br>saved<br>deleting<br>deleted<br>trashed<br>forceDeleted<br>restoring<br>restored<br>replicating|○以下のイベントをプロパティ名にして登録可<br>beforeInsert<br>beforeInsert<br>beforeUpdate<br>afterUpdate<br>beforeFind<br>afterFind<br>beforeDelete<br>afterDelete|

### インスタンス生成

データを取得せず、空のエンティティを生成したい場合の書き方です。
Eloquentだとレコード登録も同時に行なえます。

#### Eloquent

`new` 演算子を使用します。

```php
$entity = new User();
```

インスタンス生成と同時にDB保存も行いたい場合は `create()` を使用します。

```php
$entity = User::create(['name' => 'セレス　太郎']);
```

#### Entity(CodeIgniter4)

`new` 演算子を使用します。[^2]

```php
$entity = new User();
```

Eloquentの`create()`にあたる機能はないようです。

### findによる取得

引数にIDを指定し、エンティティを1件返します。
Eloquentのほうがバリエーションが多いです。

#### Eloquent

引数にIDを指定し、該当レコードが見つかれば`Model`で返します。
配列でIDを複数指定すると`Collection`で返却されます（内部的には`findMany()`を呼び出している）。

```php
$entity = User::find($id);
$entities = User::find([$id1, $id2, $id3]);
```

`findOr〜`の形のメソッドもあり、`find()`に失敗した場合に所定の動作をします。

```php
// 失敗したら例外
$entity = User::findOrFail($id);

// 失敗したらインスタンスを生成
$entity = User::findOrNew($id);
```

#### Model/Entity(CodeIgniter4)

`Model`で`find()`します。IDの配列指定で複数レコード取得可能です（配列で返却されます）。
`$returnType`に`Entity`をセットした場合は`Entity`で受け取れます。

```php
$entity = model('App\Models\UserModel')->find($id);
$entities = model('App\Models\UserModel')->find([$id1, $id2, $id3]);
```

`findOr`系はないようです。

### firstによる取得

クエリ結果の最初の1レコード目を返します。
バリエーションとしてはほぼ`find()`と同様です。

#### Eloquent

`find()`と同じく`Model`で返します。

```php
$entity = User::where(['code' => '1'])->first();
// firstWhereだとさらに簡略化できる
$entity = User::firstWhere(['code' => '1'];
```

`firstOr`系のメソッドも定義されています。

```php
// 存在しなければ例外
$entity  = User::where(['code' => '1'])->firstOrFail();

// 存在しなければインスタンスを生成
$entity = User::firstOrNew(['code' => '1']);

// 存在しなければレコード作成
$entity = User::firstOrCreate(['id' => $id], ['code' => '1', 'name' => 'セレス 太郎']);
```

#### Model/Entity(CodeIgniter4)

`find()`と同様、`$returnType`に指定した型で返します。

```php
$entity = model('App\Models\UserModel')->where(['code' => '1'])->first();
```

`firstOr`系はないようです。

### 保存・変更検知

どちらにも保存機能はありますが、Eloquentのほうが変更検知機能は豊富です。

#### Eloquent

`save()`で保存します。
こちらも`saveOr`系のメソッドが存在します。

```php
// 保存
$entity->save();

// 保存に失敗したら例外
$entity->saveOrFail();
```

`save()`はエンティティの内容に変更がある場合のみDBにアクセスします。
変更があるかどうかは`isDirty()`を呼び出します（⇔`isClean()`）。

```php
// 変更前なのでfalse
var_dump($entity->isDirty());
// 値の変更
$entity->name = 'セレス 次郎';
// 変更したのでtrue
var_dump($entity->isDirty('name'));
```

`save()`実行で実際に保存されたかを確認するには、以下のようにします。

```php
// 新規作成
$entity1 = User::create(['name' => 'セレス 次郎'])
// trueを返す
var_dump($this->wasRecentlyCreated);

// 変更
$entity2 = User::find(1)->fill(['name' => 'セレス 三郎'])->save();
// trueを返す
var_dump($this->wasChanged('name'));
// 保存したのでdirtyではなくなっている(false)
var_dump($this->isDirty('name'));
```

#### Entity(CodeIgniter4)

Eloquentと同様、`save()`で保存します。
`saveOr`系のメソッドはないようです。

```php
$entity->save();
```

エンティティが変更されたかを確認するには`hasChanged()`を使います。

```php
$entity = new User();
// falseを返す
var_dump($entity->hasChanged('name'));

$entity->name = 'セレス 次郎';
// trueを返す
var_dump($entity->hasChanged('name'));
```

実際に変更が反映されたかどうかを確認するメソッドはないようです。

### 削除

Eloquent、Model(CodeIgniter4)、いずれもソフトデリート(`SoftDeletes`)に対応しています。

#### Eloquent

`Model`に対応するレコードを削除するには`delete()`、レコードを直接削除するには`destroy()`を使用します。

```php
// モデルから削除
$user = User::find(3);
$user->delete();

// ID指定で直接削除
User::destroy(3);
```

また、`SoftDeletes`トレイトを追加することでソフトデリート（論理削除）も扱えます。

```php
// ソフトデリート
$user->delete();

// 復元
$user->restore();

// 完全削除
$user->forceDelete();
```

また、ソフトデリートに関するクエリビルダのメソッドとして以下のようなものがあります。
* `withTrashed()` : ソフトデリートしたレコードも含む
* `onlyTrashed()` : ソフトデリートしたレコードのみ

#### Model(CodeIgniter4)

レコードを削除するには`Model`の`delete()`を使用します。
`Entity`側には`delete()`は存在しないようです。

```php
model('App\Models\UserModel')->delete(3);
```

また、`Model`の`$useSoftDeletes`プロパティに`true`をセットすることでソフトデリートも扱えます。

```php
// ソフトデリート
model('App\Models\UserModel')->where('id', 3)->delete();

// 完全削除
model('App\Models\UserModel')->where('id', 3)->purgeDeleted();
```

また、ソフトデリートに関するクエリビルダのメソッドとして以下のようなものがあります。
* `withDeleted()` : ソフトデリートしたレコードも含む
* `onlyDeleted()` : ソフトデリートしたレコードのみ

### リレーション

リレーション定義はEloquentのみ可能です。

#### Eloquent

リレーションを定義するための専用のメソッドがあります。
また、これらの仕組みを使って Eager Loading を実現できます。

##### 子テーブル(1対1)

`hasOne()`を使用します。

```php:User.php
public function address()
{
    return $this->hasOne('App\Models\Address');
}
```

##### 子テーブル(1対多)

`hasMany()`を使用します。

```php:User.php
public function articles()
{
    return $this->hasMany('App\Models\Article');
}
```

##### 親テーブル

```belongsTo()```を使用します。

```php:User.php
public function department()
{
    return $this->belongsTo('App\Models\Department');
}
```

#### Model/Entity(CodeIgniter4)

リレーションを定義する仕組みはありません。

### アクセサ(getter)、ミューテタ(setter)

アクセサ、ミューテタを使用することで、取得・保存時に追加処理を加えたり、データベースにないフィールド（カスタムフィールド）を定義することができます。
Eloquent、Entity(CodeIgniter4)どちらでも定義ができ、書き方もほとんど変わりません。

#### Eloquent

アクセサは `get` + フィールド(パスカルケース) + `Attribute` 、ミューテタは `set` + フィールド(パスカルケース) + `Attribute` というメソッド名で定義します。

```php:User.php
class User extends Model
{
    // 生年月日をもとに年齢を計算する
    public function getAgeAttribute()
    {
        if ($this->birthday === null) {
            return null;
        }
        $diff = date_diff(date_create($this->birthday), date_create());
        return $diff->y;
    }

    // パスワードを暗号化する
    public function setPasswordAttribute($plain)
    {
        $this->attributes['password'] = password_hash($plain, PASSWORD_BCRYPT);
    }
}
```

なお、日付項目については自動的に`Carbon`（Laravelにも標準搭載されている日時ライブラリ）インスタンスに変換されるアクセサやミューテタがデフォルトで使われているそうです。

#### Entity(CodeIgniter4)

アクセサは `get` + フィールド(パスカルケース)、ミューテタは `set` + フィールド(パスカルケース) というメソッド名で定義します。

```php:User.php
class User extends Entity
{
    // 生年月日をもとに年齢を計算する
    public function getAge()
    {
        if ($this->birthday === null) {
            return null;
        }
        $diff = date_diff(date_create($this->birthday), date_create());
        return $diff->y;
    }

    // パスワードを暗号化する
    public function setPassword($plain)
    {
        $this->attributes['password'] = password_hash($plain, PASSWORD_BCRYPT);
        return $this;
    }
}
```


なお、日付項目については`Entity`の`$date`プロパティに配列形式で対象フィールドをセットしておくと`Time`型に自動変換してくれるそうです。

### クエリビルダ

Eloquent、Model(CodeIgniter4)にはクエリビルダ機能も備わっています。
メソッドの数が多すぎるので、主に使われるメソッドに絞って簡単に比較してみます。

||Eloquent|Model(CodeIgniter4)|
|:--|:--|:--|
|全件取得|条件を指定せず`get()`|条件を指定せず`findAll()`|
|単一行取得|`first()`または`find()`|`first()`または`find()`|
|単一値取得|`value()`|なし|
|カラム値リスト取得|`pluck()`|`findColumn()`|
|集計|`count()`,`max()`,`min()`,`avg()`,`sum()`|`countAllResults()`:集計値を整数で返す<br>`selectCount()`,`selectMax()`,`selectMin()`,`selectAvg()`,`selectSum()`:SELECT項目の追加|
|レコード存在判定|`exists()`(否定形は`doesntExist()`)|なし|
|SELECT|`select()`,`selectRaw()`(素のSELECT)|`select()`（素のSELECTも可）|
|DISTINCT|`distinct()`|`distinct()`|
|WHERE|`where()`,`whereRaw()`(素のWHERE)など|`where()`など（素のWHEREも可）|
|HAVING|`having()`,`havingRaw()`(素のHAVING)など|`having()`など（素のHAVINGも可）|
|ORDER BY|`orderBy()`,`orderByRaw()`(素のORDER BY)など|`orderBy()`など（素のORDER BYも可）|
|GROUP BY|`groupBy()`,`groupByRaw()`(素のGROUP BY)など|`groupBy()`など（素のGROUP BYも可）|
|INNER JOIN|`join()`|`join()`の第三引数に`inner`を指定|
|LEFT JOIN|`leftJoin()`|`join()`の第三引数に`left`を指定|
|RIGHT JOIN|`rightJoin()`|`join()`の第三引数に`right`を指定|
|CROSS JOIN|`crossJoin()`|`join()`の第二引数以降指定なし|
|複雑なJOIN|クロージャーで設定可|全て`string`で書く必要あり|
|サブクエリJOIN|`joinSub()`,`leftJoinSub()`,`rightJoinSub()`|サブクエリを`string`化し、エスケープなしで`join()`の第一引数にセットする|
|UNION|`union()`,`unionAll()`|`union()`,`unionAll()`|
|サブクエリWHERE|`where()`や`whereExists()`などにクロージャーで指定|サブクエリを`string`化し、エスケープなしで`where()`の第一引数にセットする|
|INSERT|`insert()`,`insertIgnore()`(エラーの無視)|`insert()`|
|バルクインサート|`insert()`に二次元配列をセットする|`insertBatch()`|
|INSERT IGNORE|`insertOrIgnore()`|`ignore(true)->insert()`|
|INSERT SELECT|`insertUsing()`|なし|
|UPSERT|`upsert()`|なし|
|更新または挿入|`updateOrInsert()`|なし|
|DELETE|`delete()`|`delete()`|
|Raw|`DB::raw()`|`new RawSql()`(v4.2以降)|

## 総評

双方のORMの主な機能をだいたい比較してみました。

やはりEloquentのほうがCodeIgniterに比べて断然使える機能は多いです。
ですが、CodeIgniterをバージョン3から使っている筆者としては、だいぶ機能が増えたなぁという感想です。

コレクションと Eager Loading の仕組みが無いのが一番大きいですが、そこに目を瞑ればCodeIgniterでも小〜中規模くらいのサービスなら開発できると思います。

また実際に開発してみて気づきましたが、Laravelのコードは複雑怪奇で読みにくい一方で、CodeIgniterのコードはすごく読みやすいです。
未だにLaravelは人気No.1のフレームワークですが、CodeIgniterももう少し人気が出てもいい気はします。

## 参考

- [Laravel 9.x Eloquentの準備](https://readouble.com/laravel/9.x/ja/eloquent.html)
- [Laravel 9.x データベース：クエリビルダ](https://readouble.com/laravel/9.x/ja/queries.html)
- [Using CodeIgniter’s Model - CodeIgniter documentation](https://codeigniter4.github.io/userguide/models/model.html)
- [Using Entity Classes - CodeIgniter documentation](https://codeigniter4.github.io/userguide/models/entities.html)
- [Query Builder Class - CodeIgniter documentation](https://codeigniter4.github.io/userguide/database/query_builder.html)
- [CodeIgniterとLaravelをざっとベンチマークしてみる](https://zenn.dev/kenjis/articles/90b3eadd761c07)

[^1]: ModelがDAO寄り、EntityがRepository寄りのクラスとして定義されており、これまで通りModelのみでもアプリケーションを作ることはできます。
[^2]: Modelのほうはnew演算子でもインスタンス生成できますが、`model()`グローバル関数を使うことが推奨されています。`model()`を使うとModelオブジェクトが共有されるため、生成コストが抑えられます。