やること
深い階層構造を持つデータを簡単に扱えちゃう `Closure Table`についての簡単な紹介と実装。環境
- Laravel5.8 - Docker - MySQL扱うデータたち
豊作で有名な2011年のアニメを季節ごとに分類するとこんな感じ。そしてこれを1テーブルで管理してみる。 (筆者が好きだったアニメを1つの季節に3つ選びました〜〜)2011年アニメ
├ 冬
| ├ 魔法少女まどかマギカ
| ├ これはゾンビですか?
| └ GOSICK
├ 春
| ├ STEINS;GATE
| ├ あの日見た花の名前を僕達はまだ知らない
| └ 花咲くいろは
├ 夏
| ├ ゆるゆり
| ├ 神様のメモ帳
| └ ロウきゅーぶ!
└ 秋
├ Fate/Zero
├ 未来日記
└ 僕は友達が少ない
ClosureTableの作成
公式の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ファイルを見て見ます。
<?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
という2つのテーブルを作るファイルになっています。最初に1テーブルで管理と宣言したのですが実はこのanimation_closure
というテーブルも自動で作られますし、このテーブルによって子孫をメソッドで取得できる仕組みになっています。
カラムを追加しておきます。
<?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
テーブルにデータを入れる
シードしてもいいのですがせっかくなのでメソッド紹介も含めて全部手動で入れる事にしました! Repositoryパターン使います〜という事でRepositoryとControllerとview作成。 (Interfaceの記述は省略してます)<?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);
}
}
<?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();
}
}
<html>
<head>
<title>季節追加</title>
</head>
<body>
季節を追加
{{ Form::open() }}
{{ Form::input('text','season') }}
{{ Form::submit('追加') }}
{{ Form::close() }}
</body>
</html>
ルート定義して、view開いて、冬・春・夏・秋を入力して送信するとデータベースはこんな感じになってるはず。
それぞれに子要素を追加していく
季節を選択してアニメを追加するviewを作成
<html>
<head>
<title>アニメ追加</title>
</head>
<body>
{{ Form::open() }}
季節を選択
{{ Form::select('season',$seasons) }}
{{ Form::input('text','animation') }}
{{ Form::submit('追加') }}
{{ Form::close() }}
</body>
</html>
こんな画面ができるので、季節を選択してアニメを入力していきます!
ちなみにanimation_closure
テーブルには数字がたくさん入ってます。これは、それぞれの要素が自分の位置と親に対してどの位置にいるかを記述したものになっています。
例えば「魔法少女まどかマギカ」,idは5です。
これは冬アニメなので親のidは1になります。つまりancestor = 1, 'descendant = 5'最初に追加されているから'depth = 1
となります。さらに自分の位置としてancestor = 5, descendant = 5, depth = 0
も記述されています。確認して見てください!
アニメの時期を取得したり
またRepositoryとControllerに追記。
public function getAnimationSeason($animation)
{
$child = Animation::where('name', $animation)->get();
return $child[0]->getParent();
}
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を書いてうまくやるとこんな感じで季節取得できます