【Laravel】外部キー制約とは?

対象 Laravelで外部キーを設定したい人

書いている人 プログラミング歴2年

前提  Laravelプロジェクトを作成できる

環境 M1 Laravel9

参考 https://readouble.com/laravel/9.x/ja/migrations.html

Laravelでmigration時に外部キーを設定することがあるが、そもそも外部キーやリレーションといった概念がよくわからずに使っていることが多いので頭の中を整理するためにアウトプットする。

目次

外部キーとは

外部キーとはforeign_key(フォーリンキー)とも言い、ざっくり言うと、テーブルとテーブルを関係づけて、矛盾が生じないようにすることのできるカラムのことだ。

そもそも、テーブルが2つあることが前提になる。

外部”と名がついているように、そのテーブルの外部キーには、「他のテーブルにあるカラムの値」が入る。

図で見た方が理解が早いと思うので、以下を見てほしい。

idname
1イチロー
2ジロー
usersテーブル
idtweetuser_id
1ラーメン食べたい。2
2腹一杯になった。2
tweetsテーブル

今、usersテーブルとtweetsテーブルの2つのテーブルがある。tweetsテーブルにあるuser_idというカラムがここでは外部キーになる。

ツイッターを思い浮かべながら、usersとtweetsのテーブルの関係を見ていこう。

usersテーブルには、各ユーザーのデータが入ってくる。tweetsテーブルには、つぶやいた内容が入ってくる。そしてもちろん、ユーザーがどんなツイートをしたのかがuser_idカラムからわかるようになっている。

もし、外部キー(ここではuser_id)がなければ、「ラーメン食べたい。」とつぶやいた投稿をした人が一体誰なのか分からないということだ。
ツイートがユーザーと紐づいていなければ、一体誰がどんな発言したかはわからなくなってしまう。(そういうSNSも面白そうだけど。)

ツイートは基本的にユーザーに紐づいているものだから、誰がそのツイートをしたのかを知るには外部キーを設定してあげる必要がある。つまり、tweetsテーブルにuser_idを用意してあげれば、誰がどのような呟きをしたのかが一目瞭然だ。

この場合、user_idはusersテーブルの2番、ジローさんに紐づいている。つまり、「ラーメン食べたい。」「腹一杯になった。」というつぶやきをした人はusersテーブルのid2のジローさん分かるわけだ。
このような感じで2つのテーブルを外部キーを利用することで関係づけることができる。

ここで1点考えてほしいことがある。
以下のテーブルのように、もし仮にtweetsテーブルのuser_idに"3"が入った場合どうなるだろうか。

usersテーブルにはid3の人物はいない。テーブルの設計上これはできないようにしたい。これが出来てしまうとテーブルの整合性がとれなくなってしまうからだ。要は「矛盾がないような設計にしましょう」ということだ。

idname
1イチロー
2ジロー
usersテーブル
idtweetuser_id
1今日の夜食は焼きそば!3
2やっぱり焼きうどんにした。3
tweetsテーブル

userテーブルにはid3の人物がいないのでこのテーブル関係には矛盾が生じてしまう。

なので、外部キーuser_idにはそもそもusersテーブルにあるidの数字しか入らないように設定されている。

次に、LaravelでMigrationファイルに外部キーを設定する方法を見ていく。

外部キーと外部キー制約の違いは?
「外部キー」と「外部キー制約」、この2つに厳密な違いはなくどちらも意味で捉えてよいと思う。「外部キー」と言った場合、その意味自身に"制約"も含まれるからだ。ただし、単純に上のテーブルでいうuser_idカラムを指し示す言葉として外部キーと言っている場合はよくあります。
もし、2つの意味に違いがあった場合はすいません。

参考https://wa3.i-3-i.info/word1992.html

LaravelのMigrationファイルに外部キーを設定する

次は、外部キーをLaravelで設定していこう。

新規にLaravelプロジェクトを立ち上げたら、Migrationファイルを作成する。上で説明したように、ここではTweetテーブルを作成していく。新規プロジェクトの立ち上げ方や、Migrationファイルがわからなければ以下の記事を参考にどうぞ。


usersテーブルは元から作成されているので、tweetsテーブルを作成しよう。

