Совсем недавно Сергей Прохоров ака proxyfabio написал статью Валидация объектов + транзакции. Немного эта тема обсуждалась здесь. От себя хочу добавить, что эта тема крайне важная, и на сегодня это одна из самых главных проблем в разработке крупных проектов на MODX Revolution.
Здесь сразу попрошу не начинать ничего вроде «Если делаете крупные проекты, не надо их делать на MODX, возьмите бла-бла-бла». Мы делали крупные проекты, и не только на MODX. На MODX вполне можно делать крупные проекты, и на сегодня есть всего лишь пара слабых мест, которые мы правим на индивидуальных проектах, в остальном же MODX на 98% пригоден для разработки крупных проектов.
Итак, одна из этих серьезных проблем связана именно с методом xPDOObject::save() (вызываемая при сохранении xPDO-объектов). Суть этой проблемы в том, что внутри него срабатывает метод сохранения связанных объектов xPDOObject::_saveRelatedObjects() дважды. Раз и два. Делается это для того, чтобы выставить первичные и вторичные ключи для этих связанных объектов (см. справочный материал от Ильи Уткина). Объясню подробней на примере. Вот код:
<?php $user_data = array( "username" => "test", ); $profile_data = array(); $user = $modx->newObject('modUser', $user_data); $user->Profile = $modx->newObject('modUserProfile', $profile_data); $user->save(); print '<pre>'; print_r($user->toArray()); print_r($user->Profile->toArray());
В целом наверняка суть этого кода понятна многим, но давайте сосредоточимся на деталях. Когда мы создали два новых объекта ($user и $user->Profile), у них еще нет айдишников, пока их не сохранили. Но сохранив только объект $user, мы на выходе получаем и сохраненный объект $user->Profile. Это как бы тоже понятно почему, Илья в своей статье все это описывает. Но вопрос, который не совсем на виду болтается — это «как xPDO „знает“ какой id у объекта $user, чтобы назначить этот id в качестве $modx->Profile->internalKey?». Для этого давайте опять-таки пробежимся по коду метода xPDO::save();
Вот у нас первый вызов метода $user->_saveRelatedObjects(). В этот момент объект $user еще не сохранен (не записан в базу), id-шника у него еще нет. $user->Profile тоже не сохранен и не имеет ни id, ни internalKey. Переходя к вызову метода $user->_saveRelatedObjects(), мы видим, что идет перебор связанных объектов и их сохранение (метод xPDO::_saveRelatedObject()). Здесь я еще раз уточню, что сохраняем мы объект $user, для которого объект $user->Profile является связанным. И вот здесь-то и получается, что фактически объект $user->Profile сохранится раньше, чем объект $user. Почему? Потому что в вызове $user->_saveRelatedObject($user->Profile) будет вызван метод $user->Profile->save(), а так как в текущий момент для $user->Profile нет связанных объектов, то он будет записан в базу данных. И что у нас здесь получается? $user->Profile уже сохранен и у него есть свой id, но id нет у объекта $user (потому что он еще не был сохранен). По этой причине и вторичный ключ $user->Profile->internalKey все еще пустой.
ОК, с этим разобрались, едем дальше. А дальше у нас идет сохранение уже самого объекта $user с записью его в БД и присвоением ему id. Все, запись сделана. Вот теперь у нас у обоих объектов есть эти id-шники, но все еще нет значения $user->Profile->internalKey. Вот как раз для этого и вызывается метод $user->_saveRelatedObjects() еще раз. Теперь, когда будет сохраняться связанный объект $user->Profile, он сможет получить значение $user->id и присвоить его в качестве $user->Profile->internalKey и сохраниться.
Да, я согласен, что все это очень запутанно (а объясняю это еще запутанней), но логика во всем этом есть. И, собственно, именно по этой причине я вижу такое упорное использование MyIsam вместо innoDB. Почему? Да потому что на innoDB это просто не сможет полноценно работать. И вот как раз сейчас мы разберем имеющуюся проблему, а не сам принцип работы. Сразу скажу, что для полного понимания всего этого требуется хорошее понимание MySQL, а именно понимание транзакций, primary и foreign key и т.п.
Давайте настроим нашу базу данных еще правильней, а именно настроим первичные и вторичные ключи на уровне самой базы. Для этого выполним следующее:
2. В таблице modx_users поле id int(10)unsigned, а в modx_users_attributes поле internalKey int(10) (не unsigned). Из-за этого мы просто не сможем настроить вторичный ключ, ибо типы данных в колонках обеих таблиц обязаны полностью совпадать.
Если при сохранении вторичного ключа вы не получили никаких ошибок, то замечательно! Но есть несколько ошибок, которые вы можете получить. Самые распространенные из них:
1. Типы данных не совпадают.
2. Для вторичной записи не существует первичной (то есть, к примеру, у вас есть запись в modx_user_attributes с internalKey = 5, а записи в modx_users с id = 5 нету).
А теперь давайте посмотрим суть проблемы на примере. Для этого выполним в консоли следующий код:
<?php $user_data = array( "username" => "test_". rand(1,100000), ); $profile_data = array( "email" => "test@local.host", ); $user = $modx->newObject('modUser', $user_data); $user->Profile = $modx->newObject('modUserProfile', $profile_data); $user->save(); print '<pre>'; print_r($user->toArray()); print_r($user->Profile->toArray());
Сейчас мы никакой проблемы не увидели, все сохранилось без замечаний.
Array ( [id] => 59 [username] => test_65309 [password] => [cachepwd] => [class_key] => modUser [active] => 1 [remote_key] => [remote_data] => [hash_class] => hashing.modPBKDF2 [salt] => [primary_group] => 0 [session_stale] => [sudo] => ) Array ( [id] => 54 [internalKey] => 59 [fullname] => [email] => test@local.host [phone] => [mobilephone] => [blocked] => [blockeduntil] => 0 [blockedafter] => 0 [logincount] => 0 [lastlogin] => 0 [thislogin] => 0 [failedlogincount] => 0 [sessionid] => [dob] => 0 [gender] => 0 [address] => [country] => [city] => [state] => [zip] => [fax] => [photo] => [comment] => [website] => [extended] => )
А теперь немного изменим наш код:
<?php $user_data = array( "username" => "test_". rand(1,100000), ); $profile_data = array( "email" => "test@local.host", ); $user = $modx->newObject('modUser', $user_data); $user->Profile = $modx->newObject('modUserProfile', $profile_data); // Заранее установим id первичному объекту. Здесь следует указать свой какой-нибудь id, убедившись, что в БД он не занят. $user->id = 40; $user->save(); print '<pre>'; print_r($user->toArray()); print_r($user->Profile->toArray());
Что мы теперь получим при выполнении этого кода?
1. Сообщение об SQL-ошибке
Array ( [0] => 23000 [1] => 1452 [2] => Cannot add or update a child row: a foreign key constraint fails (`shopmodxbox_test2`.`modx_user_attributes`, CONSTRAINT `modx_user_attributes_ibfk_1` FOREIGN KEY (`internalKey`) REFERENCES `modx_users` (`id`)) )
2. Оба наши объекта все-таки сохранились и имеют корректные id и internalKey.
Почему так происходит? При сохранении вторичного объекта xPDO проверяет имеется ли значение первичного ключа, и только если он есть, тогда уже устанавливает его значение в качестве вторичного ключа и сохраняет этот объект. В нашем случае мы вручную указали первичный ключ id и вторичный объект сумел получить его значение и попытался записаться в базу данных, но так как фактически первичной записи там нет, мы и получаем SQL-ошибку о невозможности записать вторичную запись без первичного объекта. Но сохранение первичного объекта на этом не прерывается. После этого первичный объект $user успешно записывается в базу, а при повторной попытке сохранения связанного объекта $user->Profile уже нормально все сохраняется, так как первичная запись имеется.
Из всего этого вытекает два заключения.
1. При сохранении связанных объектов невозможно отследить ошибки сохранения вторичных объектов и как-то на них среагировать. То есть никогда нельзя с уверенностью сказать, по какой причине не был сохранен вторичный объект (то ли нет пока первичного объекта, и он сможет позже записаться при повторном вызове метода xPDOObject::_saveRelatedObjects(), то ли там какой-нибудь уникальный ключ сконфликтовал и запись в принципе не может быть записана, то ли там валидация на уровне мапы не прошла и т.д. и т.п.).
2. По этой причине невозможно использовать полноценно транзакции.
Возможный путь решения этой проблемы.
Мы видим решение этой проблемы в том, чтобы разграничить первый и второй вызов метода xPDOObject::_saveRelatedObjects() по типам связанных объектов, а именно первый вызов — для первичных объектов, а второй вызов — для вторичных. В таком случае точно не будет путаницы с ключами, и если объект по какой-то причине не сохранился, то это точно будет означать ошибку и можно будет выполнять прерывание процесса сохранения (в том числе и откат транзакций).
ссылка на оригинал статьи http://habrahabr.ru/post/265485/
Добавить комментарий