業務でマテリアライズド・ビューのテーブルに対して、クエリを実行する関数のLaravelのフレームワークを使ったテストを書きたいとなったときに迷ったので備忘録のために残しておきます。
珍しいケースかなとは思うものの、実際やってみるとそんなに難しくなかったのという感じでした。
ただ、マテリアライズド・ビューのもとになるSQLクエリが複雑だと元データの作成は大変なのでそこのハードルはあるなと思いました。
マテリアライズド・ビューとは?
本来、SQLのビューは実体を持たない仮想テーブルのようなもので、SELECT文などで生成されます。
マテリアライズド・ビューはその点、実体を持つビューでクエリの結果をテーブルとして作成できる仕組みです。
このテーブルは登録、更新、削除などはできず、読み込みのみできるテーブルとなっており、通常のテーブルとは少し異なります。
参考
実際のコードサンプル
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);
}
}
これでマテリアライズド・ビューのテストデータを作成することができます!