Fi1osof 05 октября 2014 1 14
Материал для экспертов.

Сразу говорю, что материал довольно объемный и сумбурно написанный, так как очень много взаимосвязей здесь и сложностей, каждая из которых тянет на отдельные топики. Про какие-то из них я писал (и буду на них ссылаться), а какие-то являются только недавно изученными, но буду описывать их кратко. Практической пользы от этого материала скорее всего маловато, ибо мало кто это будет применять, но если кто-то все-таки столкнется, скорее всего это поможет сэкономить очень много времени.

Для начала практический пример, чтобы понимать сразу о чем речь и куда мы будем копать. У Ильи Уткина есть хорошая статья, описывающая принципы методов xPDOObject::getMany() и xPDOObject::getOne(). Методы эти держатся на связях xPDO-объектов, прописанных в их мап-файлах. К примеру, есть класс SocietyTopic (расширенный modResource). Есть класс SocietyBlogTopic (для связи Блог-Топик).
Все примеры здесь и далее из реальной практики на основе сайта MODX-Клуба.
Итак, у топика может быть несколько связей Блог-Топик, то есть топик может находиться в нескольких блогах одновременно. В мап-файле топика прописаны эти связи:
  'composites' => 
array (
// ....
'TopicBlogs' =>
array (
'class' => 'SocietyBlogTopic',
'local' => 'id',
'foreign' => 'topicid',
'cardinality' => 'many',
'owner' => 'local',
),
),

Здесь TopicBlogs - это алиас (псевдоним) связи, class - имя класса связанного объекта, local - имя колонки первичного ключа в текущем объекте, foreign - имя колонки вторичного ключа в связанном объекте, cardinality - тип связи (one - один-к-одному, many - один-ко-многим), owner - "владелец" первичного ключа. Вот "владелец" - это как раз и будет наш главный объект текущих исследований, и поговорим о нем плотно чуть дальше.

Итак, связи у нас прописаны. Что же это нам дает? Посмотрим вот на этот код:
$topic = $modx->getObject('SocietyTopic', $id);
foreach($topic->getMany('TopicBlogs') as $topicBlog){
print_r($topicBlog->toArray());
}

В данном случае мы получили объект нужного нам топика, после чего получили связанные с ним объекты по алиасу TopicBlogs. xPDO выполнил в этом случае xPDO::getCollection(), в котором на основе данных из мап-файла подставил имя класса и условие поиска по первичному-вторичному ключу. На самом деле очень удобно.

Этот код можно написать и более лаконично:
$topic = $modx->getObject('SocietyTopic', $id);
foreach($topic->TopicBlogs as $topicBlog){
print_r($topicBlog->toArray());
}

Про данную магию я писал здесь. Советую тот топик изучить очень внимательно, так как от понимания его очень сильно зависит понимание дальнейшего материала в этом топике.

Итак, двинем дальше к нашей цели. Для этого рассмотрим вот такой код:
$topic = $modx->newObject('SocietyTopic', $topicdata);
$blog = $modx->newObject('SocietyBlog', $blogdata);
$topic_blog = $modx->newObject('SocietyBlogTopic');
$topic_blog->Blog = $blog;
$topic->TopicBlogs = $topic_blog;
$topic->save();

Что здесь происходит?
1. Создаем новый объект топика.
2. Создаем новый объект блога.
3. Создаем новый (пока еще пустой) объект связки Блог-Топик.
4. Добавляем в объект-связку объект блога, как связанный объект.
5. Добавляем этот объект-связку к объекту топика, опять-таки как связанный объект.
6. Сохраняем объект топика.

Обратите внимание, что сохраняем мы только один объект - топик. А остальные объекты? А остальные объекты сохраняются как связанные с ним автоматически. Давайте чуть подробней на этом остановимся, и за одно выясним главные преимущества этого метода.

Смотрите, когда мы только еще создаем новые объекты, нам не известны их id-шники (так как они еще не сохранены в базу данных). Конечно, мы могли бы выполнить запрос к БД, чтобы получить значения максимальных id-шников из этих таблиц, но это все равно не круто. Итак, вот у нас есть объект $topic_blog, в нем, пока еще объекты не сохранены в БД и нам не известны их id-шники, вторичные ключи не имеют значений, то есть topicid = null и blogid = null. По логике мы должны здесь сначала сохранить объект топика, потом сохранить объект блога, затем их айдишники присвоить объекту связки Блог-Топик, после чего уже и сохранить объект связки Блог-топик. Так-то оно так, но вот вопрос в том, в какой момент сохранить этот объект блога? Если у нас вот такой вот локальный код, то проблемы в этом нет, а если это у нас, к примеру, расширяемый create-процессор? То есть мы там прописали сохранение блога, сохранили, а этот процессор расширили, добавили дополнительные проверки и решили прервать выполнение процессора по какой-то причине. А объект блога уже создан... А связки его с топиком нет... Вот как раз механизм связанных объектов эту проблему и решает. То есть только тогда, когда основной объект отправляется на сохранение, тогда уже механизм xPDO обеспечивает автоматическое сохранение всех связанных в нем объектов. Здесь я писал, что за это отвечает метод xPDOObject::_saveRelatedObjects(), вызываемый внутри метода xPDOObject::save(), при чем дважды. Для чего дважды? Давайте пошагово: Сохраняем объект топика (то есть вызываем $topic->save()). У топика еще нет id-шника, пока его данные не были записаны в базу данных, а в начале метода $this->save() они еще не записаны, там еще только в первый раз выполняется метод $this->_saveRelatedObjects(), который должен сохранить объект связки Блог-Топик. Итак, этот объект связки Блог-Топик сохраняется, при этом работает тот же самый механизм сохранения связанных объектов, который прежде сохранения самого объекта связки Блог-Топик сохранит связанный с ним объект блога. Блог сохранится (в нем в данный момент нет связанных объектов, поэтому он сразу записывается в базу данных), и у блога есть свой id-шник. Далее, по ходу выполнения функции сохранения связки Блог-Топик, этот объект получает значение первичного ключа объекта блога и присваивает себе в качестве вторичного ключа (в нашем случае это $this->blogid = $blog->id). Таким образом у объекта связки Блог-Топик уже есть id-шник Блога. Но у него пока еще нет значения id топика, ведь топик не является его связанным объектом в текущий момент, а сам топик еще не сохранен. В текущий момент объект связки Блог-Топик записывается в БД с ключами blogid => "id блога", topicid => 0 (так как в колонке не разрешены null-значения, а реального значения еще нет). Идем дальше по процессу сохранения объекта топика. Его связанный объект Блог-Топик уже сохранен, и в данный момент выполняется сохранение в базу данных самого объекта Топика. Как только он сохранился, у него есть свой ID-шник. И вот в этот момент выполняется повторное сохранение связанных объектов (то есть выполнение метода $this->_saveRelatedObjects()). И вот теперь, когда у топика есть свой id, топик присваивает его объекту связки Блог-Топик в качестве вторичного ключа topicid и сохраняет этот связанный объект (выполняет update уже имеющейся в базе данных записи). И вот теперь у нас в базе данных все три объекта со всеми первичными и вторичными ключами :)

