👨‍👩‍👦

【Laravel】1回のsubmitで親と子のデータを同時に送信、保存する方法

2021/08/08に公開

■はじめに

WEBエンジニア1年目のゆりです。
今回は1回のsubmitで親と子のデータを同時に送信、保存する方法をご紹介したいと思います。

↓下記でお悩みの方におすすめの記事

→全て1つのsubmitボタンで実装可能です!!!図で表すとこんな感じ。

親テーブルのcreateページに親テーブルのフォームと子テーブルのフォームを用意しました。

では、早速実装していきましょう。

完成したソースコードだけ見たい方は、
右の目録からて「完成したソースコード」をクリックして解説を飛ばしてください。

■開発環境

・MacOS Catalina
・PHP:7.3.11
・Laravel:8.49.2
・MySQL:8.0

■手順

①マイグレーションファイルの作成
②親と子のルートを記述
③それぞれモデルに親と子をリレーションを記述
④createページを作成
⑤親テーブル用のフォームを作成する
⑥取得したレコードを保存する(親)→コントローラ
⑦子テーブル用のフォームを作成する
⑧取得したレコードを保存する(子)
⑨詳細ページで取得したデータを表示する→コントローラ
⑩詳細ページで取得したデータを表示させる→ビュー

■実装&解説

①マイグレーションファイルの作成

userテーブルのマイグレーションは割愛させていただきます

①-1 Questionテーブル(親)のマイグレーションファイルを作成

ターミナル
$ php artisan migrate:make create_questions_table
2021_08_07_074458_create_questions_table.php
<?php

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

class CreateQuestionsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('questions', function (Blueprint $table) {
            $table->id();
            $table->integer('user_id')->comment('ユーザーID');
            $table->string('title', 50)->comment('題名');
            $table->string('contents', 2000)->nullable()->comment('内容');
            $table->timestamps();
        });
    }

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

①-2 voteテーブル(子)のマイグレーションファイルを作成

ターミナル
$ php artisan migrate:make create_votes_table
2021_08_07_074458_create_votes_table.php

<?php

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

class CreateVotesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('votes', function (Blueprint $table) {
            $table->id();
            $table->integer('question_id')->comment('質問ID');
            $table->string('vote')->comment('選択肢');
 
            $table->timestamps();
        });
    }

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

①-3 migrateを実行する

ターミナル
$ php artisan migrate

②親と子のルートを記述

※今回の実装とは関係のないルートは省略します

web.php
<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Auth::routes();

// Question(親のルート)

// 投稿ページを表示させるためのルート
Route::get('/questions/create', [App\Http\Controllers\QuestionController::class,  "create"])
->middleware('auth')->name("questions.create");
 
 // formから送信されたデータを保存するルート
Route::post('/questions/store', [App\Http\Controllers\QuestionController::class,  "store"])
->middleware('auth')->name("questions.store");


・middleware('auth')はログインしているユーザーのみ、アクセスや動作を許すための記述(ログインしていないユーザーは自動的にログイン画面に遷移します)
・name("questions.create")は名前付きルートを指定したいときに記述

「名前付きルート」につきましてはこちらをご確認ください。https://readouble.com/laravel/5.6/ja/routing.html

③それぞれモデルに親と子をリレーションを記述

③-1 親テーブルのモデルにhasManyで子テーブルとリレーション(1→多)

question.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\Vote; //ここを記述

class Question extends Model
{
    use HasFactory;

    protected $fillable = [
        'id',
        'title',
        'contents',
        'user_id',
    ];

// ここから下を記述
    public function vote()
    {
        return $this->hasMany(Vote::class);
    }
}

③-2 子テーブルのモデルにbelongToで親テーブルとリレーション (多→1)

vote.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\Question; // ここを記述

class Vote extends Model
{
    protected $fillable = [
        'vote',
        'question_id',
    ];

    // ここから下を記述
    public function question()
    {
        return $this->belongsTo(Question::class);
    }
}

これでquestionテーブルとvoteテーブルの親子関係が確立されました。

④createページを作成

