数据库存储集合_处理集合根的集合-存储库模式

数据库存储集合

One of the most typical aspects of traditional Domain-Driven Design (DDD) architectures is the imperative persistence agnosticism exposed by the Domain Model. In more conservative designs, including several implementations based on Active Record or Data Table Gateway (which in pursuit of a rather deceiving simplicity often end up poisoning domain logic with infrastructure), there’s always an explicit notion of an underlying storage mechanism living and breathing down the line, usually a relational database. Domain Models on the other hand are conceptually designed from the beginning in a rigid “storage-unaware” nature, thus shifting any persistence logic out of their boundaries.

传统域驱动设计 (DDD)架构最典型的方面之一是域模型暴露的命令式持久性不可知论。 在更保守的设计中,包括基于Active RecordData Table Gateway的几种实现(为了追求诱人的简单性,往往会最终破坏基础架构的域逻辑),始终存在一个明确的概念,即底层存储机制在不断地生存和呼吸。行,通常是一个关系数据库。 另一方面,领域模型是从一开始就以严格的“无存储意识”性质进行概念设计的,因此将任何持久性逻辑都移出了它们的边界。

Even considering that DDD is somewhat elusive when it comes to making a direct reference to the “database,” in the real world there most likely will be at least one sitting behind the scenes since the Domain Model must ultimately be persisted in one form or another. It’s pretty usual therefore to have a mapping layer deployed somewhere between the Model and the Data Access layer. Not only does this actively push for maintaining a decent level of isolation between each layer, but it shields every complex detail involved in moving domain objects back and forward across the seams of the layers in question from the client code.

即使考虑到直接引用“数据库”时DDD还是有些难以捉摸,但在现实世界中,由于域模型最终必须以一种或另一种形式持久存在,因此很可能至少会有一个人坐在幕后。 因此,通常在模型和数据访问层之间的某个位置部署一个映射层。 这不仅积极推动了各层之间保持良好的隔离水平,而且还屏蔽了涉及将域对象前后移入相关层的接缝中来回移动的所有复杂细节与客户端代码。

Mea culpa aside, it’s fair to admit dealing with the oddities of a layer of Data Mappers is quite a burden, often dropped into a “code once/use forever” strategy. Even though, the above schema performs decently well in fairly simplistic conditions where there are just a few domain classes handled by a small number of mappers. The situation can become a lot more awkward however when the model starts to bloat and increase in complexity, since additional mappers will be surely added over time.

除了Mea culpa ,公平地承认处理Data Mappers层的怪异是很重的负担,通常会落入“一次编码/永远使用”的策略。 即使这样,上面的模式在相当简单的情况下也能很好地执行,在这种情况下,少数映射器处理的只有少数领域类。 但是,当模型开始膨胀并增加复杂性时,情况可能会变得更加尴尬,因为随着时间的推移,肯定会添加其他映射器。

This shows in a nutshell that opening the doors of persistence ignorance when working with rich Domain Models, composed of several complex aggregate roots, can be quite difficult to accomplish in practice, at least without having to create expensive object graphs in multiple places or treading the sinful path of duplicated implementations. Worse, in large systems that need to pull expensive collections of aggregate roots from the database that match different criteria, the whole query process can be on its own an active, prolific promoter of this flawed duplication when not properly centralized through a single entry point.

简而言之,这表明,在使用由多个复杂的聚合根组成的丰富的领域模型时,打开持久性无知之门在实践中可能非常困难,至少无需在多个位置创建昂贵的对象图或踩踏复制实现的罪恶之路。 更糟糕的是,在大型系统中,需要从数据库中提取符合不同条件的昂贵的集合根集合,如果没有通过单个入口点对其进行适当的集中化,则整个查询过程可能独自成为该有缺陷重复的活跃,多产的推动者。

