在PHP中正确的存储库模式设计?



Answers

根据我的经验,以下是对您的问题的一些答案:

问:我们如何处理我们不需要的领域?

答:从我的经验来看,这真的归结为处理完整的实体与专门的查询。

一个完整的实体就像一个User对象。 它具有属性和方法等。它是您的代码库中的头等公民。

即席查询返回一些数据,但除此之外我们什么都不知道。 随着数据被传递到应用程序中,它将在没有上下文的情况下完成。 它是一个User吗? 附带一些Order信息的User ? 我们不知道。

我更喜欢与完整的实体合作。

你是对的,你会经常带回你不会使用的数据,但你可以用不同的方式解决这个问题:

  1. 积极缓存实体,所以您只需从数据库中支付一次读取价格即可。
  2. 花更多的时间为你的实体建模,以便它们之间有很好的区别。 (考虑将一个大的实体分成两个较小的实体等)
  3. 考虑有多个版本的实体。 你可以有一个后端User ,也可以有一个UserSmall用于AJAX调用。 一个可能有10个属性,一个有3个属性。

使用即席查询的缺点:

  1. 你在许多查询中获得基本相同的数据。 例如,对于一个User ,你将最终为许多调用写入基本相同的select * 。 一个电话将获得10个字段中的8个,其中一个将获得10个中的5个,一个将获得10个中的7个。为什么不用10个电话中的10个电话替换全部电话? 这是不好的原因是重新考虑/测试/模拟是谋杀。
  2. 随着时间的推移,你很难在很高层次上推论你的代码。 而不是像“为什么User这么慢?”的陈述。 您最终会追查一次性查询,因此错误修复往往很小且本地化。
  3. 取代底层技术真的很难。 如果您现在将所有内容都存储在MySQL中,并且想要迁移到MongoDB,那么更换100个临时调用要比一些实体更难。

问:我的存储库中有太多的方法。

答:除巩固通话外,我还没有看到任何其他办法。 该方法在您的存储库中调用真正映射到应用程序中的功能。 功能越多,数据特定的调用越多。 您可以推回功能并尝试将类似的调用合并为一个。

在一天结束时的复杂性必须存在某个地方。 通过存储库模式,我们将其推送到存储库界面,而不是制作一堆存储过程。

有时我必须告诉自己:“它必须放弃某处,没有银弹。”

Question

前言:我试图在MVC体系结构和关系数据库中使用存储库模式。

我最近开始在PHP中学习TDD,并且我意识到我的数据库与其他应用程序的耦合非常紧密。 我已阅读了有关存储库的信息,并使用IoC容器将其“注入”到我的控制器中。 非常酷的东西。 但是现在有关于存储库设计的一些实际问题。 考虑下面的例子。

<?php

class DbUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct($db)
    {
        $this->db = $db;
    }

    public function findAll()
    {
    }

    public function findById($id)
    {
    }

    public function findByName($name)
    {
    }

    public function create($user)
    {
    }

    public function remove($user)
    {
    }

    public function update($user)
    {
    }
}

问题#1:太多领域

所有这些查找方法都使用select all字段( SELECT * )方法。 但是,在我的应用程序中,我总是试图限制我获得的字段数量,因为这往往会增加开销并降低速度。 对于那些使用这种模式的人,你如何处理这个问题?

问题#2:方法太多

尽管此课目前看起来不错,但我知道在真实世界的应用程序中,我需要更多的方法。 例如:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • 等等。

正如你所看到的,可能有非常非常长的可能方法列表。 然后,如果您在上面的字段选择问题中添加,则问题会恶化。 在过去,我通常只是将所有这些逻辑放在我的控制器中:

<?php

class MyController
{
    public function users()
    {
        $users = User::select('name, email, status')->byCountry('Canada')->orderBy('name')->rows()

        return View::make('users', array('users' => $users))
    }

}

使用我的存储库方法,我不想结束这个:

<?php

class MyController
{
    public function users()
    {
        $users = $this->repo->get_first_name_last_name_email_username_status_by_country_order_by_name('Canada');

        return View::make('users', array('users' => $users))
    }

}

问题3:不可能匹配界面

我看到使用存储库接口的好处,所以我可以换出我的实现(用于测试目的或其他)。 我对接口的理解是他们定义了一个实现必须遵循的合约。 这很好,直到你开始向findAllInCountry()等存储库添加其他方法。 现在我需要更新我的接口以使用此方法,否则其他实现可能没有它,并且可能会破坏我的应用程序。 由此感觉疯狂...尾巴摇摆狗的情况。

