LaravelのupdateOrCreateを完全理解!5つの実践的なユースケースと性能最適化テクニック

updateOrCreateとは?基礎から完全に理解する

updateOrCreateメソッドの動作原理と基本構文

updateOrCreateメソッドは、Laravelが提供する強力なEloquent機能の1つです。このメソッドを使用することで、レコードの存在確認と更新/作成を1回のデータベーストランザクションで実行できます。

基本的な構文は以下の通りです:

// 基本構文
Model::updateOrCreate(
    ['検索条件の配列'],  // どのレコードを探すか
    ['更新/作成データの配列']  // どんな値を設定するか
);

// 具体例
User::updateOrCreate(
    ['email' => 'test@example.com'],  // emailで検索
    [
        'name' => '山田太郎',
        'age' => 30,
        'status' => 'active'
    ]
);

動作の流れ:

  1. 第1引数の条件でレコードを検索
  2. レコードが存在する場合:第2引数の値で更新
  3. レコードが存在しない場合:両方の引数の値をマージして新規作成

重要なポイント:

  • トランザクション的な処理:検索と更新/作成が自動的に1つのトランザクションで実行
  • 重複チェック不要:従来必要だった存在確認のコードが不要に
  • 原子性の保証:複数のプロセスが同時に実行しても安全

createとupdateを個別に使用する場合との違い

従来の方法とupdateOrCreateを比較してみましょう:

// 従来の方法
$user = User::where('email', 'test@example.com')->first();
if ($user) {
    $user->update([
        'name' => '山田太郎',
        'age' => 30
    ]);
} else {
    User::create([
        'email' => 'test@example.com',
        'name' => '山田太郎',
        'age' => 30
    ]);
}

// updateOrCreateを使用した場合
User::updateOrCreate(
    ['email' => 'test@example.com'],
    [
        'name' => '山田太郎',
        'age' => 30
    ]
);

updateOrCreateのメリット:

  • コードの簡潔さ:条件分岐が不要で、コード量が大幅に削減
  • パフォーマンス:データベースアクセスが最適化され、クエリ数が削減
  • 安全性:レースコンディションのリスクが低減

データの整合性とレースコンディションの防止方法

updateOrCreateを使用する際の重要な考慮点は、データの整合性の維持です。

// トランザクションを使用した安全な実装例
DB::transaction(function () {
    $result = Product::updateOrCreate(
        ['sku' => 'PROD-001'],
        [
            'name' => '商品A',
            'stock' => DB::raw('stock + 1'),  // 在庫数を安全に増加
            'updated_at' => now()
        ]
    );

    // 関連テーブルの更新も同じトランザクション内で実行
    ProductHistory::create([
        'product_id' => $result->id,
        'action' => 'stock_updated',
        'details' => '在庫数更新'
    ]);
});

整合性を保つためのベストプラクティス:

  1. ユニーク制約の活用
// マイグレーションでユニーク制約を設定
Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('sku')->unique();  // 重複不可のsku
    $table->string('name');
    $table->integer('stock');
    $table->timestamps();
});
  1. 楽観的ロックの実装
class Product extends Model
{
    public function updateStock($quantity)
    {
        return $this->where('version', $this->version)
            ->updateOrCreate(
                ['sku' => $this->sku],
                [
                    'stock' => $quantity,
                    'version' => DB::raw('version + 1')
                ]
            );
    }
}
  1. データベースロックの適切な使用
// 必要に応じてロックを使用
$product = DB::transaction(function () use ($sku) {
    return Product::lockForUpdate()  // 排他ロック
        ->updateOrCreate(
            ['sku' => $sku],
            ['stock' => DB::raw('stock + 1')]
        );
});

実装時の注意点:

  • 常にトランザクションの必要性を検討する
  • 適切なユニーク制約を設定する
  • 必要に応じて楽観的ロックや悲観的ロックを使用する
  • 複数テーブルを更新する場合は特に注意する

