大数跨境

存储库模式与原子查询构造设计模式

存储库模式与原子查询构造设计模式 索引目录
2025-08-19
0
导读:关注【索引目录】服务号,更多精彩内容等你来探索!

关注【索引目录】服务号,更多精彩内容等你来探索!

存储库模式 - 基础知识

在存储库模式中,我们通常有少数 CRUD 方法:

class ProductRepository 
{
    public function create(array $data) { /* ... */ }
    public function update(Product $product, array $data) { /* ... */ }
    public function delete(Product $product) { /* ... */ }
    public function find(int $id): ?Product { /* ... */ }
    public function all(): Collection { /* ... */ }
}

这听起来简洁明了。但在实际项目中,事情很少就此结束。虽然这些方法涵盖了众多场景,但开发人员通常会更进一步——使用专门针对特定业务需求的方法来扩展存储库。

存储库模式开始崩溃的地方

迟早,你需要获取“活跃产品”、“按类别分类的产品”或“按商店分类的产品”。通常会发生什么?人们开始在同一个类中堆叠一个又一个方法:

class ProductRepository 
{
    // base methods
    public function create(array $data) { /* ... */ }
    public function update(Product $product, array $data) { /* ... */ }
    public function delete(Product $product) { /* ... */ }
    public function find(int $id): ?Product { /* ... */ }
    public function all(): Collection { /* ... */ }

    // new fetch methods
    public function getActiveProducts() { 
        return Product::where('active', 1)->get();
    }

    public function getProductsByCategory(int $categoryId) { 
        return Product::where('category_id', $categoryId)->get();
    }

    public function getActiveProductsByCategory(int $categoryId) {
        return Product::where('active', 1)
                      ->where('category_id', $categoryId)
                      ->get();
    }

    public function getProductsByStore(int $storeId) { 
        return Product::where('store_id', $storeId)->get();
    }

    public function getActiveProductsByStore(int $storeId) { 
        return Product::where('active', 1)
                      ->where('store_id', $storeId)
                      ->get();
    }

    // and so on...
}

这导致存储库包含数十种方法,违反了该模式的初衷。

存储库困境

如果开发人员坚持使用存储库模式原有的 5 种方法,他们就会将逻辑推到服务、助手和其他类中。这会将逻辑分散到多个类中,并掩盖单一事实来源。重复的情况会where('active', 1)蔓延到各个地方,使代码更难以导航和维护。

存储库中参数的作用

另一个重要方面是,存储库通常依赖参数来决定要获取哪些数据。理论上,这可以保持灵活性。但在实践中,便利性通常更重要——开发人员会创建以特定查询命名的专用方法(例如getActiveProductsByStore)。虽然更容易记忆,但这种方法很快就会导致重复和不必要的代码臃肿。因此,参数是创建新方法的决策者。

原子查询构造(AQC)如何解决问题

AQC 通过将职责分解为专注的专用类来应对这些挑战。每个 AQC 类都接受参数,动态构建查询并返回结果。AQC 不会将查询分散到多个存储库方法或服务类中,而是将它们集中在一个结构化的位置。

思维方式也发生了转变:无需创建数十个硬编码方法,只需在一个类中定义所有可能的条件。然后,查询会根据提供的参数动态构建,从而保持灵活性、一致性,并且更易于维护。

因此,我们现在不再有具有 5 种方法的单个类,而是有 5 个类,每个类都有一个handle()方法。

└── AQC/
    └── Product/
        └── CreateProduct.php
        └── UpdateProduct.php
        └── GetProducts.php
        └── GetProduct.php           
        └── DeleteProduct.php

GetProducts课程为例:

namespace App\AQC\Product;

use App\Models\Product;

class GetProducts
{
    public static function handle($params = [], $paginate = true, $scenario = 'default')
    {
        $productObj = Product::latest('id');

        //apply only when requested
        if (isset($params['active'])) {
            $productObj->where('active', $params['active']);
        }

        // apply only when requested
        if (isset($params['store_id'])) {
              $productObj->where('store_id', $params['store_id']);
        }

        // add more conditions for different use cases

        switch ($scenario) {
            case 'minimal':
                $productObj->select(['id', 'name']);
                break;
            case 'compact':
                $productObj->select(['id', 'name', 'price', 'image']);
                break;
            case 'admin':
                $productObj->select(['id', 'name', 'price', 'sku', 'image', 'stock', 'cost']);
                break;
            default:
                $productObj->select('*');
        }

        return $paginate
            ? $productObj->paginate(Product::PAGINATE)
            : $productObj->get();
    }
}

