複数駅と複数路線など、多対多のRestful CRUD API@laravel8
1 2 |
php artisan make:model Line --all php artisan make:model Station --all |
それぞれ路線名・駅名を追加
$table->string(‘name’);
多対多のリレーションは、中間テーブルを使う
1 |
php artisan make:migration create_line_station_table |
1 2 3 4 5 6 7 8 9 10 |
Schema::create('line_station', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('line_id'); $table->unsignedBigInteger('station_id'); $table->unique(['line_id', 'station_id']); // リレーション先の路線レコード・駅レコードが削除されたら、対応する中間テーブルも削除される $table->foreign('line_id')->references('id')->on('lines')->onDelete('cascade'); $table->foreign('station_id')->references('id')->on('stations')->onDelete('cascade'); $table->timestamps(); }); |
各モデルで、中間テーブルとひもづける
App\Models\Line
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Line extends Model { use HasFactory; // 不要なカラムは表示させない protected $hidden = [ 'created_at', 'updated_at', 'deleted_at', 'pivot' ]; public function stations() { return $this->belongsToMany('App\Models\Station')->withTimestamps(); } } |
App\Models\Station
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Station extends Model { use HasFactory; // 不要なカラムは表示させない protected $hidden = [ 'created_at', 'updated_at', 'deleted_at', 'pivot' ]; public function lines() { return $this->belongsToMany('App\Models\Line')->withTimestamps(); } } |
DatabaseSeeder.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public function run() { // \App\Models\User::factory(10)->create(); \DB::table('lines')->insert([ ['name' => '山手線'], ['name' => '中央線'], ['name' => '横須賀線'], ]); \DB::table('stations')->insert([ ['name' => '東京駅'], ['name' => '秋葉原駅'], ['name' => '神田'], ['name' => '御茶ノ水駅'], ['name' => '横須賀駅'], ]); \DB::table('line_station')->insert([ ['line_id' => 1,'station_id' => 1], ['line_id' => 1,'station_id' => 2], ['line_id' => 1,'station_id' => 3], ['line_id' => 2,'station_id' => 1], ['line_id' => 2,'station_id' => 4], ['line_id' => 3,'station_id' => 1], ['line_id' => 3,'station_id' => 5], ]); } |
routes/api.php
1 2 3 4 |
use App\Http\Controllers\LineController; use App\Http\Controllers\StationController; Route::resource('line', LineController::class); Route::resource('station', StationController::class); |
LineController.php(StationController.phpも同じ感じで)
1 2 3 4 5 |
public function index() { $lines = Line::with('stations')->get(); return $lines; } |
各路線の各駅が表示される
http://localhost/laravel8/public/api/line
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
[ { "id": 1, "name": "山手線", "stations": [ { "id": 1, "name": "東京駅" }, { "id": 2, "name": "秋葉原駅" }, { "id": 3, "name": "神田" } ] }, { "id": 2, "name": "中央線", "stations": [ { "id": 1, "name": "東京駅" }, { "id": 4, "name": "御茶ノ水駅" } ] }, { "id": 3, "name": "横須賀線", "stations": [ { "id": 1, "name": "東京駅" }, { "id": 5, "name": "横須賀駅" } ] } ] |
StationController.phpからのみ、駅の追加と、所属する路線の追加・削除・更新を行えるようにする(親に対しての中間テーブルのCRUDが行える)
LineController.phpからは出来ないようにする(親に対しての中間テーブルのCRUDは出来ないようにする)
山手線に上野駅を追加する(INSERT)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public function store(Request $request) { //バリデーションルールを設定 $validator = \Validator::make($request->all(), [ 'line_id' => 'required|max:255|exists:lines,id', 'name' => 'required|max:255', ]); //バリデーションルールにでエラーの場合 if ($validator->fails()) { return response()->json([ 'success' => false, 'summary' => 'Insert Failed', 'details' => $validator->errors() ]); } // 駅レコード作成と中間テーブルレコード生成の2つINSERTがあるのでトランザクション $station = \DB::transaction(function () use ($request) { // 路線モデル取得 $line = Line::find($request->line_id); // 駅レコード作成 $station = new Station; // $request->line_idがあってもエラーにならない! $station->fill($request->all())->save(); // 中間テーブルに追加 $line->stations()->attach($station); return $station; }); // jsonで結果を返す return response()->json([ 'success' => true, 'message' => 'Insert Success!', // 生成したレコードの値を返す。リレーション先も! 'details' => Station::with('lines')->where('id', $station->id)->get() ]); } |
既存の駅に対して路線を変更。
中央線に変更してたり(UPDATE)、除外したり(DELETE)、新しい路線を追加したり(INSERT)してみた。
多対多なので、親子関係にもできないので、意外とややこしかった・・・。
駅レコード本体の情報とは別に、リレーション先の路線レコードのCRUDも同時にするのでややこしい!
attach/detachするよりも、allRelatedIdsでリレーション先の全IDを取得して、そこに修正してsyncで上書きする方が楽だな。
ちゃんと差分を見て、既存レコードは上書きされない(created_at,updated_atがそのまま)ので、syncが一番だと思う。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
public function update(Request $request, Station $station) { //バリデーションルールを設定 // newだけなら新規追加。removeだけなら削除。両方あったら更新。 $validator = \Validator::make($request->all(), [ 'new_line_id' => 'max:255|exists:lines,id', 'remove_line_id' => 'max:255|exists:lines,id', ]); //バリデーションルールにでエラーの場合 if ($validator->fails() ) { return response()->json([ 'success' => false, 'message' => 'Update failed...', 'details' => $validator->errors() ]); } // 駅レコード更新と中間テーブルレコード更新の2つINSERTがあるのでトランザクション $station = \DB::transaction(function () use ($request, $station) { // 駅レコード本体の更新 $station->fill($request->all())->save(); // 既存の路線IDを取得 $ids = $station->lines()->allRelatedIds()->toArray(); $new_line_id = $request->new_line_id; // 追加の路線ID $remove_line_id = $request->remove_line_id; // 除外する路線ID // 両方とも空なら何もしない if(empty($new_line_id) && empty($remove_line_id)) { // 追加IDだけ }else if(!empty($new_line_id) && empty($remove_line_id)){ array_push($ids,$new_line_id); // 新しい路線IDを追加 // 削除IDだけ }else if(empty($new_line_id) && !empty($remove_line_id)){ unset($ids[array_search($remove_line_id, $ids)]); //路線IDを削除 // 両方入ってたら更新 }else if(!empty($new_line_id) && !empty($remove_line_id)){ unset($ids[array_search($remove_line_id, $ids)]); //路線IDを削除 array_push($ids,$new_line_id);// 新しい路線IDを追加(更新) } // 所属する路線IDを上書き $station->lines()->sync($ids); return $station; }); // 駅レコード本体の更新 $station->fill($request->all())->save(); // jsonで結果を返す return response()->json([ 'success' => true, 'message' => 'Update Success!', // 生成したレコードの値を返す。リレーション先も! 'details' => Station::with('lines')->where('id', $station->id)->get() ]); } |