In such convoluted use cases, the implementation of an additional abstraction layer, commonly known in DDD parlance as a Repository, which mediates between the Data Mappers and the Domain Model, can effectively help to reduce query logic duplication to a minimum while exposing onto the Model the semantics of a real in-memory collection.

在这种复杂的用例中,在数据映射器和域模型之间进行中介的附加抽象层(在DDD中通常称为存储库)的实现可以有效地帮助将查询逻辑重复减少到最低程度,同时将其公开到模型上。真正的内存中集合的语义。

Unlike mappers, though, which are part of the infrastructure, a repository characterizes itself as speaking the model’s language, as it’s intimately bound to it. And because of its implicit dependency on the mappers, it preserves the persistence ignorance as well, therefore providing a higher level of data abstraction, much closer to the domain objects.

但是,与作为基础结构一部分的映射器不同,存储库将自己描述为说模型的语言,因为它与模型紧密相关。 并且由于它对映射器的隐式依赖,它还保留了持久性无知,因此提供了更高级别的数据抽象,更接近域对象。

It’s sad but true the benefits a repository brings to the table can’t be so easily realized for every single application that might exist out there, hence its implementation is only worthwhile if the situation warrants. Anyway, it’d be pretty informative to build a small repository from scratch so that you can see its inner workings and unveiling what’s actually beneath its rather esoteric shell.

遗憾的是,确实如此,对于可能存在于其中的每个应用程序,存储库带来的好处无法如此轻易地实现,因此只有在情况允许的情况下,才值得实施它。 无论如何,从头开始构建一个小的存储库将是非常有益的,以便您可以看到其内部工作原理并揭示其深奥的外壳之下的实际内容。

做一些初步的基础工作 (Doing some Preliminary Groundwork)

The process of implementing a repository can be pretty complex, because it actually hides all the nuts and bolts of injecting and handling the Data Mappers behind a simplified collection-like API, which in turn also inject some kind of persistence adapter, and so on. This successive injection of dependencies, coupled to the hiding of extensive logic, explains why a repository is often considered a plain Façade, even when some opinions currently diverge from that concept.

实现存储库的过程可能非常复杂,因为它实际上将注入和处理数据映射器的所有细节隐藏在简化的类似集合的API之后,而后者又注入了某种持久性适配器,依此类推。 这种相继的依赖关系注入,加上隐藏的广泛逻辑,解释了为什么即使当前某些观点与该概念存在分歧,存储库也经常被视为简单的外观

In either case, the first step that we should take to get a functional repository up and running is create a basic Domain Model. The one that I plan to use here will be charged with the task of modelling generic users, and its bare-bones structure looks like this:

在这两种情况下,要启动并运行功能性存储库的第一步都是创建一个基本的域模型。 我打算在此处使用的对象将负责对通用用户进行建模的任务,其基本结构如下所示:

<?php
namespace Model;

interface UserInterface
{
    public function setId($id);
    public function getId();
    
    public function setName($name);
    public function getName();
    
    public function setEmail($email);
    public function getEmail();
    
    public function setRole($role);
    public function getRole();
}
<?php
namespace Model;

class User implements UserInterface
{
    const ADMINISTRATOR_ROLE = "Administrator";
    const GUEST_ROLE         = "Guest";
    
    protected $id;
    protected $name;
    protected $email;
    protected $role;

    public function __construct($name, $email, $role = self::GUEST_ROLE) {
        $this->setName($name);
        $this->setEmail($email);
        $this->setRole($role);
    }
    
    public function setId($id) {
        if ($this->id !== null) {
            throw new BadMethodCallException(
                "The ID for this user has been set already.");
        }
        if (!is_int($id) || $id < 1) {
            throw new InvalidArgumentException(
                "The user ID is invalid.");
        }
        $this->id = $id;
        return $this;
    }
    
    public function getId() {
        return $this->id;
    }
    