questionテーブル(親)のコントローラでcreateアクションを作成します。
createアクションは質問ページを表示させたいだけなので、下記の1行でOKです。

QuestionController
public function create()
    {
        return view('questions.create');
    }

⑤親テーブル用のフォームを作成する

create.blade.php
<form method="POST" action="{{ route('questions.store') }}">
    @csrf
        <div class="form-group">
          <label for="title">
                タイトル
          </label>
          <input type="text" name="title" class="form-control">
        </div>
        <div class="form-group">
            <label for="subject">
                内容
            </label>
	<textarea type="text" name="contents" class="form-control"></textarea>
        </div>
</form>

name属性 は、要素に名前を付与します。
フォームから送られたデータをコントローラで受け取る際に使用するので重要です。
取得するカラム名で命名すると無難です。

type属性 は、フォームの種類を指定する際に使用します。
指定するtype属性により一行テキストボックス・チェックボックス・ラジオボタン・送信ボタン・リセットボタンなど種類を指定することが可能です。
どのような種類があるかは下記をご確認ください。
http://www.htmq.com/html5/input.shtml

⑥取得したレコードを保存する(親)

QuestionController
<?php

namespace App\Http\Controllers;
use App\Models\Question;                                 //ここを追加
use App\Models\Vote;                                         //ここを追加
use Illuminate\Support\Facades\DB;           //ここを追加
use Illuminate\Http\Request;                       
use App\Http\Requests\QuestionRequest;  //ここを追加

class QuestionController extends Controller
{
   public function store(QuestionRequest $request)
    {          
      DB::beginTransaction(); 
        try{
                $question = new Question($request->get('question',[
              'title' => $request->title,
              'contents' => $request->contents,
              'user_id' => auth()->user()->id,
                ]));
        
	        $question->save();
	
               }catch(Exception $e){
                 DB::rollback();
                return back()->withInput();
                }
		  
        DB::commit();
        return redirect(route('questions.index'));
            }
   }

⑥-1storeアクションを作成

QuestionController
public function store(QuestionRequest $request)
    {
    //
}

⑥-2 処理開始(トランザクション開始)

QuestionController
public function store(QuestionRequest $request)
    {
   DB::beginTransaction();
}

DB::beginTransaction()でDBへ保存する処理を開始する合図をします。

⑥-3 取得したリクエストデータを取り出し、データベースに保存

QuestionController
public function store(QuestionRequest $request)
    {
   DB::beginTransaction();
   try{
        $question = new Question($request->get('question',[
            'title' => $request->title,
            'contents' => $request->contents,
            'user_id' => auth()->user()->id,
        ]));
	
        $question->save();	
              }
   }

③リクエストから送られてきたデータをQuestionインスタンスに格納
'formのname属性'=> $request->格納したいカラム名, で記述する
⑤取得した親テーブルのレコードを保存する

⑥-4 処理が成功した場合(commit)、処理が失敗した場合(rollback)を記述

QuestionController
public function store(QuestionRequest $request)
    {
   DB::beginTransaction();
   try{
        $question = new Question($request->get('question',[
            'title' => $request->title,
            'contents' => $request->contents,
            'user_id' => auth()->user()->id,
        ]));
        $question->save();
	
    } catch(Exception $e){ // ⑥
        DB::rollback();
        return back()->withInput();
    }
    // ⑦
    DB::commit();
    return redirect(route('questions.index'));
}

⑥失敗した場合、処理を取り消し破棄(ロールバック)するように指定。
バリデーションがエラーで戻った時に値を保持しておきたい時は最後にwithInput() を加えておく
⑦処理が成功した場合、処理を確定する(コミット)入力をする。成功したら質問一覧にリダイレクトするように指定する

⑦子テーブル用のフォームを作成する

