Laravelですべての主キーを (table_name)_id にする


なにがしたい?

Laravel の Eloquent(Model) で主キー(プライマリキー)を変更する方法はご存知ですか?

App\Admin.php
class Admin extends Model
{
    protected $primaryKey = 'admin_id';

これですよね!

そうそう、そうなんですが、それ、すべての Eloquent に書きますか?

App\Post.php
class Post extends Model
{
    protected $primaryKey = 'post_id';
App\DailyTotal.php
class DailyTotal extends Model
{
    protected $primaryKey = 'daily_total_id';

これがめんどくさいのでなんとかして!という案件です。

結論

ベースクラスを書きます。

app/Eloquents/Model.php
namespace App\Eloquents;

use Illuminate\Database\Eloquent\Model as BaseModel;
use Illuminate\Support\Str;

abstract class Model extends BaseModel
{
    public function getKeyName()
    {
        // 主キーを「クラス名のスネーク_ID」にする
        return $this->primaryKey ?? Str::snake(class_basename($this)) . '_id';
    }
}

他のモデルはこれを継承するようにします。

app/Eloquents/Admin.php
use App\Eloquents\Model;
class Admin extends Model
{
}
app/Eloquents/Post.php
use App\Eloquents\Model;
class Post extends Model
{
}
app/Eloquents/DailyTotal.php
use App\Eloquents\Model;
class DailyTotal extends Model
{
}

以上

マイグレーション

もちろんマイグレーションも変更する必要があります。
こうするとカンタンです。

Schema::create('daily_totals', function (Blueprint $table) {
    $table->id('daily_total_id');

Strの解説

echo Illuminate\Support\Str::snake('CamelCase');
// camel_case

Laravel には「スネークケースとキャメルケースを変換」という機能が用意されていて、それがみんな恩恵を受けている「クラス名からテーブル名を得る(Postというモデルはpostsテーブル)」というところに使われています。ので、そこをコピペしていじらせていただいたものです。

echo Illuminate\Support\Str::plural('singular_name');
// singular_names
echo Illuminate\Support\Str::plural('genus');
// genera
echo Illuminate\Support\Str::singular('genera');
// genus
echo Illuminate\Support\Str::plural('sheep');
// sheep

ちなみに「単数形を複数形にする」やその逆といった機能もあったりして面白いです。
不規則な名詞はライブラリを持っているようですがどこまで対応しているのか……。
# 単複同形(笑)

注意点

複合主キーがうまく動きません。個人的には必要性が弱く利用を見送ったので、それ以上追求していません……。

なぜ主キーが「id」じゃないのか?

個人的にいろんな会社のいろんなプロジェクトを渡り歩いてきましたが、Laravelを使っているプロジェクトもそうでないプロジェクトも、例外なく主キーは「テーブル名+ID」で、単に「id」としているところはありませんでした。

これだけ多いと流石に、これは「日本の古い伝統だ! Laravelさんのようなモダンなフレームワークは id なんだから id が最新だ」という「ダサい」「かっこいい」という感覚で切って捨てられません……。

なぜ主キーが「id」ではダメなのか? それは実際に使ってみるとわかります。

$posts = Post::join(Tag::class, 'posts.id', '=', 'tags.post_id')->get(); // ※1
var_dump($posts->toArray());
array(
   '0' =>
  array (
    'id' => 42, // ※2
    ...

※1 同じID値なのに名前が異なる
※2 posts.id と tags.id が同じ名前なのに内容が異なる上、同じ名前なので post.id しか出てこない

$post = Post::first();
$id   = $post->id; // ※3
...
if ($id == ... ) 

※3 そのまま id という名前が使用されて、この $id の中身がわからなくなる

もちろんこうしたことは「些細な問題」でサラッと解決する手段はいくつもありますが、こうした「些細な問題」は他のところでも頻出するので、根本から断ち切っておいたほうが良さそうです。

そして根本にある原因は
同じ内容なのに違う名前(posts の id と、post_id)」と
異なる内容なのに同じ名前(posts の id と tags の id)」
であること。
プログラミングの基礎に「同じ内容は同じ変数名」「違う内容は違う変数名」にするというのがありますが、その逆を行っているワケですね。

こうした理由でほとんどのプロジェクトでは「命名規則」として「プライマリキーはテーブル名+IDとする」と決められています。なので、それを実装するLaravel側でも、ベースモデルにその規則を書いて、すべてのEloquentモデルに守ってもらいましょう、というのが本稿の趣旨です。
(仕様書や規則に倣ってコードを書く前に、仕様や規則をそのままコードにできないか?と考えるべきと思っていつもそういうアプローチを考えています)

ちなみに Laravel はなんで id なのか、というと、たぶん、わかりやすいから、でしょうか。
この初学者でも入りやすくて「とりあえず動かすことが超カンタン」というコンセプトはとても好きです。

他にも設計上の凡ミスを招きやすいという観点から「SQLアンチパターン」としても触れられています。
(正確には id の命名規則ではなく「id を使う」ことの弊害です)
こちらが勉強になりました。

余談

同じ理由で他の変数、たとえば users.name と campanies.name もそれぞれ、users.user_name と campanies.campany_name と違う名前にすべきですが、これは徹底できているところとそうでないところとまちまちでした。設計思想が末端まで行き届いていなかったのか、あるいは厳しすぎるルールも息が詰まるとゆるくしたのか。

こんな記事も書いています

Laravelのちょっとマニアックな視点から、誰も書かない記事を書いています(笑
合わせてご覧いただけると幸いです(^^)