Tweetに関するModelとControllerとMigrationファイルも同時に作りたいので以下のコマンドで3つのファイルが作成できる。

php artisan make:model -cm Tweetコマンドを叩いて、TweetsのMigrationファイルと、TweetController、TweetModelの3つを作成しよう。

赤線の3つのファイルが作成された!
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TweetController extends Controller
{
    //
}
<?php

namespace App\Models;

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

class Tweet extends Model
{
    use HasFactory;
}
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tweets', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

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

3つのファイルが作成されたが、ここでコードの変更を加えるのはtweetsテーブルのMigrationファイルだ。

Migrationファイルに、外部キーを記述したファイルが以下になる。

public function up()
    {
        Schema::create('tweets', function (Blueprint $table) {
            $table->id();
            //外部キーの設定
            $table->foreignId('user_id')->constrained();
        });
    }

$table->foreignId('user_id')->constrained();の部分で外部キーを設定している。

$table->の矢印の先にはカラムを設定できる。
例えば、$table->string('name'); とすれば、「nameという名前」の文字列しか入らないカラムが作れる。

もちろん、使えるタイプはstringだけではない。使えるカラムタイプは以下の通りだ。

使えるカラムはたくさんある

上であげた使えるカラムタイプの中で、foreign_keyに関するものが3つある。

foreign_keyに関するものは上の3つ

$table->foreignId('user_id')とすれば、user_idという名前の、UNSIGNED BIGINTタイプのカラムが作られる。
なのでいちいちforeignIdと書かなくても、unsigned_bigintで同じタイプの型が作成されるのだが、こう書いておくことで下で説明するconstrainedメソッドが使えるので便利だ。

constrainedメソッドで、その中で紐付けたいテーブル名を設定すれば勝手に紐づいてくれるので以下の一行だけでOKだ。

この1行だけでuserテーブルのidと紐づけることができる。

userテーブルと明示的に指定していなくてもいいのかと疑問に思う人もいるかもしれないが、Laravelにはテーブル名の規則的なものがあって、user_idとつけた場合は勝手に、usersテーブルのidと紐づけてくれる仕組みがある。

実際にLaravelで試してみよう。

public function up()
    {
        Schema::create('tweets', function (Blueprint $table) {
            $table->id();
            //外部キーの設定
            $table->foreignId('user_id')->constrained();
        });
    }

上のようなmigrationファイルでphp artisan migrateコマンドを叩くと、tweetテーブルにuser_idというカラムが作成され、foreign_keyという項目を見ると、usersテーブルのidに紐づいていることがわかる。

tablePlusでDBを確認。お好きなGUI等で確認してみてください。

外部キーってどっちのテーブルに作成するんだっけ?問題
DBを触り始めたばかりの時、外部キーってどっちのテーブルに作ればいいのか分からなくなる時がよくあった。
テーブル同士には関係性があって、主従関係にある。上で言うと、usersテーブルとtweetsテーブルではusersテーブルが主でtweetsテーブルが従にあたる。基本的には、従の方に外部キーを用意するということを覚えておくといいかもしれない。どっちが主で従かわからんっていう人は、テーブルの図を書いてみたり、とにかく色々触って感覚で掴んでもらうのがよいと思う。

制約を超えて他のテーブルのidと紐づけたい。

もし、user_idと名付けたけれど、他のテーブルのidと紐づけたい場合は、constrainedメソッドにテーブル名を指定してあげれば、そのテーブルと紐づけることができる。

他のテーブルに紐づけたい場合は、constrainedの引数に違うテーブル名を入れてあげよう。

上を試すために、新しくexamplesテーブルを作成してみた。このexamplesテーブルのidにtweetsテーブルのuser_idを紐づけてみる。

新しくexamplesテーブルを作成
public function up()
    {
        Schema::create('tweets', function (Blueprint $table) {
            $table->id();
            //外部キーの設定 constrainedメソッドの引数にexamplesを指定
            $table->foreignId('user_id')->constrained('examples');
        });
    }

tweetsテーブルのconstrainedメソッドの中で作成したexampleテーブルを指定して、もう一度、migrateしてみると、
tweetsテーブルのuser_idがexamplesテーブルのidに紐づいたことがわかるはずだ。

examplesテーブルのidに紐づいた!