create.blade.php
<form method="POST" action="{{ route('questions.store') }}">
    @csrf
        <div class="form-group">
          <label for="title">
                タイトル
          </label>
          <input type="text" name="title" class="form-control">
        </div>
        <div class="form-group">
            <label for="subject">
                内容
            </label>
            <textarea id="name" type="textarea" name="contents" class="form-control"></textarea>
        </div>
	
    <!-- ここから子フォーム記述 -->
        <div class="form-group">
            <label for="subject">
                選択肢1
            </label>
            <input class="form-control" name="vote[0][vote]" type="text" id="vote" value="{{old('vote')}}">
        </div>
        <div class="form-group">
            <label for="subject">
                選択肢2
            </label>
	        <input class="form-control" name="vote[1][vote]" type="text" id="vote" value="{{old('vote')}}">
                </div>
    <!-- ここまで子フォーム記述 -->
		
        <a class="btn btn-secondary" href="{{ route('questions.index') }}">
            キャンセル
        </a>
        <button type="submit" class="btn btn-primary">
           投稿する
        </button>
</form> 
<input class="form-control" name="vote[0][vote]" type="text" id="vote" value="{{old('vote')}}">

同じカラムで複数のデータを送りたい場合は、
name属性に配列を記述すると同時にコントローラへデータを送信できる。

⑧取得したレコードを保存する(子)

QuestionController
public function store(QuestionRequest $request)
    {

        DB::beginTransaction();
        try{
        $question = new Question($request->get('question',[
            'title' => $request->title,
            'contents' => $request->contents,
            'user_id' => auth()->user()->id,
        ]));

        $question->save();

           // ここから下を記述
        $votes = $request->all(); // ①
	
	// ②
        foreach ($votes['vote'] as $vote) {
            foreach ($vote as $key => $value) {
                $data = [
                    'vote' => $value,
                    'question_id' => $question->id,
                ];
		
		// ③
                $vote = Vote::insert($data);
            }
       // ここまでを記述
        }

    }catch(Exception $e){
        DB::rollback();
        return back()->withInput();
    }
    DB::commit();
    return redirect(route('questions.index'));
}

①取得したリクエストデータを全て取得(この時点では配列が2つ取得されている)
②取得した配列をforeachで分解し、voteカラムと**$question->save();** でリレーションされている質問カラムのidをを取得する
③取得した複数のレコ-ドをinsert() でVoteテーブルのデータベースにまとめて保存する(bulk insert)

・createメソッド、saveメソッド、insertメソッドの違いはこちら
https://katsusand.dev/posts/laravel-save-data-db/

・bulk insertの説明
https://www.larajapan.com/2021/05/03/bulk-insertで大量のデータをdbに登録する/

⑨詳細ページで取得したデータを表示する(コントローラ)

QuestionController
    public function show($id)
    {
        $question = Question::find($id); //①
        $votes= $question->vote; // 親に紐づいたvoteのテーブルを取得

        return view('questions.show', compact('question', 'votes'));
    }

① 親テーブルのデータをidで取得
② ①に紐づいているvoteテーブルのデータを取得

⑩詳細ページで取得したデータを表示させる(ビュー)

レイアウトはまだ完成してないので殺風景です(笑)
投票機能を作成してる例なのでformが用意されてます。

show.blade.php

<!-- 親テーブルのデータを表示 -->
  {{ $question->id }}
  {{ $question->contents }}
  {{ $question->user->name }}
<!-- /親テーブルのデータを表示 -->
<form method="post" action="{{ route('votes.vote')}}">
   @csrf
   @method('put')
<!-- 子テーブルのデータを表示 -->
   @foreach ($votes as $vote)
     <div class="form-check">
        <input name="vote" value="{{ $vote->vote }}" type="radio">
        <label class="form-check-label" for="{{ $vote->vote }}">{{ $vote->vote }}</label>
     </div>
     @endforeach
<!-- 子テーブルのデータを表示 -->
         <button type=”submit” class="btn btn-danger btn-primary">投票する</button>
</form>

子テーブルのデータにつきましては同じカラムで複数の値を表示させたいので
value="{{ $vote->vote }}"にし、foreachで値を順番に取得させています。

実際にフォームを使用し、取得、保存、表示できるか確認

フォームに値を入力し、「投稿する」をクリック

