デザインワン・ジャパン Tech Blog

DesignOne Japan | Activate the World.

【Laravel】頻出Eloquent逆引きリファレンス

概要説明

こんにちは!株式会社デザインワン・ジャパンで口コミサービス エキテンのリニューアルを担当しているサービス開発部の鈴木セシル(@suzuki_cecil_)です。

リニューアルプロジェクト通して溜まったLaravelのEloquentのナレッジを活用して逆引きリファレンスにしてみようかと思います。皆様の実装の助けになれば幸いです。

テーブル

今回は店舗テーブルとカテゴリテーブルおよび中間テーブルを例に出して解説をしていきます。テーブル設計、サンプルデータおよびEloquentモデルは以下の通りです。

ER図

f:id:doj_suzuki_cecil:20211208190559p:plain
ER図

shop(店舗テーブル)

shop_id name prefecture_id review_count photo_count
1 エキテンマッサージ 1 100 90
2 エキテン整骨院 1 90 90
3 エキテン接骨院 2 90 100

category(カテゴリテーブル)

category_id name
1 マッサージ
2 整体
3 整骨
4 接骨

shop_category(中間テーブル)

id shop_id category_id
1 1 1
2 2 1
3 2 2
4 2 3
5 3 1
6 3 2
7 3 4

DDL/DML

DDL/DML

CREATE TABLE `shop` (
  `shop_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(50) COLLATE utf8mb4_bin NOT NULL,
  `prefecture_id` int(11) NOT NULL,
  `review_count` int(11) NOT NULL,
  `photo_count` int(11) NOT NULL,
  PRIMARY KEY (`shop_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO `shop` VALUES (1, 'エキテンマッサージ', 1, 100, 90);
INSERT INTO `shop` VALUES (2, 'エキテン整骨院', 1, 90, 90);
INSERT INTO `shop` VALUES (3, 'エキテン接骨院', 2, 90, 100);

CREATE TABLE `category` (
  `category_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(50) COLLATE utf8mb4_bin NOT NULL,
  PRIMARY KEY (`category_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO `category` VALUES (1, 'マッサージ');
INSERT INTO `category` VALUES (2, '整体');
INSERT INTO `category` VALUES (3, '整骨');
INSERT INTO `category` VALUES (4, '接骨');

CREATE TABLE `shop_category` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `shop_id` int(11) NOT NULL,
  `category_id` int(11) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO `shop_category` VALUES (1, 1, 1);
INSERT INTO `shop_category` VALUES (2, 2, 1);
INSERT INTO `shop_category` VALUES (3, 2, 2);
INSERT INTO `shop_category` VALUES (4, 2, 3);
INSERT INTO `shop_category` VALUES (5, 3, 1);
INSERT INTO `shop_category` VALUES (6, 3, 2);
INSERT INTO `shop_category` VALUES (7, 3, 4);

モデル

Shop

<?php
namespace App\Adapter\Gateway\Dao\Eloquent\Model;
use Illuminate\Database\Eloquent\Model;

class Shop extends Model
{
    protected $table = 'shop';
    protected $primaryKey = 'shop_id';
}

Category

<?php
namespace App\Adapter\Gateway\Dao\Eloquent\Model;
use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    protected $table = 'category';
    protected $primaryKey = 'category_id';
}

ShopCategory

<?php
namespace App\Adapter\Gateway\Dao\Eloquent\Model;
use Illuminate\Database\Eloquent\Model;

class ShopCategory extends Model
{
    protected $table = 'shop_category';
    protected $primaryKey = 'id';
}

A AND (B OR C)

shopテーブルから prefecture_id が 1かつ、 review_count もしくは photo_count が100以上のレコードを取得するとします(該当するレコードはshop_id = 1 のレコード)。

単純に考えると以下のような考えに至るかと思います。

<?php
Shop::query()
    ->where("prefecture_id", "=", 1)
    ->where("review_count", ">=", 100)
    ->orWhere("photo_count", ">=", 100)
    ->get();

しかし、これだと shop_id = 1 のレコードと、 shop_id = 3 のレコードが取得されてしまいます。上記の実装のWhere句を生のSQLで書くと

prefecture_id = 1 AND review_count >= 100 OR photo_count >= 100

となり、評価の順番が prefecture_id = 1, AND review_count >= 100, OR photo_count >= 100 となるため prefecture_id の値に関わらず photo_count >= 100 のレコードが全て取得されてしまうためです。

A AND (B OR C)のように明示的に評価の順番を指定するためには以下のようにクロージャで定義する必要があります。

<?php
Shop::query()
    ->where("prefecture_id", "=", 1)
    ->where(function ($query) {
        $query->where("review_count", ">=", 100)
            ->orWhere("photo_count", ">=", 100);
    })
    ->get();

クロージャで定義した場合はクロージャ内が先に評価されます。そのため review_count >= 100, OR photo_count >= 100 が先に評価され、その後に AND prefecture_id = 1 が評価されることとなり、無事 shop_id = 1 のレコードのみが取得されます。

GROUP BYごとにCOUNTする

shop_categoryテーブルから category_id ごとのレコード数(店舗数)を取得するとします。

直感的には以下のように groupBy メソッドと count メソッドを利用すれば取得できそうです。

<?php
ShopCategory::query()
    ->groupBy("category_id")
    ->count();

しかし、これでは 3 が取得されます。上記の記述ではshopテーブルを category_id でGROUP BYした結果の1行目の値のみが取得されてしまうためです(今回の場合では category_id = 1のレコード数)。

shop_categoryテーブルから category_id ごとのレコード数を取得するためには以下のように DB::raw を利用して生のSQLを併用する必要があります。

<?php
ShopCategory::query()
    ->select(DB::raw('category_id, COUNT(*) AS count'))
    ->groupBy("category_id")
    ->get();

上記の記述によりGROUP BYごとにCOUNTすることが可能となります。

関連テーブルと結合するレコードを取得

category_id = 2 の shop_category のレコードと shop_id で結合する shop テーブルのレコードを取得するとします。

まずは Shop モデルに ShopCategory モデルとの リレーション である category メソッドを定義します。

<?php
namespace App\Adapter\Gateway\Dao\Eloquent\Model;
use Illuminate\Database\Eloquent\Model;

class Shop extends Model
{
    protected $table = 'shop';
    protected $primaryKey = 'shop_id';

    public function category(): HasMany
    {
        return $this->hasMany(ShopCategory::class, 'shop_id', 'shop_id');
    }
}

リレーションを定義した上でクエリは以下のように記述します。

<?php
Shop::query()
    ->whereHas("category", function ($query) {
        $query->where("category_id", "=", 2);
    })
    ->get();

whereHas メソッドは第一引数に指定した名前のリレーションを持つことを条件とします。さらに第二引数にクロージャを定義することができ、クロージャ内でリレーションに条件を追加することができます。今回の場合は shop.shop_id = shop_category.shop_id の条件に AND shop_category.category_id = 2 を追加しています。

これにより関連テーブルと結合するレコードを取得が可能となります。

おわりに

仲間を募集しております

募集中の職種については以下を御覧ください。

www.wantedly.com