注意点として、migrationsファイルは上から順番(日付は早い順番)に作成されるので、参照されるテーブルを参照するテーブルの前に作成しておかなくてはならない。今回だったら、tweetテーブルが先に作成された場合、まだexamplesテーブルは作成されていないのにconstrainedメソッドで参照されてしまうからエラーになってしまう。その場合、先にexamplesテーブルが作成されるように日付を適当に変更して作成される順番を変更しよう。

tweetsテーブルより先にexamplesテーブルが作成されるように日付を変更。

php artisan migrate:fresh --seed コマンドで全てのデータベースを削除して、新たにシーディングしてくれるので覚えておくと便利だ。上の手順ではmigrationsファイルを作成してコードを書いてから、このコマンドを叩いてみよう。
本番環境では気をつけて使用してください。

idカラム以外の他のカラムと紐づけたい。

ここまで、外部キーを使って、片方のテーブルのidに他のカラムを紐づけてきたが、id以外のカラムと紐づけることはできないのだろうか?もちろんそれも設定できる。むしろ、id以外に紐づけたいことはよくあるはずだ。ここではそのやり方を紹介する。

例として、以下のようにuserテーブルとtweetテーブルのカラムを変更してみる。userテーブルには新しくuniform_numカラムを作成して、tweetテーブルにはuser_idからuniform_idへと名前を変更した。

idnameuniform_num
1イチロー51
2ジロー33
Userテーブル
idtweetuniform_id
1草野球楽しい51
2もう一杯いくか33
Tweetテーブル

ここでやりたいことは、tweetテーブルのuniform_idをuserテーブルのuniform_numと紐づけたいということだ。

まずはusrerテーブル、tweetテーブル共に、migrationファイルを以下のように書き換える

 public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->unsignedBigInteger('uniform_num')->unique();
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }
public function up()
    {
        Schema::create('tweets', function (Blueprint $table) {
            $table->id();
            $table->string('tweet');
            //外部キーの設定
            $table->foreignId('uniform_id')->constrained('users','uniform_num');
        });
    }

tweetsテーブルで、constraindeメソッドの第二引数にuniform_numと渡しているように、カラムの指定もできる。この場合、usersテーブルのuniform_numカラムと、uniform_idを紐づけてくださいと言った意味になる。

migrationファイルを変更したら、php artisan migrate:fresh --seed コマンドを打って、テーブルを確認してみよう。指定のカラムに紐づいたことが確認できるはずだ。

usersテーブルのuniform_numカラムに紐づいた。

ここでポイントが2つ。

1点目は、参照されるカラムにはunique制約をかけておくこと。uniform_idから参照される、usersテーブルのuniform_numには$table->unsignedBigInteger('uniform_num')->unique();とunique制約をつけている。uniqueとは同じ値が入らないような設定にするということだ。もし同じ値が入ってしまうと、整合性がとれなくなってしまうからだ。

2点目は参照する側のカラム」と「参照される側のカラム」の型を揃えること$table->unsignedBigInteger('uniform_num')->unique();とuniform_numカラムをunsignedBigInteger型に指定している。これは上述したように、$table->foreignId()unsignedBigInteger型を作成する為だ。

この2点をしっかり押さえておかないとエラーになるので、id以外のカラムに紐づけたい場合は気をつけよう。


constrainedメソッドの詳細を以下に貼っておくので気になる人は見てみてください

https://laravel.com/api/9.x/Illuminate/Database/Schema/ForeignIdColumnDefinition.html#method_constrained

バージョン7以前の外部キーの設定の仕方

Laravelの7以前のバージョンでは、以下のように一度カラムを作成してから、外部キーにそのカラムを指定してどのテーブルと紐付けるかを指定していた。つまり、外部キーを設定するのに2行かかっていた。

1行目でuser_idカラムをunsignedBigIntegerで定義した後に、2行目でuser_idカラムを外部キーにしている。

現在もこの方法は使えるので、外部キーをunsigedBigInteger以外の型で指定したい場合は使ってみよう。

データの削除と更新

外部キーが設定されているレコードは基本的に消すことができないし、外部キーの値も更新もすることができない。

試しにusersテーブルとtweetsテーブルにデータを入れてみて、usersテーブルのuser1を削除しようとする。

