如何验证Data Mapper模式中的唯一性?

问题描述 投票:3回答:2

使用数据映射器模式:

  • 对象/实体不知道数据映射器和存储(例如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();
        }
    }
}

问题是这将在两个地方分割验证,即在实体(User)和DataMapper / Controller中(由DataMapper检测并传递给Controller进行记录)。或者,DataMapper可以捕获并处理异常/ MySQL错误代码,但这违反了单一责任原则,同时没有减轻“拆分验证”问题。

另外,PDO / MySQL一次只能抛出一个错误。如果有两个或更多个唯一列,我们一次只能“验证”其中一个。

在两个地方拆分验证的另一个结果是,如果以后我们想要添加更多唯一列,那么除了User实体之外,我们还必须修改Register控制器(以及ChangeEmailAddress和ChangeProfile控制器等)。

第二种方法是我目前正在使用的方法,即将验证分成单独的对象。

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...
    }
}

对于每个实体子类,都需要验证子类。

所以,我们回到原来的问题。如何(正确)验证Data Mapper模式中的唯一性?

php sql oop datamapper
2个回答
4
投票

你为什么要做RDBMS的工作?除非您使用一些过时的SQL连接抽象API(如死但未被遗忘的ext/mysql),否则尝试违反UNIQUE约束将导致抛出异常。

因此,您的数据映射器应该只捕获该异常(假设使用PDO,因此它将是PDOException),找出错误代码,然后将其重新抛出为正确的业务域异常。而已。

然后可以在服务层处理该域异常。

您的数据映射器不应负责数据完整性检查。这些由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
        }
    }

}

1
投票

我认为这很棘手。 Based on martin fowlers definition

一层Mappers(473),它在对象和数据库之间移动数据,同时保持它们彼此独立以及映射器本身。

看起来你的解决方案是正确的,因为不仅你的业务领域User不知道任何sql,映射器不知道你的业务领域User,它完全解耦,因为它们都不知道另一个。

IMO the 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传递到数据映射器的是什么?现在它只需要传递一个名字,但将来很可能会有许多其他领域。 Luckily clean architecture has recommendations on what to pass.


现在验证的问题是并发性,如果两个并发的apache线程/进程正在创建一个具有相同名称的用户,两者都可以得到count == 0,在这种情况下,需要在表上有某种唯一约束.name用于创建,因此只有一个插入成功!

© www.soinside.com 2019 - 2024. All rights reserved.