|||||||||||||||||||||

なんぶ電子

- 更新: 

Laravelのモデルとクエリビルダ

以前にLaravelでデータベースの設定をしました。

Laravelにおいてデータベース処理の要となっているのがモデルやクエリビルダです。覚えるべき項目が多くありますが、その分使いこなせるようになった時の効果も大きいです。

Laravelのモデル

MVCにおける「モデル」は「アプリケーションが扱う領域のデータと手続きを表現するもの」と定義されていますが、Laravelで「php artisan make:model ...」として作成される「モデル」は「Eloquent」と呼ばれる機能を持ちます。

「Eloquent」はオブジェクトリレーショナルマッパー(ORM)で、「データベースの行とPHPのオブジェクト(連想配列)を相互変換するもの」です。

つまり、Laravelにおける「モデル」は本来の意味からは外れることはありませんが、それより具体的な機能を持ったものとなります。対応するデータベースのテーブルをもち、その選択や挿入や変更、削除等のUIをなど備えています。このLaravelのモデルを本来の「モデル」と区別して「Eloquentモデル」と呼んでいるようです。

モデル

モデルは、データベース処理の中心的な位置にいます。そのためデータベースのテーブルの他、マイグレーションや、ファクトリー、コントローラーなどとも連携されます。

その為、モデル生成時にはそのようなLaravelのファイルと一緒に作成できるようになっています。

例えば、ファクトリーはf、コントローラーc、マイグレーションはm、オプションを加えることで同時生成されます。

PS> php artisan make:model Terminal -fcm

作成時渡した名前をxとすると、モデルは「X.php」、コントローラーは「XController.php」、ファクトリーは「XFactory.php」という名前になります。これらの名前はキャメルケースで生成されます。

マイグレーションでは「作成時刻_create_Xs_table.php」という名前でスネークケースのファイル名になります。マイグレーションはその実行順序を保持するために先頭に作成時刻が入ります。そしてcreateとtableの間に、モデルとして設定した名前が入りますが、 名前は複数形に変換されます。

名前が英単語になっている場合は英語における複数形のルールが適用されます。countryはcountriesとなりますし、sheepやdataといった単数形と複数形が同じ単語の場合はそのままになります。

またマイグレーションによって生成されるデータベースのテーブルも複数形でスネークケースの名前となります。

この命名ルールに従う事えば、それぞれのファイルに関係性の記述するのを省略できますが、必須ではありません。

モデルクラスのプロパティとメソッド