每个条件都是可选的,并且仅在相应参数存在时才适用。结果是一个可以处理多种场景的类。

现在,您不必每次都编写新方法,只需调用:

// Get active products
$products = GetProducts::handle(['active' => true]);

// Get category products
$products = GetProducts::handle(['category_id' => 5]);

// Get store products
$products = GetProducts::handle(['store_id' => 12]);

同一种方法可以处理所有情况。

灵活性:AQC 的杀手锏

这就是 AQC 真正闪耀的地方。AQC 无需为每个场景添加新方法,而是依靠参数——每个参数都能带来指数级的查询灵活性。

例如,一开始只有一个条件“活跃”。您可以获取活跃产品。添加store_id,您便可以按商店获取产品,或按商店获取活跃产品。引入category_id,现在您可以进行混合搭配:按类别获取产品、按类别获取活跃产品、按特定商店获取类别获取活跃产品等等。添加brand_id,组合数量将进一步增加——所有这些都无需编写任何额外的方法。

在传统的存储库中,每种场景通常都需要专门的方法。而对于 AQC 来说,它只是另一个参数而已。

namespace App\AQC\Product;

use App\Models\Product;

class GetProducts
{
    public static function handle($params = [], $paginate = true, $scenario = 'default')
    {
        $productObj = Product::latest('id');

        // apply only when requested
        if (isset($params['active'])) {
            $productObj->where('active', $params['active']);
        }

        // apply only when requested
        if (isset($params['store_id'])) {
            $productObj->where('store_id', $params['store_id']);
        }

        // apply only when requested
        if (isset($params['category_id'])) {
            $productObj->where('category_id', $params['category_id']);
        }

        // apply only when requested
        if (isset($params['brand_id'])) {
            $productObj->where('brand_id', $params['brand_id']);
        }

        // add more conditions for different use cases

        switch ($scenario) {
            case 'minimal':
                $productObj->select(['id', 'name']);
                break;
            case 'compact':
                $productObj->select(['id', 'name', 'price', 'image']);
                break;
            case 'admin':
                $productObj->select(['id', 'name', 'price', 'sku', 'image', 'stock', 'cost']);
                break;
            default:
                $productObj->select('*');
        }

        return $paginate
            ? $productObj->paginate(Product::PAGINATE)
            : $productObj->get();
    }
}

以下是它们的称呼。

// Get active products
$products = GetProducts::handle(['active' => true]);

// Get category products
$products = GetProducts::handle(['category_id' => 5]);

// Get store products
$products = GetProducts::handle(['store_id' => 12]);

// Get category products but active
$products = GetProducts::handle(['category_id' => 5, 'active' => true]);

// Get store products but active
$products = GetProducts::handle(['store_id' => 12, 'active' => true]);

// Get Store products but of only specific category and must be active
$products = GetProducts::handle(['store_id' => 12, 'category_id' => 5, 'active' => true]);

这是乘法效应:



这意味着每个新条件都会增加灵活性,而无需增加方法。在存储库中,您必须为每种组合编写一个新方法,这简直是一场噩梦。

但是您可以在存储库中执行相同操作吗?

是的,当然可以在存储库模式中实现相同的模式。让我们看看如何实现。

<?php

namespace App\Repositories;