データが無事表示されました!

■完成したソースコード

ルート

web.php
<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Auth::routes();

Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');

// Question(親のルート)

Route::get('/questions/create', [App\Http\Controllers\QuestionController::class,  "create"])
->middleware('auth')->name("questions.create");
 
Route::post('/questions/create', [App\Http\Controllers\QuestionController::class,  "store"])
->middleware('auth')->name("questions.store");

コントローラ

QuestionController
<?php

namespace App\Http\Controllers;
use App\Models\Question;
use App\Models\Vote;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;
use App\Http\Requests\QuestionRequest;

class QuestionController extends Controller
{
    public function create()
    {
        return view('questions.create');
    }

    public function store(QuestionRequest $request)
    {
       DB::beginTransaction();
       try{
	  $question = new Question($request->get('question',[
            'title' => $request->title,
            'contents' => $request->contents,
            'user_id' => auth()->user()->id,
         ]));

	  $question->save();

          $votes = $request->all();

          foreach ($votes['vote'] as $vote) {
              foreach ($vote as $key => $value) {
                $data = [
                    'vote' => $value,
                    'question_id' => $question->id,
                 ];

                $vote = Vote::insert($data);
            }
        }

      }catch(Exception $e){
        DB::rollback();
        return back()->withInput();
    }
      DB::commit();
      return redirect(route('questions.index'));
  }
    
    public function show($id)
    {
        $question = Question::find($id);
        $votes= $question->vote;

        return view('questions.show', compact('question', 'votes'));
    }
}

?>

ビュー(投稿ページ)

create.blade.php
<form method="POST" action="{{ route('questions.store') }}">
    @csrf
        <div class="form-group">
          <label for="title">
                タイトル
          </label>
          <input type="text" name="title" class="form-control">
        </div>
        <div class="form-group">
            <label for="subject">
                内容
            </label>
            <textarea id="name" type="textarea" name="contents" class="form-control"></textarea>
        </div>
	
    <!-- ここから子フォーム記述 -->
        <div class="form-group">
            <label for="subject">
                選択肢1
            </label>
            <input class="form-control" name="vote[0][vote]" type="text" id="vote" value="{{old('vote')}}">
        </div>
        <div class="form-group">
            <label for="subject">
                選択肢2
            </label>
	        <input class="form-control" name="vote[1][vote]" type="text" id="vote" value="{{old('vote')}}">
                </div>
    <!-- ここまで子フォーム記述 -->
		
        <a class="btn btn-secondary" href="{{ route('questions.index') }}">
            キャンセル
        </a>
        <button type="submit" class="btn btn-primary">
           投稿する
        </button>
</form> 

ビュー(詳細ページ)

show.blade.php

<!-- 親テーブルのデータを表示 -->
  {{ $question->id }}
  {{ $question->contents }}
  {{ $question->user->name }}
<!-- /親テーブルのデータを表示 -->
<form method="post" action="{{ route('votes.vote')}}">
   @csrf
   @method('put')
   @foreach ($votes as $vote)
<!-- 子テーブルのデータを表示 -->
     <div class="form-check">
        <input name="vote" value="{{ $vote->vote }}" type="radio">
        <label class="form-check-label" for="{{ $vote->vote }}">{{ $vote->vote }}</label>
     </div>
     @endforeach
<!-- 子テーブルのデータを表示 -->
         <button type=”submit” class="btn btn-danger btn-primary">投票する</button>
</form>

以上です。
最後までお読みいただきありがとうございました。

記事変更履歴

2021/08/11 記事の内容とは関係のないルートがあったため削除(子テーブルのルート)

参考サイト

https://qiita.com/zaburo/items/5c019d9062ddf1493d16
https://www.larajapan.com/2021/05/03/bulk-insertで大量のデータをdbに登録する/
https://medium-company.com/commit/
http://www.htmq.com/html5/input.shtml
https://reference.hyper-text.org/html5/attribute/name/
https://qiita.com/tetsu-upstr/items/2ea4ba8536669b7820c5

Discussion