ClosureTableを使って〇〇年のアニメを季節ごとに1テーブルで管理する


<h1>やること</h1>
深い階層構造を持つデータを簡単に扱えちゃう `Closure Table`についての簡単な紹介と実装。

<h1>環境</h1>
- Laravel5.8
- Docker
- MySQL
<h1>扱うデータたち</h1>
豊作で有名な2011年のアニメを季節ごとに分類するとこんな感じ。そしてこれを1テーブルで管理してみる。
(筆者が好きだったアニメを１つの季節に３つ選びました〜〜)

```
2011年アニメ
├　冬
|   ├ 魔法少女まどかマギカ
|   ├ これはゾンビですか？
|   └ GOSICK   
├　春
|   ├ STEINS;GATE
|   ├ あの日見た花の名前を僕達はまだ知らない
|   └ 花咲くいろは
├　夏
|   ├ ゆるゆり
|   ├ 神様のメモ帳
|   └ ロウきゅーぶ!
└　秋
    ├ Fate/Zero
    ├ 未来日記
    └ 僕は友達が少ない
```

<h1>ClosureTableの作成</h1>
公式のGithubに色々載ってます。
https://github.com/franzose/ClosureTable

まず下記コマンドでインストール。

```
$ composer require franzose/closure-table
```
インストールできたらテーブルを作ります。

```
$ php artisan closuretable:make --entity=animation
```

うまくいくと、migrationファイルと、`Animation.php` `AnimationInterface.php` `AnimationClosure.php` `AnimationClosureInterface.php` というモデル&インターフェースたちができています。

migrationファイルを見て見ます。

```create_animation_table.php

<?php

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

class CreateAnimationsTableMigration extends Migration
{
    public function up()
    {
        Schema::create('animations', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('parent_id')->unsigned()->nullable();
            $table->integer('position', false, true);
            $table->integer('real_depth', false, true);
            $table->softDeletes();

            $table->foreign('parent_id')
                ->references('id')
                ->on('animations')
                ->onDelete('set null');
        });

        Schema::create('animation_closure', function (Blueprint $table) {
            $table->increments('closure_id');

            $table->integer('ancestor', false, true);
            $table->integer('descendant', false, true);
            $table->integer('depth', false, true);

            $table->foreign('ancestor')
                ->references('id')
                ->on('animations')
                ->onDelete('cascade');

            $table->foreign('descendant')
                ->references('id')
                ->on('animations')
                ->onDelete('cascade');
        });
    }

    public function down()
    {
        Schema::table('animation_closure', function (Blueprint $table) {
            Schema::dropIfExists('animation_closure');
        });

        Schema::table('animations', function (Blueprint $table) {
            Schema::dropIfExists('animations');
        });
    }
}
```

見ての通り、`animation`と`animation_closure`という２つのテーブルを作るファイルになっています。最初に１テーブルで管理と宣言したのですが実はこの`animation_closure`というテーブルも自動で作られますし、このテーブルによって子孫をメソッドで取得できる仕組みになっています。

カラムを追加しておきます。

```create_animation_table.php
<?php

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

class CreateAnimationsTableMigration extends Migration
{
    public function up()
    {
        Schema::create('animations', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name'); 　　　//追加
            $table->integer('parent_id')->unsigned()->nullable();
            $table->integer('position', false, true);
            $table->integer('real_depth', false, true);
            $table->softDeletes();

            $table->foreign('parent_id')
                ->references('id')
                ->on('animations')
                ->onDelete('set null');
        });

        Schema::create('animation_closure', function (Blueprint $table) {
            $table->increments('closure_id');

            $table->integer('ancestor', false, true);
            $table->integer('descendant', false, true);
            $table->integer('depth', false, true);

            $table->foreign('ancestor')
                ->references('id')
                ->on('animations')
                ->onDelete('cascade');

            $table->foreign('descendant')
                ->references('id')
                ->on('animations')
                ->onDelete('cascade');
        });
    }

    public function down()
    {
        Schema::table('animation_closure', function (Blueprint $table) {
            Schema::dropIfExists('animation_closure');
        });

        Schema::table('animations', function (Blueprint $table) {
            Schema::dropIfExists('animations');
        });
    }
}
```

そしてmigrationします。

```
$ php artisan migrate
```

