Взаимосвязи объектов-сущностей (entities) в ORM Doctrine2
Долгое время писал слой доступа к БД на уровне SQL-запросов. Теперь решил попробовать перейти на ORM. В частности — Doctrine2. Есть одна концептуальное недопонимание части процесса работы с entities.
Пример:
try {
$user = $entityManager->find('User', 1);
$user->setName('New name');
$entityManager->flush();
} catch (...)
{
// изменение провалилось по какой-то причине, т.е. объект не был сохранен
}
// ...
$user = $entityManager->find('User', 1);
$user->setOther('Other');
$entityManager->flush(); // <- здесь сохранится и [other], и [name], потому что [name] осталось измененным кодом выше
Проблема следующая. В одном месте кода я запросил $user, изменил его и попытался сохранить. Если бы сохранение прошло удачно, то проблемы бы и не было. Но у меня оно провалилось. Объект $user остался измененным в коде, но не в БД.
Ниже по коду происходит независимый от первой части запрос $user. EntityManager вернет тот же самый объект, который он вернул при первом вызове, т.е. объект с измененным свойством [name]. Но нижний блок кода об этом не знает. Изменив другое поле, код делает повторный запрос на сохранение. Но этот запрос с точки зрения вызывающего будет некорректным, т.к. в результате отправится UPDATE сразу на два свойства [name] и [other], вместо одного только [other].
Я догадываюсь, что просто неправильно работаю с ORM. Помогите мне, пожалуйста, понять правильную идеологию и разрулить приведенную ситуацию. Вопрос не в том, какие костыли вставить в коде, чтобы это работало как надо — я и сам вижу массу вариантов. Вопрос в том, как это нужно делать правильно.
Я бы избегал flush в рекурсии. Вместо detach можно делать refresh, но пересмотрите свою логику. Я могу себе представить лишний флаш в плагине, но делать это в рекурсии в родном модуле думаю неправильно, зачем?
В отличии от привычного вам "слоя SQL запросов", Доктрина не стремится сохранять в БД каждое изменение при первом же чихе пользователя. Изменяйте сущности в пределах разумного, затем один раз обновите все это в хранилище одним flush(). Это правильно и нормально, когда ваш скрипт в начале работы делает select'ы, а в конце — update/delete. И неправильно, когда по ходу работы скрипта в базу летит пачка запросов на модификацию разных полей в одной и той же табличке. Вначале немного ломает, но подиссонируете и попустит =)
Ок. С аспектом flush-ей понятно. Но что делать с таким. Пользователь просит сменить e-mail и в качестве нового значения указывает такое, которое в базе уже есть — дубликат.
// Model:
class UserModel
{
function getUser($id){return $entityManager->find('User', $id);}
function saveUser($user)
{
/*Здесь проверка на дубликаты e-mail-а и мое исключение;*/
$entityManager->persist($user); // в случае TrackingPolicy = DEFERRED_EXPLICIT
}
}
// Controller:
$model = new UserModel;
$user = $model->getUser(1);
$user->setEmail('duplic@te');
$model->saveUser($user); // вот здесь должно вылететь мое исключение о дубликате
// Later in View:
$model = new UserModel;
$user = $model->getUser(1);
echo $user->getEmail(); // напечатан неверный e-mail пользователя
// Later:
$entityManager->flush();
Где бы не находился $entityManager->flush();, ситуация не изменится. Все равно после $user->setEmail() в поле User::$email уже новое значение, которое будет выведено во View. Единственный вариант, который я тут вижу (и наверное это и есть true way в случае ORM) — это проверять валидность адреса прямо в методе $user->setEmail() и не давать установить невалидное значение.
Другой вариант, который мне ну совсем не нравится, это в UserModel::saveUser() в случае ошибок делать либо EntityManager::refresh, либо EntityManager::detach. Оба метода вызовут глобальные неприятные последствия.
Короче, я понимаю, что мое мышление, перенесенное из модели SQL-запросов в случае ORM работает неверно. И я прихожу к пониманию того, что наверное ORM нужно прежде всего там, где объекты долго в памяти — десктопные приложения и серверы на событиях, где не нужно постоянно вытягивать граф объектов из базы. Там объекты расползутся по приложению и изменение в одном месте вызовет изменения во всех других местах, и это круто. Типа $user->setEmail($x); и с этого момента e-mail считается уже измененном.
А в скрипте на обработку запроса все, что нужно — это только мэппинг result set-а в объекты. А дальнейшее отслеживание их состояний не нужно. Когда надо что-то сохранить, проще явно попросить это сделать. Я делаю $user = get(), потом $user->setX($newX), потом save($user) — и только после save я знаю, что все сохранено.
feedbee, какая вообще задача у страницы? Допустим редактирование профиля пользователя. Есди так, то в случае неправильного емэйла, разве правильно сохранять имя и день рождения? Обычно считается неправильным, т.е. в контроллере следует назначать все значения юзеру, и всему что там еще затронуто (также правильно советует Fesor — валидируем, но не обязательно), затем делаем один раз flush для всего, в catch назначаем текст ошибки (такой емэйл уже есть) и показываем редактор с введенными значениями и ошибкой.
Если есть сложности с пониманием форм в симфони2, то есть демонстрационный AcmePizzaBundle с примерами простых и продвинутых форм.
Основной вопрос не в этом. Я подкорректировал код в примере, чтобы убрать неверный акцент. Основной вопрос в том, что объект был получен, изменен, а во второй раз получен уже измененным, хотя это не предполагалось…