Laravelのテストコードでマテリアライズド・ビューのテーブルに対するクエリのテストをする

業務でマテリアライズド・ビューのテーブルに対して、クエリを実行する関数のLaravelのフレームワークを使ったテストを書きたいとなったときに迷ったので備忘録のために残しておきます。
珍しいケースかなとは思うものの、実際やってみるとそんなに難しくなかったのという感じでした。

ただ、マテリアライズド・ビューのもとになるSQLクエリが複雑だと元データの作成は大変なのでそこのハードルはあるなと思いました。

# マテリアライズド・ビューとは？

本来、SQLのビューは実体を持たない仮想テーブルのようなもので、SELECT文などで生成されます。
マテリアライズド・ビューはその点、実体を持つビューでクエリの結果をテーブルとして作成できる仕組みです。

このテーブルは登録、更新、削除などはできず、読み込みのみできるテーブルとなっており、通常のテーブルとは少し異なります。

参考

https://products.sint.co.jp/siob/blog/materializedview#:~:text=%E3%83%9E%E3%83%86%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%BA%E3%83%89%E3%83%93%E3%83%A5%E3%83%BC%E3%81%A8%E3%81%AF%E3%83%87%E3%83%BC%E3%82%BF,%E6%B0%B8%E7%B6%9A%E5%8C%96%E3%81%A8%E8%A8%80%E3%81%84%E3%81%BE%E3%81%99%E3%80%82

# 実際のコードサンプル

1.マテリアライズド・ビューのもとになるテーブルデータのmigration

```
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        // Products table
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->decimal('price', 10, 2);
            $table->timestamps();
        });

        // Orders table
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->foreignId('product_id')->constrained();
            $table->integer('quantity');
            $table->timestamp('order_date')->useCurrent();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('orders');
        Schema::dropIfExists('products');
    }
};

```

2.マテリアライズド・ビューのテーブルを作成するmigration


```
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

return new class extends Migration
{
    public function up()
    {
        DB::statement("
            CREATE MATERIALIZED VIEW product_sales_summary AS
            SELECT
                p.id AS product_id,
                p.name AS product_name,
                SUM(o.quantity) AS total_quantity_sold,
                SUM(o.quantity * p.price) AS total_sales_amount
            FROM
                products p
            JOIN
                orders o ON p.id = o.product_id
            GROUP BY
                p.id, p.name
        ");
    }

    public function down()
    {
        DB::statement('DROP MATERIALIZED VIEW IF EXISTS product_sales_summary');
    }
};

```

3.マテリアライズド・ビューのもとになるModelとFactoryの作成


```
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    use HasFactory;
}

```

```
<?php

namespace Database\Factories;

use App\Models\Order;
use App\Models\Product;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Order>
 */
class OrderFactory extends Factory
{
    // モデルに対応するファクトリ
    protected $model = Order::class;

    // ファクトリの定義
    public function definition()
    {
        return [
            'product_id' => Product::factory(), // 関連するProductを生成
            'quantity' => $this->faker->numberBetween(1, 10), // 1〜10のランダムな数量
            'order_date' => $this->faker->dateTimeThisYear(), // 今年のランダムな日時
        ];
    }
}


```

```
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use HasFactory;
}

```

```
<?php

namespace Database\Factories;

use App\Models\Product;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product>
 */
class ProductFactory extends Factory
{
    // モデルに対応するファクトリ
    protected $model = Product::class;

    // ファクトリの定義
    public function definition()
    {
        return [
            'name' => $this->faker->word(),
            'price' => $this->faker->randomFloat(2, 10, 1000), // 10〜1000の範囲でランダムな価格
        ];
    }
}

```

4.テスト実行時のFactoryの実行と、マテリアライズド・ビューのRefreshの実行

```
<?php

namespace Tests\Unit;

use App\Models\Order;
use App\Models\Product;
use App\Models\ProductSalesSummary;
use Illuminate\Support\Facades\DB;
use PHPUnit\Framework\TestCase;

class ProductSalesSummaryTest extends TestCase
{
    /**
     * A basic unit test example.
     *
     * @return void
     */
    public function test_example()
    {
        // 複数のProductを生成
        $products = Product::factory(5)->create();

        // 複数のOrderを生成
        Order::factory(5)->sequence(
            ['product_id' => $products[0]->id],
            ['product_id' => $products[1]->id],
            ['product_id' => $products[2]->id],
            ['product_id' => $products[3]->id],
            ['product_id' => $products[4]->id],
        )->create();

        DB::statement('REFRESH MATERIALIZED VIEW product_sales_summary');

        $actual = ProductSalesSummary::all();

        dd($actual);
    }
}

```

これでマテリアライズド・ビューのテストデータを作成することができます！