    public function setName($name) {
        if (strlen($name) < 2 || strlen($name) > 30) {
            throw new InvalidArgumentException(
                "The user name is invalid.");
        }
        $this->name = htmlspecialchars(trim($name), ENT_QUOTES);
        return $this;
    }

    public function getName() {
        return $this->name;
    }

    public function setEmail($email) {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException(
                "The user email is invalid.");
        }
        $this->email = $email;
        return $this;
    }
    
    public function getEmail() {
        return $this->email;
    }
    
    public function setRole($role) {
        if ($role !== self::ADMINISTRATOR_ROLE
            && $role !== self::GUEST_ROLE) {
            throw new InvalidArgumentException(
                "The user role is invalid.");
        }
        $this->role = $role;
        return $this;
    }
    
    public function getRole() {
        return $this->role;
    }
}

In this case in particular, the Domain Model is a pretty skeletal layer, barely above a plain data holder capable of validating itself, which defines through just a segregated interface and a banal implementer the data and behavior of some fictional users. To keep things uncluttered and easy to understand, I’m going to keep the model that thin.

尤其是在这种情况下,域模型是一个漂亮的骨架层,仅在能够验证自身的普通数据持有者之上,该层仅通过隔离的接口和平庸的实现者定义了一些虚构用户的数据和行为。 为了使事情整洁易懂,我将模型保持得很薄。

With the model already going about its business in relaxed isolation, let’s make it a little bit richer by adding to it an additional class, responsible for handling collections of user objects. This “addendum” component is just a classic array wrapper implementing the Countable, ArrayAccess and IteratorAggregate SPL interfaces:

由于该模型已经可以轻松地开展业务,因此,通过向其添加一个负责处理用户对象集合的附加类,使其变得更加丰富。 这个“附录”组件只是一个经典的数组包装器,实现了CountableArrayAccessIteratorAggregate SPL接口:

<?php
namespace ModelCollection;
use MapperUserCollectionInterface,
    ModelUserInterface;
    
class UserCollection implements UserCollectionInterface
{
    protected $users = array();
    
    public function add(UserInterface $user) {
        $this->offsetSet($user);
    }
    
    public function remove(UserInterface $user) {
        $this->offsetUnset($user);
    }
    
    public function get($key) {
        return $this->offsetGet($key);
    }
    
    public function exists($key) {
        return $this->offsetExists($key);
    }
    
    public function clear() {
        $this->users = array();
    }
    
    public function toArray() {
        return $this->users;
    }
    
    public function count() {
        return count($this->users);
    }
    
    public function offsetSet($key, $value) {
        if (!$value instanceof UserInterface) {
            throw new InvalidArgumentException(
                "Could not add the user to the collection.");
        }
        if (!isset($key)) {
            $this->users[] = $value;
        }
        else {
            $this->users[$key] = $value;
        }
    }
    
    public function offsetUnset($key) {
        if ($key instanceof UserInterface) {
            $this->users = array_filter($this->users,
                function ($v) use ($key) {
                    return $v !== $key;
                });
        }
        else if (isset($this->users[$key])) {
            unset($this->users[$key]);
        }
    }
    
    public function offsetGet($key) {
        if (isset($this->users[$key])) {
            return $this->users[$key];
        }
    }
    
    public function offsetExists($key) {
        return ($key instanceof UserInterface)
            ? array_search($key, $this->users)
            : isset($this->users[$key]);
    }
    
    public function getIterator() {
        return new ArrayIterator($this->users);
    }
}

In fact, placing this array collection within the model’s boundaries is entirely optional, as pretty much the same results can be yielded by using a plain array. In this case however, by relying on a standalone collection class makes easier to access sets of user objects fetched from the database through an object-oriented API.

实际上,将此数组集合放置在模型的边界内完全是可选的,因为使用普通数组几乎可以得到相同的结果。 但是,在这种情况下,通过依赖独立的集合类,可以更容易地通过面向对象的API访问从数据库中获取的用户对象集。