Но все это я объяснил в простой разговорной форме, и у наблюдательного читателя (как это часто пишут в книгах :)) должен возникнуть вопрос "а на основании чего вообще объект связки Блог-Топик решает, что ему надо получить значение id блога, чтобы присвоить его себе в качестве blogid, а объект топика решает, что свой id-шник надо принудительно присвоить в качестве topicid объекту Блог-Топик?". А вот за это как раз и отвечает мап-файл и в частности значение переменной owner в описании связей с другими объектами. И с этим надо очень серьезно разобраться, так как неверное указание значения этой переменной может повлиять на то, что значения вторичных ключей вообще не будут установлены, и связанные с этим коварные последствия мы как раз и рассмотрим чуть ниже. Вообще, по задумке разработчиков MODX-а, можно было бы в большинстве случаев вообще не указывать значение переменной owner, но с этим есть бага, которая выставляет прямопротивоположные значения)). Пуллреквест я уже отправил. Итак, правило довольно простое: "Если это главный объект, содержащий первичный ключ, и значение этого ключа должно быть присвоено вторичному объекту, то owner=>local. А если наоборот, объект при сохранении должен принять id-шник связанного объекта и присвоить себе в качестве вторичного ключа, то тогда owner=>foreign". Если это правило не соблюсти, то при сохранении связанного объекта вторичный ключ может вообще не присвоиться.

А вот сейчас перейдем к самой главной заморочке, из-за которой мне и пришлось так подробно изучить весь этот хоть и удобный, но крайне сложный механизм, ибо у меня при сохранении объектов бушевал Ктулху)))

Итак, ситуация, которая на несколько часов меня заморочила: есть у нас уже готовый топик, отнесенный к некоему блогу. И вот при редактировании этого топика (здесь, на странице редактирования), я хочу ему сменить блог, в который его занесли. Все ОК, обновление топика проходит успешно и без ошибок, но вот только связка Блог-Топик не меняется. При чем самое интересное то, что раньше работало, а тут "вдруг" перестало работать.
Раньше код в update-процессоре был примерно следующий (когда работать перестало):
foreach($TopicBlogs as & $TopicBlog){
$TopicBlog->blogid = $blogid;
}

И вот объект топика сохранялся нормально, а связка Блог-Топик нет. При чем уже доходило до смешного. Делал вот так:
foreach($TopicBlogs as & $TopicBlog){
$TopicBlog->blogid = $blogid;
print_r($TopicBlog->toArray()); // Здесь выводятся корректные данные
$TopicBlog->save();
print_r($TopicBlog->toArray()); // А вот здесь уже опять данные с id исходного блога
exit;
}

То есть перед самым сохранением данные нужные, а сразу после сохранения объекта связки ключ блога в ней восстанавливается. Я уже думал, что где-нибудь выше по коду текущего или расширяемого процессора я добавил связи и что они и перебиваю все, и сделал так:
$topic = $modx->getObject('SocietyTopic', $id);
foreach($topic->TopicBlogs as & $TopicBlog){
$TopicBlog->blogid = $blogid;
print_r($TopicBlog->toArray()); // Здесь выводятся корректные данные
$TopicBlog->save();
print_r($TopicBlog->toArray()); // А вот здесь уже опять данные с id исходного блога
exit;
}

Вот здесь связка TopicBlogs вообще чистая, без лишних связанных объектов (я никаких объектов к ней не добавлял и не пытался их получить), но эффект тот же самый... Как же так?

Но как оказалось, я только думал, что эта связка чистая. А на самом деле... Совсем недавно я написал замечательный топик про проверку доступов к xPDO-объектам, и там демонстрировал расширение этих классов и продемонстрировал свой класс топика, в котором прописана кастомная проверка прав к объекту. Нет, ошибки в том топике нет, и проверки все прописаны корректно, но как раз в том коде и крылась разгадка всей этой заморочки, а именно вот в этих строчках:
foreach((array)$this->TopicBlogs as $topicblog){
if(
$blog = $topicblog->Blog
AND $blog->checkPolicy($criteria, $targets, $user)
){
$hasBlogAccess = true;
break;
}
}

То есть еще в момент получения топика $topic = $modx->getObject('SocietyTopic', $id), когда выполняется проверка прав на этот объект, уже тогда подгружаются объекты связок Блог-Топик этого топика и их объекты блогов. И вот когда я в обновлении топика менял значения blogid в связках Блог-Топик, то при сохранении эти значения перезаписывались id-шниками уже имеющихся связанных объектов блогов, ибо owner=>"foreign", и есть в наличии связанный объект. Вот в этом и крылась разгадка восстановления значений в объекте-связке при сохранении :)

