使用数据映射器模式:
- 一个对象/实体不知道数据映射器和存储(例如RDBMS)。
- 存储不知道数据映射器和对象/实体。
- 数据映射器当然很清楚并桥接对象/实体和存储。
我如何验证对象/实体中的唯一字段(例如$user->name
),而无需了解数据映射器和存储(即$user
不能简单地调用$userDataMapper->count('name='.$this->name)
)?
class User
{
private $name; // unique
public function validate(): bool
{
// what to put here to validate that $this->name
// is unique in column `users`.`name`?
}
}
到目前为止,我知道有两种潜在的解决方案(Tereško提出了一种),但两者都有缺点。
Tereško建议的第一个是捕获PdoException。
class UserDataMapper
{
public function store($user)
{
$sql = 'INSERT INTO `users` SET `name` = :name, `email_address` = :emailAddress...';
$params =
[
'name' => $user->getName(),
'emailAddress' => $user->getEmailAddress(),
// ...
];
$statement = $this->connection->prepare($sql);
try
{
$statement->execute($params);
}
catch (PDOException $e)
{
if ($e->getCode() === 23000)
{
// problem: can only receive one unique error at a time.
// parse error message and throw corresponding exception.
if (...name error...)
{
thrown new NameAlreadyRegistered;
}
elseif (...email address error...)
{
thrown new EmailAlreadyRegistered;
}
}
throw $e; // because if this happens, you missed something
}
}
}
// Controller
class Register
{
public function run()
{
if ($user->validate()) // first step of validation
{
// second step of validation
try
{
$this->userDataMapper->store($this->user);
}
catch (NameAlreadyRegistered $e)
{
$this->errors->add(... NameAlreadyRegistered ...)
}
catch (EmailAlreadyRegistered $e)
{
$this->errors->add(... EmailAlreadyRegistered ...)
}
// ...other catches...
}
else
{
$this->errors = $user->getErrors();
}
}
}
问题在于,这将在两个位置分配验证,即在实体(用户)和datamapper/Controller内部(由datamapper检测并传递给控制器以记录)。另外,DatamApper可以捕获并处理异常/MySQL错误代码,但这违反了单一责任,同时又不减轻"拆分验证"问题。
此外,PDO/MySQL一次只能丢弃一个错误。如果有两个或更多独特的列,我们一次只能"验证"其中一个。
在两个地方进行拆分验证的另一个后果是,如果以后,我们想添加更多唯一的列,然后再加上用户实体,我们还必须修改寄存器控制器(以及ChangeEmailAddress和Change -Profile Controllers等)。
第二种方法是我目前正在使用的方法,它是将验证分为单独的对象。
Class UserValidation
{
public function validate()
{
if ($this->userDataMapper->count('name='.$user->getName() > 0))
{
$this->errors->add(...NameAlreadyRegistered...);
}
if ($this->userDataMapper->count('email_address='.$user->getEmailAddress() > 0))
{
$this->errors->add(...EmailAlreadyRegistered...);
}
}
}
// Controller
class Register
{
public function run()
{
if ($this->userValidation()->validate())
{
$this->userDataMapper()->store($user);
}
else
{
$this->errors = $this->userValidation()->getErrors();
}
}
}
这有效。直到扩展实体为止。
class SpecialUser extends User
{
private $someUniqueField;
}
// need to extend the UserValidation to incorporate the new field(s) too.
class SpecialUserValidation extends UserValidation
{
public function validate()
{
parent::validate();
// ...validate $this->user->someUniqueField...
}
}
对于每个实体子类,需要一个验证子类。
所以,我们回到了我的最初问题。如何(正确)验证数据映射器模式中的唯一性?
为什么要做RDBMS的工作?除非您使用一些过时的SQL连接抽象API(例如死者,但没有被遗忘的ext/mysql
),否则尝试违反UNIQUE
约束将导致异常。
因此,您的数据映射器应该捕获该异常(假设使用PDO,因此将是PDOException
),找出错误代码,然后将其重新插入为适当的业务域例外。就是这样。
然后可以在服务层处理该域异常。
您的datamapper应该不对数据完整性检查负责。这些由RDBMS的CONSTRAINT
定义处理。当然,可用约束的范围将取决于您使用的RDBMS。
namespace ModelMapper;
use ModelEntity;
use ModelException;
use ComponentDataMapper
class User extends DataMapper
{
// DB $this->connection passing is probably shared, so it's nice to just leave it in superclass
public function store(EntityUser $user)
{
$statement = $this->connection->prepare('INSERT INTO ...');
$statement->bindValue(':email', $user->getEmailAddress());
try {
$statement->execute();
} catch (PDOException $e) {
if ($e->getCode() === 23000) {
thrown new ExceptionEmailAlreadyRegistered;
}
throw $e; // because if this happens, you missed something
}
}
}
我认为这很棘手。基于Martin Fowlers的定义:
一层映射器(473),该映射器在对象和一个之间移动数据 数据库使它们彼此独立和映射器 本身。
看来您的解决方案是正确的,因为您的业务域User
不知道任何SQL,Mapper不了解您的业务域User
,而且完全脱钩了,因为它都不知道另一个。
imo UserMapper
应该绝对意识到DB,因为这是工作:
class UserDataMapper
{
private $db; // this should be avoided?
private $name;
public function __construct($db, $name)
{
$this->db; // bad? NOPE!
$this->name = $name;
}
public function validate()
{
if ($this->db->count('name='.$this->name) > 0)
return false;
}
}
但是,互换数据的问题是,从您的实体User
传递给数据映射器的是什么?现在,它只需要传递名称,但将来很可能还有许多其他领域。幸运的是,干净的体系结构有关于通过的建议。
现在,验证问题是并发,如果两个并发的Apache线程/进程正在创建具有相同名称的用户,则两者都可以得到计数== 0,在这种情况下,需要某种唯一的唯一table.name创建的限制,因此只有一个插入成功!