In addition, considering that the Domain Model must be entirely ignorant about the underlying storage set down in the infrastructure, the next logical step that we should take is implement a mapping layer that keeps it nicely separated from the database. Here are the elements that compose this tier:

此外,考虑到域模型必须完全不了解基础架构中设置的底层存储,因此下一步应该采取的逻辑措施是实现一个映射层,以使其与数据库保持良好的分离。 以下是构成此层的元素:

<?php
namespace Mapper;
use ModelUserInterface;

interface UserCollectionInterface extends Countable, ArrayAccess, IteratorAggregate 
{
    public function add(UserInterface $user);
    public function remove(UserInterface $user);
    public function get($key);
    public function exists($key);
    public function clear();
    public function toArray();
}
<?php
namespace Mapper;
use ModelRepositoryUserMapperInterface,  
    ModelUser;

class UserMapper implements UserMapperInterface
{    
    protected $entityTable = "users";
    protected $collection;

    public function __construct(DatabaseAdapterInterface $adapter, UserCollectionInterface $collection) {
        $this->adapter = $adapter;
        $this->collection = $collection;
    }
    
    public function fetchById($id) {
        $this->adapter->select($this->entityTable,
            array("id" => $id));
        if (!$row = $this->adapter->fetch()) {
            return null;
        }
        return $this->createUser($row);
    }
    
    public function fetchAll(array $conditions = array()) {
        $this->adapter->select($this->entityTable, $conditions);
        $rows = $this->adapter->fetchAll();
        return $this->createUserCollection($rows);
        
    }
    
    protected function createUser(array $row) {
        $user = new User($row["name"], $row["email"],
            $row["role"]);
        $user->setId($row["id"]);
        return $user;
    }
    
    protected function createUserCollection(array $rows) {
        $this->collection->clear();
        if ($rows) {
            foreach ($rows as $row) {
                $this->collection[] = $this->createUser($row);
            }
        }
        return $this->collection;
    }
}

Out of the box, the batch of tasks performed by UserMapper are fairly straightforward, limited to just exposing a couple of generic finders which are charged with pulling in users from the database and reconstructing the corresponding entities through the createUser() method. Moreover, if you’ve already sunk your teeth into a few mappers before, or even written your own mapping masterpieces, surely the above should be pretty easy to understand. Quite possibly the only subtle detail worth stressing is that the UserCollectionInterface has been placed into the mapping layer, rather than in the model’s. I decided to do so pretty much deliberately in this case, as that way the abstraction (the protocol) that the user collection depends on is explicitly declared and owned by the higher-level UserMapper, in consonance with the guidelines promoted by the Dependency Inversion Principle.

开箱即用,由UserMapper执行的UserMapper批任务非常简单,仅限于公开几个通用查找器,这些查找器负责从数据库中引入用户并通过createUser()方法重建相应的实体。 此外,如果您之前已经将牙齿弄成几张地图绘制器,或者甚至撰写了自己的地图绘制杰作,那么肯定可以很容易理解以上内容。 值得强调的唯一细微的细节可能是UserCollectionInterface已放置在映射层中,而不是模型中。 我决定在这种情况下非常有意地这样做,因为与依赖关系反转原理所提倡的指导方针相UserMapper ,用户集合所依赖的抽象(协议)由更高级别的UserMapper明确声明并拥有。 。

With the mapper already set, we could just consume it right out of the box and pull in a few user objects from storage to get the model hydrated in a snap. While at first glance this would seem to be the right path to pick up indeed, in fact we’d be unnecessarily polluting application logic with infrastructure, as the mapper is effectively a part of it. What if down the road it becomes necessary to query user entities according to more distilled, domain-specific conditions, other than just the blanket ones exposed by the mapper’s finders?

