カテゴリー: laravel

  • 【laravel】N+1問題について考える

    こんにちは、らいか(@hideiwa1)です。

    今回は、DBを扱う上で注意するべき「N+1問題」について解説します。

    N+1問題とは

    リレーションを使用する場合、関連するモデルには「遅延読み込み」という特性があります。
    「遅延読み込み」とはどういう事かというと、

    プロパティにアクセスされるまで、データを取得しない

    つまり、リレーションをしただけではクエリを実行しないという事です。

    実際のコードで、どのような事が起きるのかを見てみましょう。

    「User」と「Item」が「1対多」でリレーションしているとします。
    このとき、すべてのuserとitemを取得すると、以下の通りになります。

    
      $user = User::all();
    
      foreach($users as $user)
      {
          $user->items;
      }

    この時、どのようなクエリが実行されているかというと、

    
      select * form Users;
      
      select * from Items where Items.user_id = 1 and Items.user_id is not null;
      select * from Items where Items.user_id = 2 and Items.user_id is not null;
      …

    このように、

    ・まず「users」テーブルより全件を取得
    ・次に、user_id毎に「Items」テーブルから一致するものを取得

    という流れになっています。

    この時、「users」が全10件あったとすると、

    ・「users」テーブルより全件を取得 → 1回
    ・user_id毎に「Items」テーブルから一致するものを取得 → 1~10で10回

    合計で11回、クエリを実行することになります。
    これが「N+1問題」となります。
    (10件を取得するのに、11回クエリを実行する)

    解決方法は?

    正直なところ、対象が10件程度でしたらそれほど影響はありません。
    しかし、これが100件、1000件と増えた場合はどうでしょうか?

    クエリの数が多くなれば、その分処理速度が低下することになります。
    そのため、基本的にN+1問題は避けなければなりません。

    N+1問題の解決方法は、「with」メソッドを使用する方法です。

    withメソッドには、「Eagerロード(積極的読み込み)」という特徴があります。
    Eagerロードでは、データが事前に取得されます。

    先ほどと同じ例を使って、流れを見てみましょう。

    
      $user = User::with('item')->get();
    
      foreach($users as $user)
      {
          $user->items;
      }

    この際に実行されるクエリは、

    
      select * form Users;
      
      select * from Items where Items.user_id in (1, 2, ・・・);

    このように、

    ・まず「users」テーブルより全件を取得
    ・次に、「Items」テーブルから全件を取得

    と、2つへ削減されました。
    そして、件数が増えてもクエリが増えることはありません。

    まとめ

    リレーションをする場合は、「with」メソッドを使用する方が良い。

  • 【laravel】リレーションの基本

    こんにちは、らいか(@hideiwa1)です。

    今回は、laravelでテーブル同士を関連付ける「リレーション」の使い方を解説します。
    これにより、関連するテーブル同士の扱いが容易になります。

    リレーションとは

    複数のテーブルを扱う場合、テーブル同士が関連していることがあります。
    例えば、「ユーザー」と「買った商品」、「商品」と「商品のカテゴリー」などです。

    usersテーブル

    idname
    1山田
    2加藤
    3鈴木
    4佐々木

    itemsテーブル

    idnameuser_id
    1りんご2
    2みかん4
    3バナナ1
    4ぶどう3

    例えばこのような、「ユーザー」と「買った品物」のテーブルがあったとします。

    リレーションを使わない場合、ユーザーの名前と買った品物を取得するには次のようになります。

    
      $user_id = 1;
      $user = User::find($user_id);
      $user_name = $user->name;  //名前の取得
    
      $item = Item::where('user_id', '=', $user_id) -> get();
      $item_name = $item->name; //品物名の取得

    これをリレーションを使用すると、次のように簡潔に書くことが出来ます。

    
      $user_id = 1;
      $user = User::find($user_id);
      $user_name = $user->name;  //名前の取得
    
      $item_name = $user->item->name  //品物名の取得

    リレーションの設定

    では、実際にリレーションの設定をしていきましょう。

    まず、テーブル同士の関係には大きく分けて「1対1」、「1対多」、「多対多」の3種類があります。

    1対1

    「1対1」の関係は、1つのモデルが1つのモデルと関連している状態です。
    例えば、「users」テーブルの1人のユーザーが、「items」テーブルの1つの品物とのみ関連している状態です。

    usersテーブル

    idname
    1山田
    2加藤
    3鈴木
    4佐々木

    itemsテーブル

    idnameuser_id
    1りんご2
    2みかん4
    3バナナ1
    4ぶどう3

    この状態では、1つのユーザーは、1つの品物とだけ関係しています。
    また、1つの品物も、1つのユーザーとだけ関係があります。

    この時、「users」テーブルが「親」、「items」テーブルが「子」となります。
    (「子」テーブルには、「親」テーブルのIDを持つ必要があります。)

    この場合のリレーションの設定は、各モデル内で次のように行います。

    
      app/Models/User.php  //親モデル
    
      namespace App\Models;
    
      use Illuminate\Database\Eloquent\Model;
    
      class User extends Model
      {
          /**
           * ユーザーに関連している品物の取得
           */
          public function item()
          {
              return $this->hasOne(Item::class);
          }
      }
    
      app/Models/Item.php  //子モデル
    
      namespace App\Models;
    
      use Illuminate\Database\Eloquent\Model;
    
      class Item extends Model
      {
          /**
           * 品物に関連しているユーザーの取得
           */
          public function user()
          {
              return $this->belongsTo(User::class);
          }
      }

    「親」→「子」へのリレーションには「hasOne()
    「子」→「親」へのリレーションには「belongsTo()

    この設定により、モデルからリレーション先のモデルを呼び出すことが出来ます。
    さらに、リレーション先のモデルのプロパティを呼び出すことも可能です。

    
      $item = User::find(1)->item;
      $item_name = User::find(1)->item->name;

    1対多

    次に「1対多」の関係です。
    こちらは、1つのモデルが複数のモデルと関連している状態です。
    例えば、「users」テーブルの1人のユーザーが、「items」テーブルの複数の品物と関連している状態です。

    usersテーブル

    idname
    1山田
    2加藤
    3鈴木
    4佐々木

    itemsテーブル

    idnameuser_id
    1りんご1
    2みかん2
    3バナナ1
    4ぶどう3

    この状態では、1つのユーザーは、複数の品物と関係しています。
    また、1つの品物は、1つのユーザーとだけ関係があります。

    この時、「users」テーブルが「親」、「items」テーブルが「子」となります。
    (「子」テーブルには、「親」テーブルのIDを持つ必要があります。)

    この場合のリレーションの設定は、各モデル内で次のように行います。

    
      app/Models/User.php  //親モデル
    
      namespace App\Models;
    
      use Illuminate\Database\Eloquent\Model;
    
      class User extends Model
      {
          /**
           * ユーザーに関連している品物の取得
           */
          public function items()  // 複数形
          {
              return $this->hasMany(Item::class);
          }
      }
    
      app/Models/Item.php  //子モデル
    
      namespace App\Models;
    
      use Illuminate\Database\Eloquent\Model;
    
      class Item extends Model
      {
          /**
           * 品物に関連しているユーザーの取得
           */
          public function user()
          {
              return $this->belongsTo(User::class);
          }
      }

    「親」→「子」へのリレーションには「hasMany()
    *メソッド名は複数形

    「子」→「親」へのリレーションには「belongsTo()
    *メソッド名は単数形

    「1対1」の時と変わって、親モデルのメソッドが「複数形」になっているのに注意です。
    これは、1つのユーザーに対して複数の子モデルが関連しているためです。

    そのため、各プロパティにアクセスするにはforeachなどで各配列ごとにする必要があります。

    
      $items = User::find(1)->items;
      foreach(User::find(1)->items as $item)
      {
          $item_name[] = $item->name;
      }

    多対多

    次に「多対多」の関係です。
    こちらは、複数のモデルが複数のモデルと関連している状態です。
    例えば、「users」テーブルの1人のユーザーが、「items」テーブルの複数の品物と関連している状態です。
    さらに、「items」テーブルの1つの品物も、「users」テーブルの複数のユーザーと関連しています。

    usersテーブル

    idname
    1山田
    2加藤
    3鈴木
    4佐々木

    item_userテーブル

    idname
    1りんご
    2みかん
    3バナナ
    4ぶどう

    itemsテーブル

    user_iditem_id
    11
    14
    22
    21

    この状態では、1つのユーザーは、複数の品物と関係しています。
    また、1つの品物は、複数のユーザーと関係があります。

    この場合は、2つのテーブルを結びつける「中間テーブル」が必要になります。
    中間テーブルは2つのモデルを「_(アンダースコア)」でつなげます。
    また、要素に「(モデル名)_id」のカラムを持たせます。

    この場合のリレーションの設定は、各モデル内で次のように行います。

    
      app/Models/User.php 
    
      namespace App\Models;
    
      use Illuminate\Database\Eloquent\Model;
    
      class User extends Model
      {
          /**
           * ユーザーに関連している品物の取得
           */
          public function items()  // 複数形
          {
              return $this->belongsToMany(Item::class);
          }
      }
    
      app/Models/Item.php 
    
      namespace App\Models;
    
      use Illuminate\Database\Eloquent\Model;
    
      class Item extends Model
      {
          /**
           * 品物に関連しているユーザーの取得
           */
          public function users()  // 複数形
          {
              return $this->belongsToMany(User::class);
          }
      }

    リレーションには「belongsToMany()
    *メソッド名は複数形

    今回も複数モデルが取得されるため、各プロパティにアクセスするにはforeachなどで各配列ごとにする必要があります。

    
      $items = User::find(1)->items;
      foreach(User::find(1)->items as $item)
      {
          $item_name[] = $item->name;
      }

    「多対多」のリレーションの使い方については、こちらの記事も参考にしてください。

    【laravel】リレーションbelongsToManyの使い方

    リレーションの応用

    外部キーを変更する

    リレーションは、自動的に外部キーを「親モデルの名前+_id」であると想定します。
    先の「users」(親)と「items」(子)の例では、子モデルに「user_id」が存在すると想定しています。

    しかし、設計上別の名前にしたい場合もあると思います。
    その際には、リレーション時に外部キーの名称を指定することが出来ます。
    (第2引数に指定します)

    また、親モデルのid以外を関連付けたい場合も、リレーションでkeyを指定することが出来ます。
    (第3引数に指定します)

    
      //外部キーをcustomer_keyへ変更
      return $this->hasOne(Item::class, 'customer_key');
    
      //usersテーブルのcustomer_idを利用
      return $this->hasOne(Item::class, 'customer_key', 'customer_id');

    usersテーブル

    idcustomer_idname
    11001山田
    22268加藤
    35812鈴木
    45224佐々木

    itemsテーブル

    idnamecustomer_key
    1りんご1001
    2みかん5812
    3バナナ2268
    4ぶどう5224

    リレーションのデータを全件取得する

    今までは1つのモデルについて、リレーション先のモデルを取得していました。
    今回は、全部のモデルに対して取得してみたいと思います。

    
      $users = User::all();
      foreach($users as $user)
      {
          $items[] = $user->item;
      }

    この方法でもよいのですが、「N+1問題」があるためにあまり推奨されません。
    (ここでは詳しく説明しませんが、クエリの効率が良くありません。)

    【laravel】N+1問題について考える

    よりクエリの効率をよくするためには、「with」メソッドを使用します。

    
      $users = User::with('item')->get();
      foreach($users as $user)
      {
          $items[] = $user->item;
      }

    まとめ

    「1対1」の場合:
    ・親 「hasOne(Model::class)」
    ・子 「belongsTo(Model::class)」

    「1対多」の場合:
    ・親 「hasMany(Model::class)」
    ・子 「belongsTo(Model::class)」

    「1対多」の場合:
    ・親 「belongsToMany(Model::class)」
    ・子 「belongsToMany(Model::class)」

  • 【laravel】リレーションbelongsToManyの使い方

    こんにちは、らいか(@hideiwa1)です。

    今回は、laravelでテーブル同士を関連付ける【belongsToMany】の使い方を解説します。
    これにより、多対多のテーブルの扱いが容易になります。

    リレーションの基本

    まずは、laravelにおけるリレーションの確認です。

    リレーションとは、データベースのテーブル同士を関連づける機能です。
    例えば、「users」テーブルと「items」テーブルを例にしてみましょう。

    1対1

    ユーザーが1つの品物だけを持っていて、その品物がすべて1つずつの場合、「users」テーブルと「items」テーブルの関係は「1対1」の関係です。

    usersテーブル
    id name
    1 山田
    2 加藤
    3 佐々木
    itemsテーブル
    id name users_id 
    1 りんご 1
    2 みかん 3
    3 バナナ 2

    1対多

    ユーザーが複数の品物を持っていて、その品物がすべて1つずつの場合、「users」テーブルと「items」テーブルの関係は「1対多」の関係です。

    usersテーブル
    id name
    1 山田
    2 加藤
    3 佐々木
    itemsテーブル
    id name users_id 
    1 りんご 1
    2 みかん 3
    3 バナナ 1

    多対多

    ユーザーが複数の品物を持っていて、その品物が複数存在する場合、「users」テーブルと「items」テーブルの関係は「多対多」の関係です。
    今回使用するのは、こちらのケースになります。

    usersテーブル
    id name
    1 山田
    2 加藤
    3 佐々木
    item_userテーブル
    user_id item_id
    1 2
    1 1
    3 3
    2 3
    3 1
    itemsテーブル
    id name
    1 りんご
    2 みかん
    3 バナナ
    【laravel】リレーションの基本

    belongsToManyの使い方

    テーブル設計

    では、早速リレーションの設定をしていきましょう。

    まずは、各テーブルを作成します。
    「多対多」の場合、2つのテーブルを結びつける「中間テーブル」が必要となります。

    中間テーブルを作成する際の注意点ですが、以下の通りです。

    関連付けるテーブル名を「_(アンダースコア)」でつなげる

    つなげるテーブル名は「単数形

    つなげる順番は、「アルファベット順

    各テーブルのIDをカラムに指定する
    (今回の場合は「user_id」と「item_id」)

    今回の場合、「item_user」テーブルとなります。

    リレーションの設定

    中間テーブルが作成出来たら、今度はモデルへのリレーションの設定です。

    「多対多」の場合、「belongsToMany」メソッドを使って新しいメソッドを作成します。
    userモデル、itemモデルのそれぞれに設定が必要となります。

    belongsToMany(モデルクラス名、中間テーブル名、外部キー、関連づけるモデルの外部キー)
    第2引数以降は省略可能です。

    
      app/Models/User.php
    
      public funcion items()
      {
          return $this->belongsToMany(Item::class);
      }
    
    
      app/Models/Item.php
    
      public funcion users()
      {
          return $this->belongsToMany(User::class);
      }
    

    データを追加する

    次に、中間テーブルへデータを追加していきましょう。
    データを追加するには、「attach」メソッドを使用します。

    
      $user = User::find(1);
    
      $item_id = 2;
    
      $user->items()->attach($item_id);

    こちらの例では、user_id:1、item_id:2 のデータが追加されます。

    また、attach() の引数には配列を 指定することも可能です。

    データを取得する

    次に、リレーション先からデータを取り出してみましょう。

    リレーション先のデータにアクセスする際には、プロパティ名を指定します。
    中間テーブルのデータにアクセスする際には、「pivot」属性を使用します。

    belongsToMany で関連付けられたモデルには、自動的に pivot 属性が割り当てられます。
    この属性の中には、中間テーブルを示すモデルが含まれています。

    
      $user = User::find(1);
    
      foreach($user->items as $item)
      {
          echo $item->name;
          echo $item->pivot->item_id;
      }

    データを更新する

    中間テーブルのデータを更新する際には、「sync」メソッドを使用します。

    syncメソッドを使用すると、引数に指定されたIDのみが中間テーブルに残るように更新されます。

    
      $user = User::find(1);
    
      $user->items()->sync([1,3]);
    item_userテーブル
    user_id item_id
    1 1
    1 2
    3 2
    2 3
    3 1

    item_userテーブル
    user_id item_id
    1 1
    3 2
    2 3
    3 1
    1 3

    データを削除する

    データを削除する際には、「detach」メソッドを使用します。

    detachメソッドを使用すると、該当するデータを中間テーブルから削除します。
    各モデルのデータは、そのままデータベースに残ります。

    
      $user = User::find(1);
    
      $user->items()->detach(1);
        //user_id:1、item_id:1のデータを削除する
    
      $user->items()->detach();
        //user_id:1のデータを全て削除する

    使い方の応用例

    中間テーブルに項目を追加する

    初期状態では、中間テーブルには各モデルキーのみが存在します。
    この中間テーブルに対して、追加の項目を設定したい場合は、リレーションの設定で項目の指定が必要となります。

    項目を指定するには、「withPivot」メソッドを使用します。
    また、created_at および updated_at のタイムスタンプを使用する場合は、「withTimestamps」メソッドを使用します。

    
      app/Models/User.php
    
      public funcion items()
      {
          return $this->belongsToMany(Item::class)
              ->withPivot('count', 'price')
              ->withTimestamps();
      }

    保存する際は、attachの第2引数へ追加データの配列を指定します。
    sync を使用する場合は、指定のIDに追加データを指定します。

    
      $user = User::find(1);
    
      $item_id = 2;
    
      $user->items()->attach($item_id, ['count' => 3]);
    
      $user->items()->sync([1 => ['count' => 3], 3]);

    中間テーブルにメソッドを追加する

    中間テーブルへメソッドを追加したい場合、中間テーブルのカスタムピボットモデルを定義する必要があります。

    作成する際は、Model ではなく、Pivot を継承する点が注意です。

    
      app/Models/ItemUser.php
    
      namespace App\Models;
    
      use Illuminate\Database\Eloquent\Relations\Pivot;
    
      class ItemUser extends Pivot
    
      public funcion hello()
      {
         echo 'hello';
      }
    

    次に、リレーションへ「using」メソッドを追加します。
    これにより、中間テーブルのメソッドを使用することが出来るようになります。

    
      app/Models/User.php
    
      public funcion items()
      {
          return $this->belongsToMany(Item::class)
              ->using(ItemUser::class);
      }

    メソッドを呼び出す際は、「pivot」属性を使用します。

    
      $user = User::find(1);
    
      foreach($user->items as $item)
      {
          echo $item->pivot->hello();
      }

    まとめ

    「多対多」のリレーションには「belongsToMany(Model::class)」

    「table_table」の中間テーブルが必要

    追加は「attach」、更新は「sync」、削除は「detach」

    中間テーブルの要素を取得するには「pivot」