use App\Models\Product;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class ProductRepository
{
    /**
     * Fetch products with dynamic filters and scenarios.
     */
    public function findAll(array $params = [], bool $paginate = true, string $scenario = 'default')
    {
        $query = Product::latest('id');

        // Apply filters only when present
        if (isset($params['active'])) {
            $query->where('active', $params['active']);
        }

        if (isset($params['store_id'])) {
            $query->where('store_id', $params['store_id']);
        }

        if (isset($params['category_id'])) {
            $query->where('category_id', $params['category_id']);
        }

        if (isset($params['brand_id'])) {
            $query->where('brand_id', $params['brand_id']);
        }

        // Add more conditions as needed...

        // Handle scenarios (projection logic)
        switch ($scenario) {
            case 'minimal':
                $query->select(['id', 'name']);
                break;

            case 'compact':
                $query->select(['id', 'name', 'price', 'image']);
                break;

            case 'admin':
                $query->select(['id', 'name', 'price', 'sku', 'image', 'stock', 'cost']);
                break;

            default:
                $query->select('*');
        }

        return $paginate
            ? $query->paginate(Product::PAGINATE)
            : $query->get();
    }

    /**
     * Find a single product by ID.
     */
    public function find(int $id): Product
    {
        return Product::findOrFail($id);
    }

    /**
     * Create a new product.
     */
    public function create(array $data): Product
    {
        return Product::create($data);
    }

    /**
     * Update an existing product.
     */
    public function update(int $id, array $data): Product
    {
        $product = $this->find($id);
        $product->update($data);

        return $product;
    }

    /**
     * Delete a product by ID.
     */
    public function delete(int $id): bool
    {
        $product = $this->find($id);
        return $product->delete();
    }
}

以下是我们的使用方法。

$productRepo = new \App\Repositories\ProductRepository();

// Get active products
$products = $productRepo->findAll(['active' => true]);

// Get category products
$products = $productRepo->findAll(['category_id' => 5]);

// Get store products
$products = $productRepo->findAll(['store_id' => 12]);

// Get category products but active
$products = $productRepo->findAll(['category_id' => 5, 'active' => true]);

// Get store products but active
$products = $productRepo->findAll(['store_id' => 12, 'active' => true]);

// Get Store products but of only specific category and must be active
$products = $productRepo->findAll(['store_id' => 12, 'category_id' => 5, 'active' => true]);

虽然我们已经做到了这一点,但在存储库类方法中定义它会使类变得非常庞大,有时我们可能需要滚动。与findAll()类似 ,其他方法也可能具有相同的 where 条件,从而使类变得庞大。

我们必须定义所有 Where 条件吗?

这里可能会出现一个问题。我们是否需要提前将所有列定义为可选的 where 条件?

答案很简单:不。我们只会在系统中真正需要的时候才写条件。如果我们不需要折扣商品,就不会写这个条件。

但是当我们确实添加一个时,我们添加一次并立即解锁所有组合。

除了 GetProducts 之外:其他操作

AQC 不仅限于获取查询。考虑其他操作:

  • DeleteProduct
    :按id(单个记录)或category_id(多个记录)删除。
  • UpdateProduct
    :根据角色、上下文或参数有条件地应用更新。
  • InsertProduct
    :即使插入也可以根据参数而变化 - 例如,根据用户角色保存不同的数据集。

在每种情况下,由参数驱动的可选条件使类既灵活又易于维护。


存储库与 AQC – 平衡的观点

值得注意的是,AQC 的一些优势可以在精心设计的存储库中得到体现。理论上,你可以在存储库方法中使用参数构建灵活的查询。但在实践中:

  • 存储库通常会变得太大(神级)。
  • 或者它们分裂成多个服务/助手,分散逻辑。

AQC 通过将逻辑提取到小型、专用、参数驱动的类中,并将它们分组到公共命名空间下,从而避免了这两种极端情况。这使得代码库更简洁、更易读、更易于维护。

AQC 摘要

  1. 无重复——每个条件只需写一次,而不是分散在多个方法中。
  2. 随着复杂性而扩展——添加一个条件即可解锁指数组合。
  3. 单一事实来源——所有产品获取逻辑都保留在一个类中。
  4. 更清晰的 API – 消费者只需调用handle()并传递参数。

Repository 与 AQC:快速比较



最后的想法

归根结底,我并不认为 AQC 会取代存储库模式——我认为它是一种进化。存储库模式非常适合 CRUD 和简单的查找,但随着实际场景的增多,它开始变得捉襟见肘。AQC 为我们提供了这种缺失的灵活性,无需将类炸成一堆杂乱的方法。一个方法,多个条件,无限的组合。这正是我在项目中追求的控制力和清晰度。


关注【索引目录】服务号,更多精彩内容等你来探索!


【声明】内容源于网络
0
0
索引目录
索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
内容 444
粉丝 0
索引目录 索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
总阅读12
粉丝0
内容444