设置好映射器后,我们可以立即使用它,并从存储中拉出一些用户对象,以快速将模型水化。 乍一看,这似乎确实是正确的选择,但实际上,我们将不必要地使用基础结构来污染应用程序逻辑,因为映射器实际上是其中的一部分。 如果一路走来,有必要根据提炼的,特定于域的条件查询用户实体,而不仅仅是映射者的发现者暴露的笼统条件?

In such cases, there would be a real need to place an additional layer on top of the mapping one, which not only would provide a higher level of data access, but it would carry chunks of query logic through one single point. This is, in the last instance, the wealth of benefits we’d expect to get from a repository.

在这种情况下,确实需要在映射层的顶部放置一个附加层,这不仅将提供更高级别的数据访问,而且还将通过单个点承载查询逻辑块。 最后,这是我们期望从存储库中获得的大量收益。

实施用户存储库 (Implementing a User Repository)

In production, repositories can implement under their surface pretty much every thing one can think of in order to expose onto the model the illusion of an in-memory collection of aggregate roots. Nevertheless, in this case we just can’t be so naive and expect to enjoy of such expensive luxuries for free, since the repository that we’ll be building will be a pretty contrived structure, responsible for fetching users from the database:

在生产中,存储库几乎可以将人们想到的每件事都实现在其表面之下,以便将聚合根的内存中集合的假象暴露给模型。 但是,在这种情况下,我们不能天真地期望免费享受如此昂贵的奢侈品,因为我们将要建立的存储库将是一个非常虚构的结构,负责从数据库中获取用户:

<?php
namespace ModelRepository;

interface UserMapperInterface
{
    public function fetchById($id);
    public function fetchAll(array $conditions = array());
}
<?php
namespace ModelRepository;

interface UserRepositoryInterface
{
    public function fetchById($id);
    public function fetchByName($name);
    public function fetchbyEmail($email);
    public function fetchByRole($role);
}
<?php
namespace ModelRepository;

class UserRepository implements UserRepositoryInterface
{
    protected $userMapper;
    
    public function __construct(UserMapperInterface $userMapper) {
        $this->userMapper = $userMapper;
    }
    
    public function fetchById($id) {
        return $this->userMapper->fetchById($id);
    }
    
    public function fetchByName($name) {
        return $this->fetch(array("name" => $name));
    }
    
    public function fetchByEmail($email) {
        return $this->fetch(array("email" => $email));
    }
    
    public function fetchByRole($role) {
        return $this->fetch(array("role" => $role));
    }
    
    protected function fetch(array $conditions) {
        return $this->userMapper->fetchAll($conditions);
    }
}

Although sitting on top of a somewhat lightweight structure, the implementation of UserRepository is pretty intuitive considering that its API allows it to pull in collections of user objects from storage that conform to refined predicates which are closely related to the model’s language. Furthermore, in its current state, the repository exposes just some simplistic finders to client code, which in turn exploit the functionality of the data mapper to gain access to the storage.

尽管位于稍微轻巧的结构之上,但考虑到其API允许UserRepository的API允许其从存储中提取符合与模型语言密切相关的精确谓词的用户对象集合,因此该实现非常直观。 此外,在其当前状态下,存储库仅向客户端代码公开一些简单的查找器,从而反过来利用数据映射器的功能来访问存储。

In a more realistic environment, a repository should have the capability of persisting aggregate roots as well. If you’re in the mood to pitch an insert() method or something else along that line to UserRepository, feel free to do so.

在更现实的环境中,存储库还应该具有持久存储聚合根的功能。 如果您打算将insert()方法或其他方法添加到UserRepository ,请随时进行操作。

In either case, one effective manner to catch the actual advantages of using a repository is by example.

在任何一种情况下,以示例的方式来抓住使用存储库的实际优势的一种有效方式。

<?php
use LibraryLoaderAutoloader,
    LibraryDatabasePdoAdapter,
    MapperUserMapper,
    ModelCollectionUserCollection,
    ModelRepositoryUserRepository;