updateOrCreateの実践的な使用例5選

ユーザープロフィールの更新と作成の自動化

ソーシャルログイン連携時のユーザープロフィール同期は、updateOrCreateの代表的なユースケースです。

class SocialAuthController extends Controller
{
    public function handleOAuthCallback($provider)
    {
        // OAuth認証情報の取得
        $socialUser = Socialite::driver($provider)->user();

        // ユーザー情報の更新または作成
        $user = User::updateOrCreate(
            ['email' => $socialUser->getEmail()],
            [
                'name' => $socialUser->getName(),
                'oauth_id' => $socialUser->getId(),
                'oauth_type' => $provider,
                'avatar_url' => $socialUser->getAvatar(),
                'last_login_at' => now()
            ]
        );

        // プロフィール詳細情報も同期
        $user->profile()->updateOrCreate(
            ['user_id' => $user->id],
            [
                'bio' => $socialUser->user['bio'] ?? null,
                'location' => $socialUser->user['location'] ?? null
            ]
        );

        Auth::login($user);
        return redirect()->intended('/dashboard');
    }
}

このコードのポイント:

  • メールアドレスを一意のキーとして使用
  • 関連テーブル(profiles)も同時に更新
  • 最終ログイン日時の自動記録

在庫管理システムでの商品情報の更新

外部システムとの在庫同期処理では、updateOrCreateを使用して効率的にデータを更新できます。

class InventoryService
{
    public function syncStockLevel(array $stockData)
    {
        return DB::transaction(function () use ($stockData) {
            $product = Product::updateOrCreate(
                ['sku' => $stockData['sku']],
                [
                    'stock_quantity' => DB::raw("stock_quantity + {$stockData['quantity']}"),
                    'last_stock_update' => now()
                ]
            );

            // 在庫履歴の記録
            StockHistory::create([
                'product_id' => $product->id,
                'quantity_changed' => $stockData['quantity'],
                'reason' => $stockData['reason'] ?? 'stock_sync',
                'source' => $stockData['source'] ?? 'api'
            ]);

            // 在庫アラートの確認
            if ($product->stock_quantity <= $product->reorder_point) {
                event(new LowStockAlert($product));
            }

            return $product;
        });
    }
}

実装のポイント:

  • トランザクションでの一括処理
  • DB::rawを使用した数値の増減
  • 在庫履歴の自動記録
  • 在庫アラートの連携

設定値の動的な保存と更新

アプリケーション設定の管理には、updateOrCreateを使用して柔軟な設定管理を実現できます。

class SettingsManager
{
    public function setSetting(string $key, $value, string $group = 'general')
    {
        $setting = Setting::updateOrCreate(
            [
                'key' => $key,
                'group' => $group
            ],
            [
                'value' => is_array($value) ? json_encode($value) : $value,
                'data_type' => is_array($value) ? 'json' : gettype($value),
                'updated_by' => auth()->id(),
                'updated_at' => now()
            ]
        );

        // キャッシュの更新
        Cache::tags('settings')->put("{$group}.{$key}", $value);

        return $setting;
    }

    public function getSetting(string $key, string $group = 'general', $default = null)
    {
        // キャッシュからの取得を試みる
        $cachedValue = Cache::tags('settings')->get("{$group}.{$key}");
        if ($cachedValue !== null) {
            return $cachedValue;
        }

        $setting = Setting::where('key', $key)
            ->where('group', $group)
            ->first();

        if (!$setting) {
            return $default;
        }

        $value = $setting->data_type === 'json' 
            ? json_decode($setting->value, true)
            : $setting->value;

        // キャッシュに保存
        Cache::tags('settings')->put("{$group}.{$key}", $value);

        return $value;
    }
}

関連データを含む複雑なレコードの処理

ECサイトの注文処理など、複数のテーブルを跨ぐ更新処理の例です。

class OrderProcessor
{
    public function processOrder(array $orderData)
    {
        return DB::transaction(function () use ($orderData) {
            // 注文メインデータの処理
            $order = Order::updateOrCreate(
                ['order_number' => $orderData['order_number']],
                [
                    'customer_id' => $orderData['customer_id'],
                    'total_amount' => collect($orderData['items'])->sum('price'),
                    'status' => 'pending',
                    'ordered_at' => now()
                ]
            );

            // 注文詳細の更新
            foreach ($orderData['items'] as $item) {
                $order->items()->updateOrCreate(
                    [
                        'product_id' => $item['product_id'],
                        'order_id' => $order->id
                    ],
                    [
                        'quantity' => $item['quantity'],
                        'unit_price' => $item['unit_price'],
                        'total_price' => $item['quantity'] * $item['unit_price']
                    ]
                );

                // 在庫数の更新
                Product::find($item['product_id'])->decrement('stock', $item['quantity']);
            }

            // 配送情報の更新
            $order->shipping()->updateOrCreate(
                ['order_id' => $order->id],
                $orderData['shipping']
            );

            return $order->load('items', 'shipping');
        });
    }
}

API応答に基づくデータの同期処理

外部APIとの連携時の効率的なデータ同期処理の例です。

class ApiSyncService
{
    public function syncProducts(array $apiProducts)
    {
        $processed = 0;
        $errors = [];

        // チャンク単位での処理
        foreach (collect($apiProducts)->chunk(100) as $chunk) {
            try {
                DB::beginTransaction();

                foreach ($chunk as $apiProduct) {
                    $product = Product::updateOrCreate(
                        ['external_id' => $apiProduct['id']],
                        [
                            'name' => $apiProduct['name'],
                            'description' => $apiProduct['description'],
                            'price' => $apiProduct['price'],
                            'stock' => $apiProduct['stock'],
                            'status' => $apiProduct['status'],
                            'last_synced_at' => now()
                        ]
                    );

                    // カテゴリの同期
                    if (!empty($apiProduct['categories'])) {
                        $product->categories()->sync($apiProduct['categories']);
                    }

                    // 画像の同期
                    if (!empty($apiProduct['images'])) {
                        foreach ($apiProduct['images'] as $image) {
                            $product->images()->updateOrCreate(
                                ['url' => $image['url']],
                                [
                                    'sort_order' => $image['order'],
                                    'alt_text' => $image['alt'] ?? null
                                ]
                            );
                        }
                    }

                    $processed++;
                }

                DB::commit();

            } catch (\Exception $e) {
                DB::rollBack();
                $errors[] = [
                    'chunk' => $chunk->pluck('id'),
                    'error' => $e->getMessage()
                ];
                Log::error('Product sync failed', ['error' => $e->getMessage()]);
            }
        }

        return [
            'processed' => $processed,
            'errors' => $errors
        ];
    }
}

実装のポイント:

  • チャンク単位での処理による負荷分散
  • トランザクション管理
  • エラーハンドリングとログ記録
  • 関連データの同期処理
  • 処理結果のレポート機能

updateOrCreateのパフォーマンス最適化

データベースインデックスの正しい設定方法

updateOrCreateのパフォーマンスを最大化するには、適切なインデックス設計が不可欠です。

// マイグレーションでのインデックス設定例
Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('sku');
    $table->string('name');
    $table->decimal('price', 10, 2);
    $table->integer('stock');
    $table->string('status');
    $table->timestamps();

    // 検索条件として使用する可能性の高いカラムにインデックスを設定
    $table->unique('sku');  // SKUは一意であるべき
    $table->index('status');  // ステータスでの絞り込みが頻繁
    $table->index(['status', 'created_at']);  // 複合インデックス
});

インデックス設計のポイント:

  1. WHERE句の最適化
// 良い例:インデックスが効く
Product::updateOrCreate(
    ['sku' => 'PROD-001'],  // skuカラムにインデックスあり
    ['price' => 1000]
);

// 悪い例:インデックスが効かない
Product::updateOrCreate(
    ['LOWER(sku)' => strtolower('PROD-001')],  // 関数使用
    ['price' => 1000]
);
  1. 複合インデックスの順序
// マイグレーションでの複合インデックス設定
public function up()
{
    Schema::table('orders', function (Blueprint $table) {
        // カーディナリティの高い順に左から指定
        $table->index(['customer_id', 'status', 'created_at']);
    });
}

大量レコード処理時の注意点とバッチ処理テクニック

大量のレコードを処理する際は、メモリ使用量とパフォーマンスの両面で最適化が必要です。

class BulkProductProcessor
{
    private const CHUNK_SIZE = 1000;

    public function syncProducts(array $products)
    {
        // 一時テーブルの作成
        Schema::create('temp_products', function (Blueprint $table) {
            $table->string('sku')->primary();
            $table->string('name');
            $table->decimal('price', 10, 2);
            $table->timestamps();
        });

        try {
            // データを一時テーブルに挿入
            collect($products)
                ->chunk(self::CHUNK_SIZE)
                ->each(function ($chunk) {
                    DB::table('temp_products')
                        ->insert($chunk->toArray());
                });

            // 効率的な一括更新
            DB::statement("
                INSERT INTO products (sku, name, price, created_at, updated_at)
                SELECT t.sku, t.name, t.price, NOW(), NOW()
                FROM temp_products t
                ON DUPLICATE KEY UPDATE
                    name = t.name,
                    price = t.price,
                    updated_at = NOW()
            ");

        } finally {
            // 一時テーブルの削除
            Schema::dropIfExists('temp_products');
        }
    }
}

// キューを使用したバッチ処理
class ProductSyncJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    private $products;
    private $chunkSize;

    public function __construct(array $products, int $chunkSize = 100)
    {
        $this->products = $products;
        $this->chunkSize = $chunkSize;
    }

    public function handle()
    {
        collect($this->products)
            ->chunk($this->chunkSize)
            ->each(function ($chunk) {
                DB::transaction(function () use ($chunk) {
                    foreach ($chunk as $product) {
                        Product::updateOrCreate(
                            ['sku' => $product['sku']],
                            [
                                'name' => $product['name'],
                                'price' => $product['price']
                            ]
                        );
                    }
                });
            });
    }
}

N+1問題の回避とイーガーローディングの活用

updateOrCreate使用時のN+1問題を回避するためのテクニックを紹介します。

class OrderProcessor
{
    public function syncOrders(array $orders)
    {
        // 関連データを事前にロード
        $products = Product::with(['category', 'pricing'])
            ->whereIn('sku', collect($orders)->pluck('product_sku'))
            ->get()
            ->keyBy('sku');

        $customers = Customer::with(['addresses', 'preferences'])
            ->whereIn('email', collect($orders)->pluck('customer_email'))
            ->get()
            ->keyBy('email');

        return collect($orders)->map(function ($orderData) use ($products, $customers) {
            return DB::transaction(function () use ($orderData, $products, $customers) {
                $product = $products[$orderData['product_sku']];
                $customer = $customers[$orderData['customer_email']];

                $order = Order::updateOrCreate(
                    ['order_number' => $orderData['order_number']],
                    [
                        'customer_id' => $customer->id,
                        'product_id' => $product->id,
                        'price' => $product->pricing->current_price,
                        'category_id' => $product->category->id,
                        'status' => 'pending'
                    ]
                );

                // 配送情報の更新
                $order->shipping()->updateOrCreate(
                    ['order_id' => $order->id],
                    [
                        'address' => $customer->addresses->first()->full_address,
                        'preferences' => $customer->preferences->delivery_notes
                    ]
                );

                return $order;
            });
        });
    }
}

実装のポイント:

  1. クエリの最適化
// 良い例:必要なリレーションのみロード
$products = Product::with(['category', 'pricing'])
    ->whereIn('id', $productIds)
    ->get();

// 悪い例:不要なリレーションまでロード
$products = Product::with('*')
    ->whereIn('id', $productIds)
    ->get();
  1. キャッシュの活用
class ProductRepository
{
    private const CACHE_TTL = 3600; // 1時間

    public function findBySku(string $sku)
    {
        $cacheKey = "product:{$sku}";

        return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($sku) {
            return Product::updateOrCreate(
                ['sku' => $sku],
                ['last_checked_at' => now()]
            );
        });
    }
}
  1. クエリログの監視
// クエリログの設定
DB::listen(function ($query) {
    if ($query->time > 100) {  // 100ms以上かかるクエリを記録
        Log::warning('Slow query detected', [
            'sql' => $query->sql,
            'bindings' => $query->bindings,
            'time' => $query->time
        ]);
    }
});

これらの最適化テクニックを適切に組み合わせることで、updateOrCreateを使用する処理のパフォーマンスを大幅に改善できます。ただし、最適化は実際のユースケースやデータ量に応じて適切に選択する必要があります。

updateOrCreateのエラーハンドリング

よくあるエラーパターンと解決方法

updateOrCreate使用時によく遭遇するエラーとその対処方法を解説します。

class ProductService
{
    public function syncProduct(array $data)
    {
        try {
            DB::beginTransaction();

            $product = Product::updateOrCreate(
                ['sku' => $data['sku']],
                [
                    'name' => $data['name'],
                    'price' => $data['price'],
                    'stock' => $data['stock']
                ]
            );

            // 在庫履歴の記録
            $product->stockHistories()->create([
                'quantity_changed' => $data['stock'],
                'type' => 'sync'
            ]);

            DB::commit();
            return $product;

        } catch (QueryException $e) {
            DB::rollBack();

            // MySQLのエラーコードで分岐
            switch ($e->errorInfo[1]) {
                case 1062:  // Duplicate entry
                    throw new DuplicateProductException(
                        "SKU '{$data['sku']}' は既に存在します。",
                        $e
                    );

                case 1452:  // Foreign key constraint
                    throw new InvalidReferenceException(
                        '参照整合性制約エラーが発生しました。',
                        $e
                    );

                default:
                    Log::error('Product sync failed', [
                        'error' => $e->getMessage(),
                        'data' => $data
                    ]);
                    throw new ProductSyncException(
                        'データの同期に失敗しました。',
                        $e
                    );
            }
        } catch (\Exception $e) {
            DB::rollBack();
            throw $e;
        }
    }
}

バリデーションの実装とエラーメッセージのカスタマイズ

入力データの検証と適切なエラーメッセージの実装例です。

class ProductController extends Controller
{
    public function store(Request $request)
    {
        // リクエストバリデーション
        $validator = Validator::make($request->all(), [
            'sku' => [
                'required',
                'string',
                'max:50',
                Rule::unique('products')->ignore(
                    $request->input('id'),
                    'id'
                )
            ],
            'name' => 'required|string|max:255',
            'price' => 'required|numeric|min:0',
            'stock' => 'required|integer|min:0'
        ], [
            'sku.unique' => 'この商品コードは既に使用されています。',
            'price.min' => '価格は0以上で指定してください。',
            'stock.min' => '在庫数は0以上で指定してください。'
        ]);

        if ($validator->fails()) {
            return response()->json([
                'message' => 'バリデーションエラーが発生しました。',
                'errors' => $validator->errors()
            ], 422);
        }

        try {
            $product = Product::updateOrCreate(
                ['sku' => $request->sku],
                [
                    'name' => $request->name,
                    'price' => $request->price,
                    'stock' => $request->stock,
                    'updated_by' => auth()->id()
                ]
            );

            // 監査ログの記録
            activity()
                ->performedOn($product)
                ->causedBy(auth()->user())
                ->withProperties([
                    'old' => $product->getOriginal(),
                    'new' => $product->getAttributes()
                ])
                ->log('product.updated');

            return response()->json([
                'message' => '商品情報を更新しました。',
                'product' => $product
            ]);

        } catch (\Exception $e) {
            Log::error('Product update failed', [
                'error' => $e->getMessage(),
                'user_id' => auth()->id(),
                'data' => $request->all()
            ]);

            return response()->json([
                'message' => '商品情報の更新に失敗しました。',
                'error' => config('app.debug') ? $e->getMessage() : null
            ], 500);
        }
    }
}

例外処理と監視の重要性

効果的な例外処理と監視の実装例を見ていきます。

// カスタム例外クラス
class ProductException extends \Exception
{
    protected $product;
    protected $context;

    public function __construct(
        string $message,
        $product = null,
        array $context = [],
        ?\Throwable $previous = null
    ) {
        parent::__construct($message, 0, $previous);
        $this->product = $product;
        $this->context = $context;
    }

    public function getProduct()
    {
        return $this->product;
    }

    public function getContext()
    {
        return $this->context;
    }
}

// 商品サービスクラス
class ProductService
{
    private $monitor;
    private $logger;

    public function __construct(
        ProductMonitor $monitor,
        LoggerInterface $logger
    ) {
        $this->monitor = $monitor;
        $this->logger = $logger;
    }

    public function updateProductStock(string $sku, int $quantity)
    {
        try {
            DB::beginTransaction();

            // 商品の取得(排他ロック付き)
            $product = Product::where('sku', $sku)
                ->lockForUpdate()
                ->firstOrFail();

            // 在庫数の検証
            if ($quantity < 0 && abs($quantity) > $product->stock) {
                throw new ProductException(
                    '在庫が不足しています。',
                    $product,
                    ['requested' => $quantity, 'available' => $product->stock]
                );
            }

            // 更新処理
            $result = $product->updateOrCreate(
                ['sku' => $sku],
                [
                    'stock' => DB::raw("stock + ({$quantity})"),
                    'last_stock_update' => now()
                ]
            );

            // 在庫履歴の記録
            StockHistory::create([
                'product_id' => $product->id,
                'quantity_changed' => $quantity,
                'performed_by' => auth()->id(),
                'type' => $quantity > 0 ? 'increase' : 'decrease'
            ]);

            DB::commit();

            // 監視メトリクスの記録
            $this->monitor->recordStockUpdate($product, $quantity);

            // 在庫アラートの確認
            if ($result->stock <= $result->reorder_point) {
                event(new LowStockAlert($result));
            }

            return $result;

        } catch (ModelNotFoundException $e) {
            DB::rollBack();
            $this->logger->error('Product not found', ['sku' => $sku]);
            throw new ProductException(
                "商品が見つかりません: {$sku}",
                null,
                ['sku' => $sku],
                $e
            );

        } catch (ProductException $e) {
            DB::rollBack();
            $this->logger->warning($e->getMessage(), [
                'product' => $e->getProduct(),
                'context' => $e->getContext()
            ]);
            throw $e;

        } catch (\Exception $e) {
            DB::rollBack();
            $this->logger->error('Stock update failed', [
                'error' => $e->getMessage(),
                'sku' => $sku,
                'quantity' => $quantity
            ]);
            throw new ProductException(
                '在庫の更新に失敗しました。',
                null,
                ['sku' => $sku],
                $e
            );
        }
    }
}

// モニタリングクラス
class ProductMonitor
{
    public function recordStockUpdate(Product $product, int $quantity)
    {
        // 処理時間の記録
        $executionTime = $product->updated_at->diffInMilliseconds(
            $product->getOriginal('updated_at')
        );

        if ($executionTime > 1000) {  // 1秒以上
            Log::warning('Slow stock update detected', [
                'product_id' => $product->id,
                'execution_time' => $executionTime
            ]);
        }

        // 大きな数量変更の検知
        if (abs($quantity) > 1000) {
            Log::notice('Large stock change detected', [
                'product_id' => $product->id,
                'quantity' => $quantity
            ]);
        }

        // メトリクスの記録
        Metrics::gauge('product.stock', $product->stock, ['sku' => $product->sku]);
        Metrics::counter('product.updates', 1, ['sku' => $product->sku]);
    }
}

これらの実装を通じて、以下のポイントに注意を払うことが重要です:

  1. エラー処理の階層化
  • アプリケーション固有の例外クラス
  • 適切なエラーメッセージ
  • コンテキスト情報の保持
  1. トランザクション管理
  • 適切なロールバック処理
  • デッドロック対策
  • 楽観的ロックの活用
  1. ログとモニタリング
  • エラーの詳細な記録
  • パフォーマンス監視
  • アラート設定

updateOrCreateを使用する際のベストプラクティス

コードの可読性を高めるリファクタリング手法

updateOrCreateを使用するコードをより保守性の高いものにするためのベストプラクティスを紹介します。

class ProductService
{
    private Product $product;
    private ProductValidator $validator;
    private ProductEventDispatcher $events;

    public function __construct(
        Product $product,
        ProductValidator $validator,
        ProductEventDispatcher $events
    ) {
        $this->product = $product;
        $this->validator = $validator;
        $this->events = $events;
    }

    public function syncProduct(array $data): Product
    {
        // バリデーション
        $this->validator->validate($data);

        return DB::transaction(function () use ($data) {
            // 商品の更新または作成
            $product = $this->updateOrCreateProduct($data);

            // 関連データの同期
            $this->syncRelatedData($product, $data);

            // イベント発行
            $this->events->dispatch($product);

            return $product;
        });
    }

    private function updateOrCreateProduct(array $data): Product
    {
        return $this->product->updateOrCreate(
            $this->getSearchCriteria($data),
            $this->getProductData($data)
        );
    }

    private function getSearchCriteria(array $data): array
    {
        return [
            'sku' => $data['sku']
        ];
    }

    private function getProductData(array $data): array
    {
        return [
            'name' => $data['name'],
            'description' => $data['description'] ?? null,
            'price' => $data['price'],
            'status' => $data['status'] ?? 'active',
            'updated_by' => auth()->id()
        ];
    }

    private function syncRelatedData(Product $product, array $data): void
    {
        if (isset($data['categories'])) {
            $product->categories()->sync($data['categories']);
        }

        if (isset($data['images'])) {
            $this->syncProductImages($product, $data['images']);
        }
    }

    private function syncProductImages(Product $product, array $images): void
    {
        foreach ($images as $image) {
            $product->images()->updateOrCreate(
                ['url' => $image['url']],
                [
                    'sort_order' => $image['order'] ?? 0,
                    'alt_text' => $image['alt'] ?? null
                ]
            );
        }
    }
}

ユニットテストの実装方法と重要性

updateOrCreateを使用するコードの効果的なテスト方法を解説します。

class ProductServiceTest extends TestCase
{
    use RefreshDatabase;

    private ProductService $service;
    private Product $product;
    private MockInterface $validator;
    private MockInterface $events;

    protected function setUp(): void
    {
        parent::setUp();

        $this->product = new Product();
        $this->validator = $this->mock(ProductValidator::class);
        $this->events = $this->mock(ProductEventDispatcher::class);

        $this->service = new ProductService(
            $this->product,
            $this->validator,
            $this->events
        );
    }

    /** @test */
    public function it_creates_new_product_when_not_exists()
    {
        // テストデータ
        $data = [
            'sku' => 'TEST-001',
            'name' => 'Test Product',
            'price' => 1000,
            'categories' => [1, 2]
        ];

        // バリデーションのモック
        $this->validator->expects('validate')
            ->with($data)
            ->once();

        // イベントのモック
        $this->events->expects('dispatch')
            ->once();

        // 実行
        $result = $this->service->syncProduct($data);

        // アサーション
        $this->assertDatabaseHas('products', [
            'sku' => 'TEST-001',
            'name' => 'Test Product'
        ]);

        $this->assertEquals([1, 2], $result->categories->pluck('id')->toArray());
    }

