Laravelでmigration時に外部キーを設定することがあるが、そもそも外部キーやリレーションといった概念がよくわからずに使っていることが多いので頭の中を整理するためにアウトプットする。
外部キーとは
外部キーとはforeign_key(フォーリンキー)とも言い、ざっくり言うと、テーブルとテーブルを関係づけて、矛盾が生じないようにすることのできるカラムのことだ。
そもそも、テーブルが2つあることが前提になる。
”外部”と名がついているように、そのテーブルの外部キーには、「他のテーブルにあるカラムの値」が入る。
図で見た方が理解が早いと思うので、以下を見てほしい。
id | name |
---|---|
1 | イチロー |
2 | ジロー |
id | tweet | user_id |
---|---|---|
1 | ラーメン食べたい。 | 2 |
2 | 腹一杯になった。 | 2 |
今、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の人物はいない。テーブルの設計上これはできないようにしたい。これが出来てしまうとテーブルの整合性がとれなくなってしまうからだ。要は「矛盾がないような設計にしましょう」ということだ。
id | name |
---|---|
1 | イチロー |
2 | ジロー |
id | tweet | user_id |
---|---|---|
1 | 今日の夜食は焼きそば! | 3 |
2 | やっぱり焼きうどんにした。 | 3 |
userテーブルにはid3の人物がいないので、このテーブル関係には矛盾が生じてしまう。
なので、外部キーuser_idにはそもそもusersテーブルにあるidの数字しか入らないように設定されている。
次に、LaravelでMigrationファイルに外部キーを設定する方法を見ていく。
参考: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つを作成しよう。
<?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つある。
$table->foreignId('user_id')とすれば、user_idという名前の、UNSIGNED BIGINTタイプのカラムが作られる。
なのでいちいちforeignIdと書かなくても、unsigned_bigintで同じタイプの型が作成されるのだが、こう書いておくことで下で説明するconstrainedメソッドが使えるので便利だ。
constrainedメソッドで、その中で紐付けたいテーブル名を設定すれば勝手に紐づいてくれるので以下の一行だけでOKだ。
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に紐づいていることがわかる。
制約を超えて他のテーブルのidと紐づけたい。
もし、user_idと名付けたけれど、他のテーブルのidと紐づけたい場合は、constrainedメソッドにテーブル名を指定してあげれば、そのテーブルと紐づけることができる。
上を試すために、新しくexamplesテーブルを作成してみた。このexamplesテーブルのidにtweetsテーブルのuser_idを紐づけてみる。
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に紐づいたことがわかるはずだ。
php artisan migrate:fresh --seed
コマンドで全てのデータベースを削除して、新たにシーディングしてくれるので覚えておくと便利だ。上の手順ではmigrationsファイルを作成してコードを書いてから、このコマンドを叩いてみよう。
※本番環境では気をつけて使用してください。
idカラム以外の他のカラムと紐づけたい。
ここまで、外部キーを使って、片方のテーブルのidに他のカラムを紐づけてきたが、id以外のカラムと紐づけることはできないのだろうか?もちろんそれも設定できる。むしろ、id以外に紐づけたいことはよくあるはずだ。ここではそのやり方を紹介する。
例として、以下のようにuserテーブルとtweetテーブルのカラムを変更してみる。userテーブルには新しくuniform_numカラムを作成して、tweetテーブルにはuser_idからuniform_idへと名前を変更した。
id | name | uniform_num |
---|---|---|
1 | イチロー | 51 |
2 | ジロー | 33 |
id | tweet | uniform_id |
---|---|---|
1 | 草野球楽しい | 51 |
2 | もう一杯いくか | 33 |
ここでやりたいことは、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
コマンドを打って、テーブルを確認してみよう。指定のカラムに紐づいたことが確認できるはずだ。
ここでポイントが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メソッドの詳細を以下に貼っておくので気になる人は見てみてください
バージョン7以前の外部キーの設定の仕方
Laravelの7以前のバージョンでは、以下のように一度カラムを作成してから、外部キーにそのカラムを指定してどのテーブルと紐付けるかを指定していた。つまり、外部キーを設定するのに2行かかっていた。
現在もこの方法は使えるので、外部キーをunsigedBigInteger以外の型で指定したい場合は使ってみよう。
データの削除と更新
外部キーが設定されているレコードは基本的に消すことができないし、外部キーの値も更新もすることができない。
試しにusersテーブルとtweetsテーブルにデータを入れてみて、usersテーブルのuser1を削除しようとする。
すると、以下のようなエラーが表示されて怒られる。(更新する時も同じエラー表示)
なぜなら、user1のデータを消してしまうと、外部キーに存在しないidが入っていることになり、矛盾が起きてしまうからだ。もちろん、これが正しいのだが、アプリを作成していると、データを削除したい時はもちろん生じる。また、データを更新しようとした時にも矛盾が起きるので上の表示が出てしまう。
こういう時に便利なのがonUpdate
メソッドとonDelete
メソッドだ。onDelete
メソッドの引数にcascade
と文字列を渡してあげると、参照されるデータが削除された時に、外部キーを持つデータも一緒に削除してくれる。
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
まずは、データの更新から。現在は以下のようなテーブル構成になっている。usersテーブルのuniform_num
カラムを更新した時にTweetsテーブルのuniform_id
カラムが変更されればオッケーだ。
id | name | uniform_num |
---|---|---|
1 | イチロー | 51 |
id | tweet | uniform_id |
---|---|---|
1 | 草野球楽しい | 51 |
UserController内で以下のメソッドをかける。(Controllerはなんでも、SQLだけでも、とにかく更新できればいいです。)
public function index ()
{
$user = DB::table('users')->where('id',1)->update(['uniform_num' => 31]);
}
更新をかけると、今度はエラーは表示されずに、外部キーの数値が変更された。
次に、削除をかけてみる。(好きな方法で削除かけてください。)
public function index ()
{
DB::table('users')->where('id',1)->delete();
}
上のメソッドを飛ばすと、DBからデータが削除される。
基本的にcascadeはかけといた方がDBの整合性が取れるので、かけといた方がよいと思う。
onUpdateとonDelete以外にも構文があるみたいなので、そちらを使用してもよいかも。
まとめ
- 外部キーはテーブル同士の関係性を作るカラムのこと。
- 「参照したいテーブル名_id」という外部キーを用意することでLaravelが勝手に「参照したいテーブル名のidカラム」と紐づける。
例.users_idとあるテーブルのカラムに用意すると、userテーブルのidカラムと勝手に紐づけられる。 - 参照したいテーブル名を変更したい場合は、constrainedメソッドの中にテーブル名を入れる。
- idカラム以外と紐づけたい場合も、constrainedメソッドの中にカラム名を入れる。
- 「型を揃える」「migrationの順番を意識する」「id以外に紐づけたいならuniqueをつける」といったことに注意する
- onUpdateメソッドとonDeleteメソッドの引数にcascadeを指定すると、関連したレコードも一緒に更新or削除される
外部キーを設定するには、
1.どっちのテーブルに外部キーを作るのか
2.constrainedメソッドの使い方
をしっかり整理しておこう。
それ以外にも、型を揃えたり、migrationの順番を意識したりと意外と注意事項が多い。
また、cascadeをつけとかないと、後々エラーになることも考えられる。(プロジェクトにもよると思いますが)
覚えるのは大変かもしれないが、DBを使っていたら外部キーを使う機会はたくさん出てくると思うので、何度も触って押さえておこう。
Eloquent:リレーションまで書こうと思ったけれど、長くなりすぎるので別記事にまとめます。
参考になれば幸いです。