А как же тогда в таких случаях менять значения вторичных ключей? А здесь просто надо перетирать связанный объект. В нашем случае код будет выглядеть так:
foreach($TopicBlogs as & $TopicBlog){
$TopicBlog->Blog = $this->modx->getObject('SocietyBlog', $blogid);
}


Вот такой получился квест... Один из самых сложных за мою практику.
14 комментариев
AlexBaks1
AlexBaks 05 октября 2014г в 18:46 #
а как на xml выглядит схема?
Fi1osof1
Fi1osof 05 октября 2014г в 18:48 #
А Бог ее знает. Сгенерируй, посмотри. Мне лень, я этим не занимаюсь.
AlexBaks1
AlexBaks 05 октября 2014г в 18:55 #
так проблема в том что не сгенерировать из мапы схему, учитывающую зависимости, а вот из схемы класс и мапу можно с учетом зависимостей сгенерить.
Fi1osof1
Fi1osof 05 октября 2014г в 18:59 #
Понимаешь, что в схему, что в мап-файлы, в любом случае изначально надо прописывать куда-то эти связи. Даже если в XML, все равно вручную надо прописать. А потом из этого XML-а сгенерить мап-файлы. А нафига, если можно просто сразу в мап-файл прописать? Мне удобней и привычней сразу с мап-файлами работать, ибо вносить в них изменения - сразу видеть эффект на сайте, а из XML-а генерить мапы надо.
AlexBaks1
AlexBaks 05 октября 2014г в 19:04 #
Понятно что через мапу тоже все работает но xml типа наглядней и кошерно, вся модель MODX на xml, не понятно тогда накой вообще xml нужен может это потребуется во время миграции на другие бызы (как предположение).
Fi1osof1
Fi1osof 05 октября 2014г в 19:10 #
Каждому свое, и свое не каждому. Для меня наоборот XML не наглядней. Мне проще работать с отдельными классами, чем ковырять все в одном файле. В общем, это не суть для обсуждения. Здесь можно и так, и так. Я не буду генерить XML, просто потому что тебе не нравится мой приоритетный способ, потому что XML - это твой приоритетный способ.
AlexBaks1
AlexBaks 05 октября 2014г в 23:45 #
http://joxi.ru/ep4xVP3JTJAxXSC7_7I
Fi1osof1
Fi1osof 06 октября 2014г в 10:42 #
-1
Ну и что? Все равно эти связи вручную туда прописывать. Короче, для меня это не аргумент.
Fi1osof1
Fi1osof 05 октября 2014г в 19:05 #
Вот связи из мап-файлов.
K
Kutuz27 13 ноября 2014г в 13:35 #
Николай, эта тема изменяет полностью представление о связях XPDO объектов, скорее многие сталкивались с некорректной работой связей через API modX типа addOne addMany,
лично я почемуто располагал мнением что owner это владелец ссылки то-есть не всегда владелец первичного ключа.
Полезно
K
Kutuz27 13 ноября 2014г в 13:47 #
В случае если связь ссылаеться от первичного ключа к первичному ключу другого класса, в таком случае кого считать owner`ом
Fi1osof1
Fi1osof 13 ноября 2014г в 18:31 #
Правильный ответ: не должен первичный ключ ссылаться на первичный ключ.
K
Kutuz27 14 ноября 2014г в 06:12 #
получился случай, когда 2 стороны с первичником (обе стороны связаны composite связью),сделал ownerom тот класс который считаю абстрактно главнее чем другой, нареканий нет, работает пока без багов
Fi1osof1
Fi1osof 14 ноября 2014г в 10:27 #
Пока...
Технически сделано верно, но не дальновидно, особенно если в таблице auto_increment.
Ну да ладно, делайте как вам больше нравится.
Авторизуйтесь или зарегистрируйтесь (можно через соцсети ), чтобы оставлять комментарии.