「php artisan make:model」を実行するとEloquentのModelを継承したクラスが生成されます。継承したクラスのプロパティやメソッドは何も記述されていません。また、先のような命名規則に従った関連などにより、モデル生成とデータベーステーブルの関連性がデフォルト値として省略されている部分がありますので、それらを把握していきます。

  • テーブル名

    Modelの前に、データーベーステーブルとの関連性ですが先にも紹介したように、テーブル名はモデル名を複数形にしてキャメルケースをスネークケースに変換したものになります。たとえば「CamelCase.php」というモデルがあったらテーブル名は「camel_cases」となります。

    このデフォルト値とは違った任意のテーブルとモデルを結び付けたい場合は、$tableプロパティにテーブル名を設定します。

    CamelCase.php

    class CamelCase extends Model{
    
    protected $table='original_table_name';
    ...
  • 主キー

    デフォルトでは主キーはidというカラム名の整数値で、インクリメントが設定されているものになっています。

    また、Eloquentモデルでは複合主キーは利用できません。、そのようなテーブルを持ちたい場合は、別途主キーとなるIDを設けるか、Eloquentモデルの機能をあきらめてDBファサードで実装するかのどちらかになります。

    この主キーを変更するには$primaryKeyプロパティにカラム名を設定されます。

    CamelCase.php

    class CamelCase extends Model{
    
    protected $primaryKey='original_primary_key';
    ...

    主キーが整数値でない場合は$keyTypeにその種類を設定します。

    ここでは文字列「string」を設定しますが、文字列なら必然的にインクリメントしませんのでその設定も合わせて$incrementingにfalseを設定することが必要です。

    モデルで主キーやインクリメントの有無を変えても、マイグレーションには反映しないので注意してください。

    マイグレーションでidメソッドを使うと、データベースにはUnsinedBigInt、AUTO_INCREMENT、PRIMARY KEYとなるidカラムができてしまいます。なので、$table->char('id_char')->primary();など修正します。

    CamelCase.php

    class CamelCase extends Model{
    
    public $incrementing = false;
    protected $keyType='string';
    
    ...

    $incrementingだけ、publicになっているのに注意してください。

  • タイムスタンプ

    モデルのデフォルトでは、テーブルにcreated_atとupdated_atという登録日と更新日を保持するタイムスタンプカラムを要求し、モデルは更新時には暗黙のうちにそれを更新します。

    それらの挙動をとめるには$timestampsパブリックプロパティにfalseを設定します。

    CamelCase.php

    class CamelCase extends Model{
    
    public $timestamps = false;
    
    ...

    タイムスタンプ用のカラムの名前を変えたい場合は次の定数(const)を設定します。

    CamelCase.php

    class CamelCase extends Model{
      const CREATED_AT = 'cdate';
      const UPDATED_AT = 'udate';
    ...
  • データベース接続

    接続するデータベース変更も$connectionプロパティの変更によって可能です。

    これを設定する際は、config¥database.phpの修正も必要になります。connections配列に存在するプロパティに指定した名前のエントリーを修正するか、新たにエントリーを作成するかします。

    CamelCase.php

    class CamelCase extends Model{
      protected $connection = 'sqlite';
    ...
  • $hidden

    モデルをキーとバリューの連想配列にする際にはモデルからtoArray()メソッドを使うことで可能です。同様にtoJson()を使うとJSON文字列に変換されます。この時、$hiddenプロパティ配列に設定されているカラム名の列は出力されません。

    パスワード等レスポンスとしてユーザーには返さないカラム名を配列で指定します。

  • $fillable

    モデル名::create([key1=>value1,key2=>value2])(一括割り当てと呼びます)とすることで、新しい行を作成することができますがこの時に、指定可能なキーを制限するプロパティです。

    これはセキュリティ設定の一部でたとえば、(ユーザー名,メールアドレス,権限レベル)の3列で構成されるモデル内の処理で、リクエストの中にあるキーとバリューをそのままこのcreateに渡す構造になっているとします。

    実際にフォームからPOSTするのはユーザー名とメールアドレスで権限レベルはDBテーブルの初期値である0が入ることを望んでいるのですが、悪意を持ったユーザーが、POSTを改変しに権限レベルというキーと共に管理者権限である値をセットした場合に攻撃が成立してしまいます。

    これを未然に防ぐためあらかじめ変更できるカラム名を配列で指定しておくのがこの機能です。

  • $guarded

    $fillabelの逆で、一括割り当てを禁止するプロパティです。すべてのカラムで一括割り当てを許可したい場合はこの値を空の配列とします。

  • 定期削除

    登録後一定期間過ぎたら自動で削除したい場合にはprunableメソッドが利用できます。これはEloquent¥Prunableトレイトを利用します。

    たとえば作成後3カ月を超えたデータを削除するには次のようにします。subMonthは過去の日付を取得するCarbonのメソッドです。

    CamelCase.php

    use Illuminate¥Database¥Eloquent¥Prunable;
    
    class CamelCase extends Model{
      use Prunable;
      public function prunable(){
        return static::where('created_at', '<=', now()->subMonth(3));
      }
    ...
  • ソフト削除

    削除フラグを設けて、削除したように見せかけて内部的にはデータを保持しておく手法はよくあると思います。Eloquentモデルではこの機能も備えています。

    これを利用するにはテーブルにdeleted_atというカラムを設けてSoftDeletesトレイトをモデルで定義するだけです。

    すると「Model::where('id', 0)->delete();」として削除した場合に、deleted_atに値が入りクエリの結果から除去されるようになります。

    CamelCase.php

    use Illuminate¥Database¥Eloquent¥SoftDeletes;
    
    class CamelCase extends Model{
      use SoftDeletes;
    ...

    また、マイグレーションではSchemaファサードを使って、deleted_atカラムの追加や除去ができます。

    「alter_..._table」といった形で、マイグレーションを追加して次のように書けば既存のテーブルに追加できると思います。

    alter_camal_cases_table.php

    ...
      public function up(){
        Schema::table('camel_cases',function (Blueprint $table) {
          $table->softDeletes(); //設定
        });     
      }
    
      public function down(){
        Schema::table('camel_cases',function (Blueprint $table) {
          $table->dropSoftDeletes(); //削除
        });    
      }
    ...

    ソフト削除したものを含めてクエリを実行したい場合はModel::withTrashed()->where('id',1)とします。そうして取得した任意のモデルからrestore()メソッドを呼べばソフト削除したものを復元できます。

    ソフト削除を設定したモデルで、従来の削除をしたい場合はforceDelete()メソッドを呼びます。

モデルからクエリを実行

モデルのファサードからallを呼び出すだけで、テーブルにある全データを取得できます。これはクエリビルダDB::table('table-name')->get();として全件取得した際とデータの中身は同じですが、モデルから呼び出した場合はEloquentのCollectionが返り、DBファサードの戻り値とは少し違います。

モデルのファサードはクエリビルダとしても機能します。モデルファサードでは、DBファサードからテーブルを指定した状態DB::table('tablename')と同じようにクエリビルダを利用できます。この時の戻り値もEloquentのCollectionとなります。

EloquentのCollectionから呼び出せるメソッドに次のようなものがあります。

  • fresh,refresh

    一度処理したクエリを再度実行するメソッドです。

    freshは既存のインスタンスには影響しないで、あらたなインスタンスを受け取ります。refreshは戻り値の他、既存のインスタンスも実行されたクエリで更新します。

  • reject

    引数に関数をを受け取り、そこでtrueを返した行を削除して返します。

    $countries = $countries->reject(function($country){
      return $country->puplulation <= 0 ? true : false;
    });
    
  • contains

    プライマリーキーの値か、モデルを指定してその行が存在するかのチェックをboolean値で返します。

  • find

    プライマリーキーを指定して、一致したモデルを返します。キーを配列で渡すことも可能です。

クエリビルダ

主なクエリビルダの使い方を紹介しておきます。

  • select

    何も指定しない場合は全カラムを対象としますが、取得するカラムを指定したい場合はselectメソッドにカラム名を渡して使います。複数ある場合はメソッドの引数へ追加していきます。この時通常のSQL同様にasを使って別名を設定できます。

    ...
    Model->select('id','email as mail')...
    

    一度作成したクエリビルダインスタンスにselectの項目を追加したい場合はaddSelectメソッドで可能です。

    また、distinctを指定したい場合はselectの前にメソッドを呼びます。「distinct()->select(...」

  • where

    まず基本的な条件式であるwhereです。where('カラム名',検索値)として条件を指定できます。また=不等号を利用したい場合はカラム名と検索値の間の第二引数として<>=などを渡します。

    nullと比較するにはwhereNullやwhereNotNullというメソッドが容易されていてこれに検索したいカラム名を渡します。

    複数条件を指定する場合、通常のSQLではANDですが、クエリビルダでは2回目以降もwhereメソッドを使います。

    Orの場合はorWhereというメソッドがあります。

    そして、SQLにおいては()で囲んで条件をグループ化していましたが、それをクエリビルダで行う場合はクロージャを使います。

    ...
    ->where(function ($query) {
      $query->where('point', '>', 90)
      ->orWhere('point', '<=', 100);
    })

    他にも、whereのメソッドとして、日付比較を行うwhereDate('カラム名','YYYY-MM-DD')や、BETWEENを構成するwhereBetween('カラム名',['from','to'])、INやNOT IN、whereIn('カラム名',[...])などが用意されています。

  • get,first,value

    クエリビルダデータを取得しようとした際、結果の受け取り方にはいくつかパターンがあります。表(Eloquentコレクション)を取得するのか、行(モデル)を取得するのか、特定のカラムの値を取得するのかといった具合です。

    クエリビルダの最後では通常get()メソッドを呼びます。これにより表(Eloquentコレクション)が返ります。これを、first()メソッドに変えると、先頭の一行がモデルとして戻ります。また、valueメソッドにカラム名を渡すと、最初に出てきた行の指定したカラムの値を取得できます。

    $v = Model::where('id','=',10)->get(); 
    get_class($v); // Eloquent Collection
    $v = Model::where('id','>',10)->first(); 
    get_class($v); // モデル
    $v = Model::where('id','<',5)->value('id'); 
    var_dump($v); // 整数値
  • oreder

    ORDERは ->orderBy('カラム名', 'desc')とします。複数存在する場合は->orederBy('カラム名','asc')と連結させていき、最後にgetメソッドを呼びます。

    ...->orderBy('id','asc')->get();

    orderByの代わりにlatestやoldestメソッドではcreate_atの日付順に並べ替えることができます。このメソッドに日付カラム名を渡すことでそのカラムでの順番にすることもできます。

    ...->oldest('updated_at')->get();

    結果をランダムに並び変えるinRandomOrder()というメソッドもあります。

  • group

    グループは、groupByメソッドで構成します。複数ある場合はメソッドに追加します。->groupBy('col1', 'col2')

    また、グループ化後の条件式でさる、HAVINGの指定にはhavingメソッドを使います。これはwhereメソッドと同様の使い方ができます。

  • 結合

    結合式のメソッドにはjoin(内部)、leftJoin(外部)、RightJoin(外部)、crossJoin(クロス)があります。内部と外部結合のメソッドでは(結合テーブル名,元のカラム名,結合条件,結合テーブルのカラム名)となります。

    ->leftJoin('subtable', 'main_table.id', '=', 'sub_table.id');
    

    複数カラムの結合では、クロージャをとonメソッドを使います。

    ->leftJoin('subtable', function($join) {
      $join->on('main_table.id', '=', 'sub_table.id')->orOn(...);
    });
  • SQL

    SQLを直接記述したい場合は、使う場所に応じてselectRaw、whereRaw、orWhereRaw、havingRaw、orHavingRaw、groupByRaw、OrderByRawメソッドを使います。この時プレースホルダ(?)も利用可能で、それを設定した場合には第二引数にバインドする値を配列で渡します。この時メソッドに含まれる部分(SELECTやORDER BYなど)は記述しません。

    Model::selectRaw('id,name')->whereRaw('id = ?',[3])->get();
    
  • ロック

    トランザクション中にロックを発行できます。

    共有ロックはsharedLock()メソッド、占有ロックはlockForUpdate()を使います。

    getメソッドを呼ぶ前にメソッドを呼びます。

    DB::...->lockForUpdate()->get();

    公式ページには記述がありませんでしたが、ロックの粒度はおそらく基本行ロックでwhereの条件によりエスカレーションすると思われます。

    ロックはトランザクションが終了するまで続きます。

  • トランザクション

    トランザクションはクエリビルダの範疇ではないですが、先のロックに関連するので紹介しておきます。トランザクションはDBファサードから行えます。

    DB::beginTransaction(); // 開始
    DB::rollback(); // ロールバック
    DB::commit(); // コミット

    transactionメソッドは第1引数としてトランザクション処理を記述した関数を持つことができます。

    第2引数にはリトライの回数を指定でき、ロック待ちやデッドロックが起きトランザクションが終了しなかった際には、指定された回数だけリトライして終了します。

    DB::transaction(function(){
      DB::update('update users set login_count = login_count + 1 where id=?',[1]);
      DB::delete('delete from sleep_users where id=:id',['id'=> 1]);
    },3);

カーソル処理

ファサードからのallメソッドや、クエリビルダの結果は通常はテーブルから抽出されたすべての結果が戻ってきます。データが少なければなんの問題もないですが、クエリによっては長大な結果を返すのものもあり、そうした場合メモリを使い果たしてしまうことがあります。

ほかのプログラミング言語でもこのような挙動を回避するためにデータを順に送り出すカーソルという機能を持っています。

LaravelでもPHPのジェネレータ機能を利用してカーソル機能を提供しています。

User::cursor()->each(function($user) {
  var_dump($user);
});

ただし、公式ページによれば、このコードはLaravalの処理としては、モデル(行)が1行ずつ処理されるので、その点では負荷が低いのですが、PHPのPDOドライバはすべての結果を保持しようとするそうです。

そのため、lazyメソッドを利用するように推奨されています。書式はcursorがlazyに変わり、戻り値もlazyコレクションになります。

User::lazy()->each(function($user) {
  var_dump($user);
});

また、この他に結果を指定したまとまりで処理するchunkメソッドも存在します。

User::chunk(5,function($users) {
  foreach($users as $u) {
    var_dump($u);
  }
});

登録と更新

先にプロパティの説明部分でも触れましたが、モデルを登録するにはまずファサードのcreate([key1=>value1,key2=>value2])メソッドを使う方法があります。これは一括割り当てという手法で、$fillableプロパティに対象のカラム名を指定するか、$guardedプロパティに対象外のカラム名を指定して利用します。

それとは別にモデルのインスタンスを生成後saveメソッドを呼ぶ方法があります。

また、新規インスタンスではなく既存のモデルから値を変更してsaveメソッドを呼べば変更もできます。

// 新規
$user = new User;
$user->name = $request->name;
$user->save();

// 変更
$user = User::find(1); 
$user->email = 'changed@email.example.com';
$user->save();

更新ではモデルを呼ばずに先のクエリビルダでwhere条件を指定しておいて、最後でupdateメソッドを呼ぶ方法もあります。updateメソッドでは連想配列で、変更したいカラム名とその値を渡します。

User::where('id','>',10)->update(['updated_at'=>now()]);

存在しなければ「登録」、存在すれば更新というUPSERTも用意されています。

書式はModel::updateOrCreate([カラムと条件の連想配列],[存在した場合に更新するカラムと値の連想配列])とします。

存在しなかった場合は新規挿入されるのですが、この時の値は第1引数と第2引数を合成した値となります。

User::updateOrCreate(
  ['email' => 'unkown@email.example.com', 'name' => '安藤', 'password' => 'default-password'],
  ['id' => 100]
);

複数の行のupsertを指示したい場合用に、upsertメソッドも用意されています。

書式はModel::upsert([[行],[行]],[キーカラム],[存在時上書きするカラム])となります。

User::upsert([
    ['email' => 'abc@email.example.com', 'name' => '飯山', 'password' => 'default-passsword'],
    ['email' => 'def@email.example.com', 'name' => '内田', 'password' => 'default-passsword']
], ['email'], ['password']);

これらのupsert処理においても、$fillableまた$guardedプロパティが設定されている必要があります。指定されているカラムが更新可能ではなかった場合は無視されるようです(テーブルの制約に触れなければNULL等のデフォルト値となります)。また、モデルでタイムスタンプが有効なら、created_atやupdated_atは省略して自動処理に任せることができます。

モデルの削除

任意のモデルを読みだしておいて、deleteメソッドを呼ぶとその行は削除されます。ただし先のプロパティで紹介したようにソフト削除が設定されている場合は削除フラグが立ち実際にデータが消えることはありません。

$user = User::find(1);
$user->delete();

モデルのファサードから、truncateメソッドを呼ぶと、テーブルを切り捨ててインクリメントの値もリセットします。このメソッドはソフト削除の対象外です。ソフト削除を指定してあっても実際に行が消えますので注意が必要です。

同じくモデルのファサードから利用できるdestoryメソッドは、プライマリキーを指定してモデルを取得することなく削除します。プライマリキーが複数存在した場合の渡し方は連続した引数でも、配列でも、コレクションでもいいそうです。destoryメソッドは、ソフト削除の対象となります。

クエリビルダから削除するには最後にdeleteメソッドを呼びます。これもソフト削除の対象となります。

User::truncate();// ソフト削除無視で表を全削除します
User::where('id','<' 10)->delete();

リレーション(関係)

データベース上のテーブルは、単独のテーブルから取得することもありますが、先のクエリビルダのJOINでも紹介したように、結合して用いられることが多いです。

このようなテーブル同士のリレーションもLaravelでは定義することができます。

データベースのテーブル結合は「外部キー(他のテーブルでキーになっている値を持つ列)」を基準に行うことが多いため、「外部キー」という言葉を使って説明しますが、Laravelのリレーションはデータベースのテーブル設定上の外部キーを設けていなくても設定可能です。

リレーションの項目も多岐にわたりますので、ここでは1対1、1対多、多対多の基本的な関係の設定方法を紹介します。

1対1のリレーション

たとえば記事を保存するテーブルがあり、記事はひとつのカテゴリに所属するとします。

また、記事を保存するarticle、カテゴリ名を保存するcategoryテーブルと、それと同名のモデルがあるとします。

一般的なテーブル設計においてはカテゴリテーブルは独自のidを持ち、記事側で外部キーとしてカテゴリのidを持つ運用になると思いますが、機能の説明のためにカテゴリ側で記事のidを外部キーとして保持します。

この場合記事とカテゴリは1対1で結びつきます。

テーブル設計1

article(id,content)
category(id,article_id,category_name)

Laravelのモデルのデフォルトでは、外部キーとなる記述するモデル名をスネークケース化して最後に「_id」を付けたいう名前になりますので、それに合わせたカラム名にしています。

この状態で、articleモデルで次のようなメソッドを作ると、記事が所属するカテゴリを取得できます。

Article.php

class Article extends Model{
  public function getCategory(){
    return $this->hasOne(Category::class);
  }
}

相手先(カテゴリ)テーブル内で外部キーとなるカラム名を指定するには第2引数に指定します。また、その外部キーはデフォルトでは記事テーブルの「id」カラムと結び付けられますが、キーカラムの名前が違う場合は第3引数に自身のキーの名称を設定します。

Article.php

class Article extends Model{
  public function getCategory(){
    return $this->hasOne(Category::class,'article_id');
    //return $this->hasOne(Category::class,'article_id','customize_id');
  }
}

以上のような状態で、記事モデルからgetCategoryメソッドを実行するリレーションのインスタンスが取得できます。

ここではhasOneメソッドを使ったので、Illuminate¥Database¥Eloquent¥Relations¥HasOneクラスのインスタンスが取得できます。

hasOneクラスから、getResultsメソッドを呼ぶことでクエリが実行されます。先の例の場合は、内部的に次のようなSQLが実行されます。

select * from `categories` where `categories`.`article_id` = 1 and `categories`.`article_id` is not null limit 1

特定の記事からカテゴリを取得するためのコーディング例は次のようになります。

...
$article=Article::find(1);
$hasOne = $article->getCategory();
$results = $hasOne->getResults();
...

これはhasOneは外部キーを保持しない側(記事)から、保持する側(カテゴリ)のデータを取得する際に用いられるものです。今回のような例だとあまり有用ではありませんが、その逆のパターンつまり外部キーを保持する側(カテゴリ)から保持しない側(記事)を取得したい場合があると思います。

その際にはblongsToを使います。blognsToも第1引数に関係となるモデルクラスをとります。第2引数には自身が持つ外部キーのカラム名を指定しますが、何も指定しない場合はメソッド名に_idを付与した値が用いられます。第3引数に外部キーの参照先となるテーブルのキーカラム名を設定します。

Category.php

class Category extends Model{
  public function article(){
    return $this->belongsTo(Article::class);
    //return $this->belongsTo(Article::class,'article_id');
    //return $this->hasOne(Category::class,'article_id','customize_id');
  }
}

ちなみに、ルールからは外れますが、1対1の場合はカテゴリ側でhasOne(Article::class,'id','article_id')としてもgetResultsの値は同様に取得できるようでした。

1対多

一般的には先のようにカテゴリに記事のidを設けるより、記事にカテゴリのidを設けるパターンの方が多いと思います。

その場合先の例と親子関係が逆転します。

テーブル設計

article(id,category_id,content)
category(id,category_name)

まず、1対多の例示の為に、今度は特定のカテゴリからそれに所属する記事一覧を取得してみたいと思います。メソッドがhasOneからhasManyに変わりますが引数に設定する値は同じです。

Category.php

class Category extends Model{
  public function getArticles(){
    return $this->hasMany(Article::class);
    //return $this->hasMany(Article::class,'category_id');
    //return $this->hasMany(Article::class,'category_id','custom_id');
  }
}

hasMayのインスタンスからgetResults()メソッドを呼ぶと複数の行が入ったオブジェクトが返ります。

$cat = Category::first();
$cat->getArticles()->getResults();

all: [
  App¥Models¥Article {
      id: 1,
      category_id: 1,
      contents: "記事1",
    },
  ],
  ...

また、すべての関係はクエリビルダとしても機能するので、$hasMany->where('id','0',10)->getResults()というように、メソッドを間に入れることもできます。値を取得した後は、getResults()[0]->id;というように直接プロパティにアクセスすることもできます。

この逆を行うには、記事側のモデルでblognsToを使います。こちらはメソッド名もあわせて使い方は前述と同じです。逆側(多対1)のひとつの行から見たとき、処理は1対1と同じ挙動になります。

多対多

多対多の使用例としては、「記事」それぞれが複数の「タグ」を持つことができるという場合があります。ひとつの記事から見た時それが所属するタグは複数あり、ひとつのタグからみても、それが複数の記事で利用されている状態です。

この関係をリレーショナルデータベースで設定する場合は、中間テーブルを作成することになると思います。

テーブル設計

article(id,content)
tag(id,tag_name)
article_tag(article_id,tag_id)

記事(article)側でbelongsToManyをモデルに設定し、特定の記事の行から設定されている複数のタグを取得できるようにします。

Article.php

...
pubic function getTags() {
  return $this->belongsToMany(Tag::class);
}

この時の処理は中間と相手側(タグ)の結合テーブルに対して行われます。デフォルトで用いられる中間テーブル名は、ふたつのモデル名を、アルファベット順に並べてアンダーバーでつなげたものになります(ここではarticle_tag)。

特定の記事のひとつからメソッド($article->getTags()->getResults())を呼ぶと、次のようなSQLが実行され、記事に所属する複数のタグが得られます。

select `tags`.*, `article_tag`.`article_id` as `pivot_article_id`, `article_tag`.`tag_id` as `pivot_tag_id` from `tags` inner join `article_tag` on `tags`.`id` = `article_tag`.`tag_id` where `article_tag`.`article_id` = 記事番号;

中間テーブル名を変更したい場合は、第2引数にセットします。また、中間テーブルのカラムの名称を変更したい場合は第3引数(自身側)、第4引数(相手側)に設定できます。

第2引数をarticle_tag2、第3引数をarticle_id2、第4引数をtag_id2とした時実行されるSQLは次のようにかわります。

select `tags`.*, `article_tag2`.`aritcle_id2` as `pivot_aritcle_id2`, `article_tag2`.`tag_id2` as `pivot_tag_id2` from `tags` inner join `article_tag2` on `tags`.`id` = `article_tag2`.`tag_id2` where `article_tag2`.`aritcle_id2` = 記事番号;

さらに、第5引数では中間テーブルと結びつける自身(記事)の列名を、第6引数には相手側(タグ)テーブルと中間テーブルを結びつける為の相手側の列名を指定します。

第2引数以降はすべて省略が可能です。また、nullを設定することでデフォルト値になります。

行の取得時は1対多の時に似ていて$article->getTags()->getResults()とすることで取得できます。

また、$article->getTags()->getResults()->first()->pivotとすることで中間テーブルの行を取得できるのと同時にクエリビルダとしても利用可能です。

逆側の定義も同じメソッドで行います。渡す引数も同じですが自身と相手先という視点だけ反転しますので注意してください(この例の場合なら、自身がタグテーブルになり相手が記事テーブルになります)。

Tag.php

...
pubic function getArticles() {
  return $this->belongsToMany(Article::class);
}

前述の通りLaravelでは複数列のプライマリーキーを管理できません。そのためEloquentモデルの機能を中間テーブルで利用する為には個別のidを付ける必要がありますが、そうした場合それぞれのidを検索する場合にプライマリーキーのindexが使えません、中間テーブルの行数が多くなるようならそれぞれのカラムに対してindexを付与するようにします。

デバッグ

クエリビルダで生成したクエリが実際どのようなSQLになるか確認したい時は、クエリビルダからddまはたdebugメソッドを呼びます。

通常where等で設定した条件などはプレイスホルダ(?)の形でSQL化されて表示されます。それらに適用した値は最後に配列の形でコンソールに出力されます。

ddを実行した場合は処理がそこで中断されます。debugを実行した場合は処理は継続されます。

$query->dump();
"select `created_at`, `author`, `title` from `articles`  where `created_at` between ? and ? 
array:2 [
  0 => "2022-07-01 00:00:00"
  1 => "2022-07-31 23:59:59"
]

筆者紹介


自分の写真
がーふぁ、とか、ふぃんてっく、とか世の中すっかりハイテクになってしまいました。プログラムのコーディングに触れることもある筆者ですが、自分の作業は硯と筆で文字をかいているみたいな古臭いものだと思っています。 今やこんな風にブログを書くことすらAIにとって代わられそうなほど技術は進んでいます。 生活やビジネスでPCを活用しようとするとき、そんな第一線の技術と比べてしまうとやる気が失せてしまいがちですが、おいしいお惣菜をネットで注文できる時代でも、手作りの味はすたれていません。 提示されたもの(アプリ)に自分を合わせるのでなく、自分の活動にあったアプリを作る。それがPC活用の基本なんじゃなかと思います。 そんな意見に同調していただける方向けにLinuxのDebianOSをはじめとした基本無料のアプリの使い方を紹介できたらなと考えています。

広告