<h1>テーブルにデータを入れる</h1>
シードしてもいいのですがせっかくなのでメソッド紹介も含めて全部手動で入れる事にしました！
Repositoryパターン使います〜という事でRepositoryとControllerとview作成。
(Interfaceの記述は省略してます)

```EloquentAnimationRepository.php
<?php
namespace App\Repositories\Repository;

use App\Animation;
use App\Repositories\Contract\AnimationContract;

class EloquentAnimationRepository implements AnimationContract
{
    public function addName($name)
    {
        $animation = new Animation;
        $animation->name = $name;
        return $animation;
    }

    public function getSeason()
    {
        return Animation::select('name')->where('parent_id', null)->get();
    }

    public function addAnimation($season, $animation)
    {
        $parent = Animation::where('name', $season)->get();
        $animation = $this->addName($animation);
        $parent[0]->addChild($animation);
    }

}

```



```AnimationController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Repositories\Contract\AnimationContract;
use App\Animation;

class AnimationController extends Controller
{
    /**
     * @var AnimationContract
     */
    private $animation;

    public function __construct(AnimationContract $animation)
    {
        $this->animation = $animation;
    }

    public function season()
    {
        return view('animation.season');

    }

    public function addSeason(Request $request)
    {
        $this->animation->addName($request->season)->save();

        return redirect()->back();
    }

    public function animation()
    {
        $seasons = [];
        foreach($this->animation->getSeason() as $season){
           
            $seasons[$season->name] = $season->name;
        }

        return view('animation.animation')->with('seasons', $seasons);
    }

//アニメ追加
    public function addAnimation(Request $request)
    {
        $this->animation->addAnimation($request->season, $request->animation);

        return redirect()->back();
    }
}

```

```add.blade.php
<html>
    <head>
        <title>季節追加</title>
    </head>
    <body>
        季節を追加
        {{ Form::open() }}
        {{ Form::input('text','season') }}
        {{ Form::submit('追加') }}
        {{ Form::close() }}
    </body>
</html>

```

ルート定義して、view開いて、冬・春・夏・秋を入力して送信するとデータベースはこんな感じになってるはず。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454650/18e1dc7d-fe56-6af6-acca-74ba364c05bf.png)


<h1>それぞれに子要素を追加していく</h1>

季節を選択してアニメを追加するviewを作成

```animation.blade.php
<html>
    <head>
        <title>アニメ追加</title>
    </head>
    <body>
        {{ Form::open() }}
        季節を選択
        {{ Form::select('season',$seasons) }}
        {{ Form::input('text','animation') }}
        {{ Form::submit('追加') }}
        {{ Form::close() }}
    </body>
</html>
```

こんな画面ができるので、季節を選択してアニメを入力していきます！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454650/90ab1695-2748-b47b-0133-840b74af6d74.png)

入力し終えるとテーブルはこんな感じに。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454650/8c277218-fd23-8ac0-1563-4ccfd3ba68a7.png)

ちなみに`animation_closure`テーブルには数字がたくさん入ってます。これは、それぞれの要素が**自分の位置**と**親に対してどの位置にいるか**を記述したものになっています。

例えば「魔法少女まどかマギカ」,idは5です。
これは冬アニメなので親のidは1になります。つまり`ancestor = 1, 'descendant = 5'最初に追加されているから'depth = 1`　となります。さらに自分の位置として`ancestor = 5, descendant = 5, depth = 0`も記述されています。確認して見てください！

<h1>アニメの時期を取得したり</h1>
またRepositoryとControllerに追記。

```EloquentAnimationRepository.php

public function getAnimationSeason($animation)
    {
        $child = Animation::where('name', $animation)->get();

        return $child[0]->getParent();
    }

```

```AnimationController.php

public function search()
    {
        return view('animation.search_season');
    }

    public function searchSeason(Request $request)
    {
        $season = $this->animation->getAnimationSeason($request->animation);

        return view('animation.search_season')->with('season', $season);
    }
```

viewを書いてうまくやるとこんな感じで季節取得できます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454650/de4cd250-62ad-3e1e-9357-be9186034053.png)

<h1>最後に</h1>
今回は親子関係しか見ませんでしたが、孫まで全て取ってきたりツリーそのまま持ってきたり、位置を変更したりもできます！！が、今回はもう体力の限界になったのでまた今度

<h1>参考ページ</h1>
https://www.wakuwakubank.com/posts/599-laravel-closure-table/