owner=>local и owner=>foreign в xPDO-объектах и связанные xPDO-объекты

Материал для экспертов.

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

Для начала практический пример, чтобы понимать сразу о чем речь и куда мы будем копать. У Ильи Уткина есть хорошая статья, описывающая принципы методов 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−шника,покаегоданныенебылизаписанывбазуданных,авначалеметодаtopic->save()). У топика еще нет id-шника, пока его данные не были записаны в базу данных, а в начале метода this->save() они еще не записаны, там еще только в первый раз выполняется метод this−>saveRelatedObjects(),которыйдолженсохранитьобъектсвязкиБлог−Топик.Итак,этотобъектсвязкиБлог−Топиксохраняется,приэтомработаеттотжесамыймеханизмсохранениясвязанныхобъектов,которыйпреждесохранениясамогообъектасвязкиБлог−Топиксохранитсвязанныйснимобъектблога.Блогсохранится(внемвданныймоментнетсвязанныхобъектов,поэтомуонсразузаписываетсявбазуданных),иублогаестьсвойid−шник.Далее,походувыполненияфункциисохранениясвязкиБлог−Топик,этотобъектполучаетзначениепервичногоключаобъектаблогаиприсваиваетсебевкачествевторичногоключа(внашемслучаеэто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=topic = modx->getObject('SocietyTopic', $id), когда выполняется проверка прав на этот объект, уже тогда подгружаются объекты связок Блог-Топик этого топика и их объекты блогов. И вот когда я в обновлении топика менял значения blogid в связках Блог-Топик, то при сохранении эти значения перезаписывались id-шниками уже имеющихся связанных объектов блогов, ибо owner=>"foreign", и есть в наличии связанный объект. Вот в этом и крылась разгадка восстановления значений в объекте-связке при сохранении :)

А как же тогда в таких случаях менять значения вторичных ключей? А здесь просто надо перетирать связанный объект. В нашем случае код будет выглядеть так:

foreach($TopicBlogs as & $TopicBlog){

    $TopicBlog->Blog = $this->modx->getObject('SocietyBlog', $blogid);

}

Вот такой получился квест... Один из самых сложных за мою практику.

Comments

Так проблема в том, что не сгенерировать из мапы схему, учитывающую зависимости, а вот из схемы класс и мапу можно с учетом зависимостей сгенерить.

Понимаешь, что в схему, что в мап-файлы, в любом случае изначально надо прописывать куда-то эти связи. Даже если в XML, все равно вручную надо прописать. А потом из этого XML-а сгенерить мап-файлы. А нафига, если можно просто сразу в мап-файл прописать? Мне удобней и привычней сразу с мап-файлами работать, ибо вносить в них изменения - сразу видеть эффект на сайте, а из XML-а генерить мапы надо.

Понятно что через мапу тоже все работает но xml типа наглядней и кошерно, вся модель MODX на xml, не понятно тогда накой вообще xml нужен может это потребуется во время миграции на другие бызы (как предположение).

Каждому свое, и свое не каждому. Для меня наоборот XML не наглядней. Мне проще работать с отдельными классами, чем ковырять все в одном файле. В общем, это не суть для обсуждения. Здесь можно и так, и так. Я не буду генерить XML, просто потому что тебе не нравится мой приоритетный способ, потому что XML - это твой приоритетный способ.

Ну и что? Все равно эти связи вручную туда прописывать. Короче, для меня это не аргумент.

Николай, эта тема изменяет полностью представление о связях XPDO объектов, скорее многие сталкивались с некорректной работой связей через API modX типа addOne addMany, личной я почемуто располагал мнением что owner это владелец ссылки то-есть не всегда владелец первичного ключа. Полезно

В случае если связь ссылается от первичного ключа к первичному ключу другого класса, в таком случае кого считать owner`ом

получился случай, когда 2 стороны с первичником (обе стороны связаны composite связью), сделал ownerom тот класс который считаю абстрактно главнее чем другой, нареканий нет, работает пока без багов

Пока... Технически сделано верно, но не дальновидно, особенно если в таблице auto_increment. Ну да ладно, делайте как вам больше нравится.