すると、以下のようなエラーが表示されて怒られる。(更新する時も同じエラー表示)

外部キー制約がかかっているからデータの削除も更新もダメ。

なぜなら、user1のデータを消してしまうと、外部キーに存在しないidが入っていることになり、矛盾が起きてしまうからだ。もちろん、これが正しいのだが、アプリを作成していると、データを削除したい時はもちろん生じる。また、データを更新しようとした時にも矛盾が起きるので上の表示が出てしまう。

こういう時に便利なのがonUpdateメソッドとonDeleteメソッドだ。onDeleteメソッドの引数にcascadeと文字列を渡してあげると、参照されるデータが削除された時に、外部キーを持つデータも一緒に削除してくれる。

cascadeは英語で"滝"の意味。連続して削除or更新されるといった意味で覚えている。
 public function up()
    {
        Schema::create('tweets', function (Blueprint $table) {
            $table->id();
            $table->string('tweet');
            //外部キーの設定
            $table->foreignId('uniform_id')
                  ->constrained('users','uniform_num')
                  ->onUpdate('cascade')
                  ->onDelete('cascade');
        });
    }

tweetsテーブルのmigrationファイルを上のように変更して、もう一度migrateをかける。使ってるDBのGUIによっては以下のように、Relationを確認できる。(ここではSequel Aceを使用している。)

Sequel Ace: https://apps.apple.com/jp/app/sequel-ace/id1518036000?mt=12

OnUpdateとOnDeleteがCASCADEになってる。

まずは、データの更新から。現在は以下のようなテーブル構成になっている。usersテーブルのuniform_numカラムを更新した時にTweetsテーブルのuniform_idカラムが変更されればオッケーだ。

idnameuniform_num
1イチロー51
usersテーブル
idtweetuniform_id
1草野球楽しい51
tweetsテーブル

UserController内で以下のメソッドをかける。(Controllerはなんでも、SQLだけでも、とにかく更新できればいいです。)

public function index   ()
    {
        $user = DB::table('users')->where('id',1)->update(['uniform_num' => 31]);
    }

更新をかけると、今度はエラーは表示されずに、外部キーの数値が変更された。

uniform_numが変更され
tweetsテーブルのuniform_idも同時に変更された。

次に、削除をかけてみる。(好きな方法で削除かけてください。)

public function index   ()
    {
        DB::table('users')->where('id',1)->delete();
    }

上のメソッドを飛ばすと、DBからデータが削除される。

usersテーブルは空
tweetsテーブルも空

基本的にcascadeはかけといた方がDBの整合性が取れるので、かけといた方がよいと思う。

onUpdateとonDelete以外にも構文があるみたいなので、そちらを使用してもよいかも。

nullOnDeleteとか場合によっては使うかもしれない。

まとめ

  • 外部キーはテーブル同士の関係性を作るカラムのこと。
  • 「参照したいテーブル名_id」という外部キーを用意することでLaravelが勝手に「参照したいテーブル名のidカラム」と紐づける。
    例.users_idとあるテーブルのカラムに用意すると、userテーブルのidカラムと勝手に紐づけられる。
  • 参照したいテーブル名を変更したい場合は、constrainedメソッドの中にテーブル名を入れる。
  • idカラム以外と紐づけたい場合も、constrainedメソッドの中にカラム名を入れる。
  • 「型を揃える」「migrationの順番を意識する」「id以外に紐づけたいならuniqueをつける」といったことに注意する
  • onUpdateメソッドとonDeleteメソッドの引数にcascadeを指定すると、関連したレコードも一緒に更新or削除される

外部キーを設定するには、
1.どっちのテーブルに外部キーを作るのか
2.constrainedメソッドの使い方
をしっかり整理しておこう。
それ以外にも、型を揃えたり、migrationの順番を意識したりと意外と注意事項が多い。
また、cascadeをつけとかないと、後々エラーになることも考えられる。(プロジェクトにもよると思いますが)

覚えるのは大変かもしれないが、DBを使っていたら外部キーを使う機会はたくさん出てくると思うので、何度も触って押さえておこう。

Eloquent:リレーションまで書こうと思ったけれど、長くなりすぎるので別記事にまとめます。
参考になれば幸いです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次