规格模式?

这使我相信存储库应该只有固定数量的方法(如save()remove()find()findAll()等)。 但是,如何运行特定的查找? 我听说过规范模式 ,但在我看来,这只会减少整个记录集(通过IsSatisfiedBy() ),如果您从数据库中提取数据,这显然会出现主要的性能问题。

帮帮我?

很明显,我需要在使用存储库时重新思考一些问题。 任何人都可以启发这如何最好地处理?




我会加一点这个,因为我目前正试图自己掌握所有这些。

#1和2

这是您的ORM完成繁重任务的理想场所。 如果您使用的是实现某种ORM的模型,则可以使用它的方法来处理这些事情。 如果需要,可以自己订购实现Eloquent方法的orderBy函数。 例如使用雄辩:

class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }

你似乎在寻找的是一个ORM。 没有理由你的存储库不能基于一个。 这需要用户扩展雄辩,但我个人并不认为这是一个问题。

如果你想避免ORM,那么你必须“自己推出”来获得你想要的东西。

#3

接口不应该是硬性和快速的要求。 东西可以实现一个接口并添加到它。 它不能做的是不能实现该接口的必需功能。 您也可以像类一样扩展接口以保持干燥。

也就是说,我刚刚开始掌握,但这些实现帮助了我。




我只能评论我们(在我的公司)处理这个问题的方式。 首先,对我们来说性能并不是太大的问题,但是要有干净/正确的代码。

首先我们定义一些模型,例如使用ORM创建UserEntity对象的UserModel 。 从模型加载UserEntity ,加载所有字段。 对于引用外国实体的字段,我们使用适当的外国模式来创建相应的实体。 对于这些实体,数据将按需加载。 现在你最初的反应可能是...... ??? ...... 让我举一个例子举一个例子:

class UserEntity extends PersistentEntity
{
    public function getOrders()
    {
        $this->getField('orders'); //OrderModel creates OrderEntities with only the ID's set
    }
}

class UserModel {
    protected $orm;

    public function findUsers(IGetOptions $options = null)
    {
        return $orm->getAllEntities(/*...*/); // Orm creates a list of UserEntities
    }
}

class OrderEntity extends PersistentEntity {} // user your imagination
class OrderModel
{
    public function findOrdersById(array $ids, IGetOptions $options = null)
    {
        //...
    }
}

在我们的例子中, $db是一个能够加载实体的ORM。 该模型指示ORM加载一组特定类型的实体。 ORM包含一个映射并使用它来将该实体的所有字段注入到实体中。 然而,对于外地领域,只有这些对象的id被加载。 在这种情况下, OrderModel仅创建OrderEntity其中仅包含所引用订单的ID。 当由OrderEntity调用PersistentEntity::getField时,实体指示它的模型将所有字段延迟加载到OrderEntity 。 所有与一个UserEntity关联的OrderEntity都被视为一个结果集,并且会立即加载。

这里的神奇之处在于我们的模型和ORM将所有数据注入到实体中,并且实体仅为PersistentEntity提供的泛型getField方法提供包装函数。 总而言之,我们总是加载所有的字段,但是必要时会加载引用外部实体的字段。 只是加载一堆字段并不是真正的性能问题。 加载所有可能的外国实体,但是会降低性能。

现在基于where子句加载一组特定的用户。 我们提供了一个面向对象的类包,允许您指定可以粘在一起的简单表达式。 在示例代码中,我将它命名为GetOptions 。 它是选择查询的所有可能选项的包装。 它包含where子句,group by子句和其他所有内容的集合。 我们的where子句相当复杂,但您显然可以轻松制作更简单的版本。

$objOptions->getConditionHolder()->addConditionBind(
    new ConditionBind(
        new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
    )
);

这个系统最简单的版本是将查询的WHERE部分作为字符串直接传递给模型。

我很抱歉,这个相当复杂的回应。 我试图尽可能快速和清楚地总结我们的框架。 如果您有任何其他问题随时问他们,我会更新我的答案。

编辑:另外,如果你真的不想立即加载一些字段,你可以在你的ORM映射中指定一个延迟加载选项。 由于所有字段最终都是通过getField方法加载的,因此您可以在调用该方法时最后加载一些字段。 这在PHP中不是一个很大的问题,但我不推荐用于其他系统。




Links