ツリー構造を持つモデルをLaravelで使ってみる

# はじめに
E-kanの前中です。
Laravelを業務で使うようになって2年以上経ちました。
最初は見よう見まねで取り組んできましたが、整理のために備忘録を兼ねて記事にしてみました。

# Eloquentでツリー構造に挑戦する
LaravelではEloquentモデルを使ってDBのテーブル同士のリレーションを定義して、関係のあるテーブルのデータをまとめて扱うことができます。
実務上では「1対多」のリレーションを使う機会が多いと思いますが、「1対多」のリレーションの応用として、ツリー構造を持つモデルを定義できないか、試してみました。
ツリー構造というのは、PCのディレクトリ構造が良く例として挙げられますが、他にも上司と部下の関係をあらわす場合などに使われます。
例えば
```text
社長
├部長1
│ ├課長A
│ ├課長B
│ └課長C
└部長2
  ├課長D
  └課長E
    ├係長a
    └係長b
```
のような構造です。
データベースで扱う場合は、以下のように上司をあらわすカラムを持つテーブルになります。
| ID | 上司 | 名前 | 役職 |
|:-:|:-:|:-:|:-:|
|   1|   Null|   斎藤|   社長|
|   2|   斎藤|   山田|   部長|
|   3|   斎藤|   木下|   部長|
|   4|   山田|   金山|   課長|
|   5|   山田|   後藤|   課長|
|   6|   木下|   佐々木|   課長|
|   7|   木下|   小野寺|   課長|
|   8|   後藤|   飯田|   係長|

※実際には上司のIDで参照するべきですが、見やすさ優先ということで。
※このあたりのテーブル設計については「入れ子集合モデル」などで調べるとよいです。

# ソースを書いてみる
要は、自分自身に対してリレーションを持つテーブルということになりますので、1対多のリレーションを設定するhasMany()メソッドを使って定義すれば良いわけです。
役職でも良いのですが、地域を扱うモデルとして作ってみます。

まずはマイグレーションファイルを作ります。
```php5:create_areas_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateAreasTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('areas', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('memo')->nullable(true);
            $table->integer('parent_area_id')->nullable(true);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('areas');
    }
}

```
id、name、memo、parent_area_idとcreated_atとupdated_atのカラムを持つだけの割とシンプルなテーブルです。
※ $table->timestamps(); でcreated_atとupdated_atのカラムを作成できます。
parent_area_id カラムはnullを許容するようにします。親Areaを持たないデータがあるからです。先の例でいうなら、社長の上司が居ないのにあたります。

続いてModelを作成します。この中でリレーションも定義します。
```php5:Area.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Area extends Model
{
    use HasFactory;

    protected $table = 'areas';

    function childAreas()
    {
        return $this->hasMany(self::class, 'parent_area_id', 'id');
    }
}
```
childAreas()関数の中で自分自身に対して1対多のリレーションを定義しておきます。

テストも作ってみます。
```php5:AreaTest.php
<?php

namespace Tests\Unit\Model;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\Area;

class AreaTest extends TestCase
{
    use RefreshDatabase;

    public function test_get_child_areas()
    {
        Area::factory()
            ->hasChildAreas(3)
            ->create();

        $area = Area::get();
        $this->assertCount(4, $area);
        $this->assertcount(3, $area->first()->childAreas);
    }
}

```
各関数の説明は省きますが、ここでは子Areaを3つ持つAreaを1つ生成させています。
つまり、Areaのレコードが4件作成され、親レコードの子レコードを参照すると3件のレコードが得られるかを確認しています。

# 注意点
結構簡単にModelを作成することができました。
あとは逆引きで親レコードを参照できるようにしたり、Rootレコード（parent_area_idがnullのレコード）を抽出する関数を定義したりすれば使い物になりそうです。
しかし、こういった構造を持つModelを扱う際には注意が必要です。

## 循環参照
一番の注意点は循環参照でしょう。
これは、階層構造を持つデータを作成したうえで、何らかの変更が必要になってRootレコードに親レコードを設定する時に起こりえます。
無関係のレコードを親に設定すれば問題は発生しませんが、既に自身の子レコードや孫レコードとして設定しているレコードを親にすればどうなるでしょうか？
永遠に参照が止まらなくなってしまいますね。処理がタイムアウトエラーになればまだマシな方で、タイムアウトしない設定にしていれば、永遠に処理中になってしまいます。
次回はこの循環参照の回避策について考えてみようと思います。
回避策の候補は2つありますので、大雑把に記載しておきます。
### 回避策1
データ編集時に循環参照のチェック処理を実装し、循環参照のデータ作成を阻止する。
実務的にもこの処理が最適でしょう。ただし、プログラム以外からデータ編集をされてしまうとどうしようもありません。
※ 「そんな奴おらへんやろ」と思った貴方は善良な人です。馬鹿げた想定が現実になるのが世の中ってものです。
### 回避策2
子レコードを参照する際に同一IDが2回目に現れた時点で参照を停止する。
意地でもエラーにさせないというなら、こうするしかありませんが、実務上はここまでやる必要はないでしょう。
現実的ではないと思いますが、ストレッチ課題として挑戦してみるのも良いかもしれません。

# さいごに
LaravelのEloquentモデルは非常に便利です。
便利なのですが、習得するためには結構な時間がかかります。
DBのリレーション構造を意識しなければいけませんし、何よりも適切に設計されたテーブルとの相性が良いので、そちらも学ばなければ使いこなすことは難しいでしょう。