    /** @test */
    public function it_updates_existing_product()
    {
        // 既存のデータを作成
        $existing = Product::factory()->create([
            'sku' => 'TEST-001',
            'name' => 'Old Name',
            'price' => 500
        ]);

        // 更新データ
        $data = [
            'sku' => 'TEST-001',
            'name' => 'New Name',
            'price' => 1000
        ];

        // テスト実行
        $result = $this->service->syncProduct($data);

        // アサーション
        $this->assertEquals($existing->id, $result->id);
        $this->assertEquals('New Name', $result->name);
        $this->assertEquals(1000, $result->price);
    }

    /** @test */
    public function it_handles_concurrent_updates()
    {
        $data1 = ['sku' => 'TEST-001', 'name' => 'First Update'];
        $data2 = ['sku' => 'TEST-001', 'name' => 'Second Update'];

        // 並行処理のシミュレーション
        $results = parallel([
            fn() => $this->service->syncProduct($data1),
            fn() => $this->service->syncProduct($data2)
        ]);

        // 一つのレコードのみ存在することを確認
        $this->assertEquals(1, Product::where('sku', 'TEST-001')->count());
    }
}

実際の運用環境での監視とログ管理の方法

運用環境での効果的な監視とログ管理の実装例を紹介します。

class ProductMonitoringService
{
    private MetricsCollector $metrics;
    private LoggerInterface $logger;

    public function __construct(
        MetricsCollector $metrics,
        LoggerInterface $logger
    ) {
        $this->metrics = $metrics;
        $this->logger = $logger;
    }

    public function monitorOperation(callable $operation)
    {
        $startTime = microtime(true);
        $memoryBefore = memory_get_usage();

        try {
            $result = $operation();

            $this->recordMetrics(
                microtime(true) - $startTime,
                memory_get_usage() - $memoryBefore
            );

            return $result;

        } catch (\Exception $e) {
            $this->handleError($e, [
                'execution_time' => microtime(true) - $startTime,
                'memory_usage' => memory_get_usage() - $memoryBefore
            ]);
            throw $e;
        }
    }

    private function recordMetrics(float $duration, int $memoryUsage): void
    {
        $this->metrics->gauge('product.update.duration', $duration);
        $this->metrics->gauge('product.update.memory', $memoryUsage);

        if ($duration > 1.0) {  // 1秒以上
            $this->logger->warning('Slow operation detected', [
                'duration' => $duration,
                'memory_usage' => $memoryUsage
            ]);

            // アラートの送信
            Notification::route('slack', config('monitoring.slack_webhook'))
                ->notify(new SlowOperationDetected($duration));
        }
    }

    private function handleError(\Exception $e, array $context): void
    {
        $this->metrics->increment('product.update.errors');

        $this->logger->error('Operation failed', [
            'error' => $e->getMessage(),
            'context' => $context,
            'trace' => $e->getTraceAsString()
        ]);
    }
}

// 使用例
class ProductController extends Controller
{
    private ProductService $service;
    private ProductMonitoringService $monitor;

    public function update(Request $request)
    {
        return $this->monitor->monitorOperation(function () use ($request) {
            return $this->service->syncProduct($request->all());
        });
    }
}

実装のベストプラクティスまとめ:

  1. コード設計
  • 単一責任の原則に従う
  • 依存性の注入を活用
  • 適切な抽象化レベルを維持
  • 意図が明確な命名を心がける
  1. テスト戦略
  • 境界値のテスト
  • 並行処理のテスト
  • モックの適切な使用
  • 異常系のテスト
  1. 運用管理
  • メトリクスの収集
  • パフォーマンス監視
  • エラー検知
  • アラート設定
  1. コードの品質維持
  • 定期的なリファクタリング
  • コードレビューの実施
  • ドキュメントの更新
  • 技術的負債の管理