Как проверить уникальность в Data Mapper Pattern?

Использование Data Mapper Pattern:

  • объект / объект не знает о преобразователе данных и хранении (например, 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`?
}
}

Есть два возможных решения (одно было предложено Терешко), которые я знаю до сих пор, но оба имеют недостатки.

Первое, как предлагает Терешко, это поймать исключение 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 может перехватить и обработать код ошибки Exception / MySQL, но это нарушает единый ответственный принцип, но не устраняет проблему «разделения проверки».

Кроме того, PDO / MySQL может выдавать только одну ошибку за раз. Если существует два или более уникальных столбца, мы можем «проверять» только один из них за раз.

Еще одно следствие разделения проверки на две части состоит в том, что если позже мы захотим добавить больше уникальных столбцов, то в дополнение к сущности пользователя мы также должны изменить контроллер 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 Pattern?

3

Решение

Почему вы хотите выполнять работу СУРБД? Если вы не используете какой-то устаревший API абстракции SQL-соединения (например, мертвый, но не забытый) ext/mysql), пытаясь нарушить UNIQUE ограничение вызовет исключение.

Итак, ваш маппер данных должен просто перехватить это исключение (скажем, использовать PDO, так что это будет PDOException), найдите код ошибки, а затем повторно сгенерируйте его как правильное исключение бизнес-домена. Вот и все.

Это исключение домена может быть обработано на уровне сервиса.

Ваш datamapper должен не нести ответственность за проверку целостности данных. Они обрабатываются СУРБД CONSTRAINT определения. Разумеется, степень доступных ограничений будет зависеть от того, какую СУБД вы используете.

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

}
4

Другие решения

Я думаю, что это сложно. На основании определения Мартина Фаулса:

Слой Mappers (473), который перемещает данные между объектами и
базы данных, сохраняя их независимыми друг от друга и картографа
сам.

Похоже, что ваше решение правильное, так как не только ваш бизнес-домен User не знает ни о каком sql, картограф не знает о вашей бизнес-области UserОн полностью отделен тем, что ни один из них не знает другого.

ИМО UserMapper должен знать о БД, так как это его работа:

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, и в этом случае должно быть какое-то уникальное ограничение для таблицы. .name для создания, поэтому удалась только одна из вставок!

1

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector