使用数据映射器模式:
- 对象/实体不知道数据映射器和存储(例如 RDBMS)。
- 存储不知道数据映射器和对象/实体。
- 数据映射器当然知道并桥接对象/实体和存储。
如何在不了解数据映射器和存储(即 $user
)的情况下验证对象/实体中的唯一字段(例如 $user->name
)不能简单地调用 $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 检测并传递给 Controller 进行记录)。或者,DataMapper 可以捕获并处理异常/MySQL 错误代码,但这违反了单一责任原则,同时没有缓解“拆分验证”问题。
此外,PDO/MySQL 一次只能抛出一个错误。如果有两个或多个唯一列,我们一次只能“验证”其中一个。
在两个地方拆分验证的另一个结果是,如果稍后我们想要添加更多唯一列,那么除了 User 实体之外,我们还必须修改 Register Controller (以及 ChangeEmailAddress 和 ChangeProfile Controller 等)。
第二种方法是我目前正在使用的方法,即将验证分离到一个单独的对象中。
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
),找出错误代码,然后将其作为适当的业务域重新抛出异常(exception)。就是这样。
然后可以在服务层处理该域异常。
您的数据映射器不应该不负责数据完整性检查。这些由 RDBMS 的 CONSTRAINT
定义处理。可用约束的范围当然取决于您使用的 RDBMS。
namespace Model\Mapper;
use Model\Entity;
use Model\Exception;
use Component\DataMapper
class User extends DataMapper
{
// DB $this->connection passing is probably shared, so it's nice to just leave it in superclass
public function store(Entity\User $user)
{
$statement = $this->connection->prepare('INSERT INTO ...');
$statement->bindValue(':email', $user->getEmailAddress());
try {
$statement->execute();
} catch (\PDOException $e) {
if ($e->getCode() === 23000) {
thrown new Exception\EmailAlreadyRegistered;
}
throw $e; // because if this happens, you missed something
}
}
}
关于php - 如何验证数据映射器模式中的唯一性?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47942450/