require_once __DIR__ . "/Library/Loader/Autoloader.php";
$autoloader = new Autoloader;
$autoloader->register();

$adapter = new PdoAdapter("mysql:dbname=users", "myfancyusername", "mysecretpassword");
$userRepository = new UserRepository(new UserMapper($adapter, 
    new UserCollection()));

$users = $userRepository->fetchByName("Rachel");
foreach ($users as $user) {
    echo $user->getName() . " " . $user->getEmail() . "<br>";
}

$users = $userRepository->fetchByEmail("username@domain.com");
foreach ($users as $user) {
    echo $user->getName() . " " . $user->getEmail() . "<br>";
}

$administrators = $userRepository->fetchByRole("administrator");
foreach ($administrators as $administrator) {
    echo $administrator->getName() . " " . 
        $administrator->getEmail() . "<br>";
}

$guests = $userRepository->fetchByRole("guest");
foreach ($guests as $guest) {
    echo $guest->getName() . " " . $guest->getEmail() . "<br>";
}

As noted previously, the repository effectively interchanges business terminology with client code (the so-called Ubiquitous Language coined by Eric Evans in his book Domain Driven Design), rather than a lower-level, technical one. Unlike the ambiguity present in the data mapper’s finders, the repository’s methods on the other hand describe themselves in terms of “name,” “email,” and “role,” which are certainly a part of the attributes that model user entities.

如前所述,存储库有效地将业务术语与客户端代码(Eric Evans在其著作《 域驱动的设计》中创造的所谓的泛在语言 )互换,而不是较低级别的技术术语。 与数据映射器的查找器中存在的歧义不同,存储库的方法在另一方面用“名称”,“电子邮件”和“角色”来描述自己,这些名称当然是对用户实体进行建模的属性的一部分。

This distilled higher level of data abstraction, along with the set of full-fledged capabilities required when it comes to encapsulating query logic in complex systems, are certainly among the most compelling reasons which make using repositories appealing in multi-tiered design. Of course, most of the times there’s an implicit trade-off between getting those benefits up front and going through the hassle of deploying an additional abstraction layer, which in more modest applications may be bloated overkill.

提炼出的更高水平的数据抽象,以及在复杂系统中封装查询逻辑时所需的全套功能,无疑是使存储库在多层设计中有吸引力的最令人信服的原因之一。 当然,在大多数情况下,要预先获得这些好处与经历部署额外抽象层的麻烦之间存在隐式的权衡,这在较普通的应用程序中可能会显得过高。

总结思想 (Closing Thoughts)

Being one of the central concepts of Domain Driven Design, repositories can be found in applications written in several other languages, like Java and C#, just to name a few. In PHP however, they’re still relatively unknown, just making their first shy steps in the world. Despite this, there are some well-trusted frameworks, such as FLOW3 and of course Doctrine 2.x, which will help you embrace the DDD paradigm.

作为域驱动设计的中心概念之一,可以在用其他几种语言(例如Java和C#)编写的应用程序中找到存储库。 但是,在PHP中,它们仍然相对陌生,只是迈出了第一步。 尽管如此,还是有一些值得信赖的框架,例如FLOW3 ,当然还有Doctrine 2.x ,它们将帮助您采用DDD范例。

As with any development methodology out there, you don’t have to use repositories in your applications or even smash them unnecessarily with the pile of concepts sitting behind DDD. Just use common sense and pick them up only when you think they’re going to fit your needs. It’s really just that simple.

与现有的任何开发方法一样,您不必在应用程序中使用存储库,甚至不必不必要地使用DDD背后的大量概念粉碎存储库。 只需使用常识并仅在您认为它们将满足您的需求时才选择它们。 真的就是这么简单。

Image via Chance Agrella / Freerangestock.com

图片来自Chance Agrella / Freerangestock.com

翻译自: https://www.sitepoint.com/handling-collections-of-aggregate-roots/

数据库存储集合

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值