Продвинутые ассоциации Active Record

Введение

Вы уже немного знакомы с ассоциациями, особенно с основными их вариациями: has_one, has_many и belongs_to. До сих пор, вы наверняка использовали эти ассоциации, чтобы получать коллекции объектов вроде постов пользователя (user.posts). Помимо этого, существует множество других удобных вещей, которые Rails позволяет вам делать с помощью ассоциаций. Этот короткий раздел осветит некоторые из наиболее полезных методов, которые идут рука об руку с ассоциациями.

Пункты для размышления

Просмотрите вопросы и проверьте, знаете ли на них ответы. Проверьте себя снова после выполнения задания.

  • Как обычно Rails узнает какие таблицу и внешний ключ использовать, когда вы используете ассоциацию (например, User.first.posts)?
  • Когда вам может потребоваться указать опцию :class_name в ассоциации?
  • Что насчет опции :foreign_key?
  • Что насчет опции :source?
  • Что такое полиморфная ассоциация и когда вам следует ее использовать?
  • Назовите 2 способа использования ассоциации для создания нового объекта вместо простого вызова YourObject.new? Чем они полезны? Какой из них предпочтительнее?
  • Как автоматически уничтожить все объекты-посты пользователя, если этот пользователь был удален?
  • Как задать ассоциацию, в которой один объект связан с другим объектом того же класса (такой тип связи называется self join), вроде пользователя, подписанного на другого пользователя?

Базовая информация

Если вы до сих пор ощущаете неуверенность в основах работы с ассоциациями, сперва вернитесь назад и пробегитесь по разделу ассоциаций из урока по основам Active Record. Этот раздел служит для ознакомления с базовыми вещами, которые вам, возможно, еще не встречались.

Внешние ключи и наименования классов

Когда вы создаете ассоциацию, Rails полагается на 2 основных допущения - во-первых, имя класса модели, на которую указывает ваша ассоциация, основывается непосредственно на имени ассоциации, и, во-вторых, внешний ключ во всех отношениях belongs_to будет назван имявашейассоциации_id (например, user_id). Как только вы отходите от этих стандартов, вам потребуется указать Rails какой класс ему нужно искать и какой внешний ключ использовать.

Очень простым примером служит пользователь, способный создавать множество постов в блоге:

    # app/models/user.rb
    class User < ActiveRecord::Base
      has_many :posts
    end

    # app/models/post.rb
    class Post < ActiveRecord::Base
      belongs_to :user
    end

Таким образом, вы можете запросить все посты первого пользователя с помощью User.first.posts или автора первого поста вызовом Post.first.user. Rails знает, что нужно искать внешний ключ с именем user_id в таблице постов (Posts). Все идет замечательно, пока имя вашей ассоциации в точности совпадает с именами ваших моделей и таблиц.

Но что, если вы хотите иметь 2 типа пользователей, которым принадлежат посты - "автор" и "редактор"? В этом случае, вам потребуется два отдельных внешних ключа в таблице постов, предположим, один названный author_id и другой - editor_id. Как вы сообщите Rails, что каждый из этих внешних ключей в действительности указывает на пользователя (так, чтобы Rails знал - все записи надо искать в таблице пользователей (Users))? Просто укажите класс, на который должна указывать ваша ассоциация, с помощью удачно названной опции :class_name:

    # app/models/post.rb
    class Post < ActiveRecord::Base
      belongs_to :author, :class_name => "User"
      belongs_to :editor, :class_name => "User"
    end

В этом случае, Rails автоматически будет искать внешние ключи, именованные в соответствии с ассоциацией, к примеру author_id или editor_id, в таблице постов (Posts).

Если вы назвали ассоциации как-то отлично от имени внешнего ключа в вашей таблице, вам также потребуется указать это для Rails. Это должно быть очевидно, если смотреть на отношение со стороны пользователя. Пользователь будет иметь свои посты, для которых он является автором, и чужие посты, для которых он - редактор. Вдобавок вам понадобится переименовать ассоциацию со стороны пользователя, чтобы все было предельно понятно, например, отделив authored_posts от edited_posts.

Но теперь Rails не имеет ни малейшего представления, где и что нужно искать. По умолчанию, если вы пишете User.first.authored_posts, Rails будет искать в таблице authored_posts внешний ключ с именем user_id (ни того, ни другого у нас не существует). Чтобы ассоциация указывала на правильную таблицу, нам снова потребуется указать :class_name, и, чтобы использовался верный внешний ключ, нам нужно указать правильный :foreign_key. Для примера:

    # app/models/user.rb
    class User < ActiveRecord::Base
      has_many :authored_posts, :foreign_key => "author_id", :class_name => "Post"
      has_many :edited_posts, :foreign_key => "editor_id", :class_name => "Post"
    end

Основная суть проста - убедиться, что Rails ищет внешний ключ, имеющий соответствующее ассоциации имя, в таблице, также имеющей соответствующее ассоциации имя. Если что-то из этого не соответствует действительности из-за креативно названных ассоциации или внешнего ключа, вам потребуется это явно указать. Это довольно распространенная задача, чтобы сделать ваши ассоциации более осмысленными.

Источник данных

Теперь, когда стало ясно, что вам необходимо сообщать Rails, если у вас творчески названы ваши ассоциации и внешние ключи, требуется показать, что нужен еще один дополнительный шаг для работы с творчески названными ассоциациями типа has_many :through. Напомним, что ассоциации типа "имею-множество-через" (has-many-through) - это такие ассоциации, при которых вы создаете "сквозную таблицу" как промежуточное связующее звено между двумя моделями, имеющими отношение "многие-ко-многим".

Например, мы изменим пример выше так, что у поста может быть несколько авторов (но по-прежнему 1 редактор). Нам понадобится создать новую таблицу, которую мы назовем post_authorings. post_authorings соединяет эти две модели вместе и содержит столбцы authored_post_id и post_author_id. Наверняка, вы заметили к чему все движется - мы назвали наши внешние ключи более подробно и понятно, нежели просто post_id и user_id, но это потребует сообщить Rails об этих изменениях. Наши модели выглядят так:

    # app/models/post.rb
    class Post < ActiveRecord::Base
      has_many :post_authorings, :foreign_key => :authored_post_id
      has_many :authors, :through => :post_authorings, :source => :post_author
      belongs_to :editor, :class_name => "User"
    end

    # app/models/user.rb
    class User < ActiveRecord::Base
      has_many :post_authorings, :foreign_key => :post_author_id
      has_many :authored_posts, :through => :post_authorings
      has_many :edited_posts, :foreign_key => :editor_id, :class_name => "Post"
    end

    # app/models/post_authoring.rb
    class PostAuthoring < ActiveRecord::Base
      belongs_to :post_author, :class_name => "User"
      belongs_to :authored_post, :class_name => "Post"
    end

Самым примечательным моментом тут является то, что с ассоциациями типа "многие-ко-многим" Rails использует имя ассоциации в "сквозной" таблице для определения, по какому внешнему ключу и к какой таблице следует обращаться. Если она названа как-то нестандартно, используйте опцию :source для указания, какая ассоциация в действительности указывает на таблицу, с которой мы хотим связаться. Вы можете представить :source как аналог :class_name, но для определения нужной ассоциации в "сквозной" таблице.

Может оказаться полезным иллюстрация того, что же делает Rails. В примере выше, вы можете запросить Post.first.authors, тогда Rails "думает" примерно следующим образом:

  1. Вы только что вызвали ассоциацию "авторы" для модели постов
  2. Для получения "авторов", нам сперва нужно пройти сквозь ассоциацию post_authorings.
  3. Как только мы добрались до модели PostAuthoring, чтобы оказаться в author, мы используем ассоциацию :belongs_to, которая называется post_author. Мы знаем это благодаря опции :source. Если бы мы не использовали опцию :source в исходной ассоциации "имею-множество-через" (has-many-through), то искали бы вместо этого belongs_to :author.
  4. Теперь мы имеем всю необходимую информацию, чтобы построить наши SQL соединения и взять список авторов поста.

Это звучит слегка ненадежно, но эта логика повторяет предыдущую - если Rails не может понять, основываясь на своих допущениях, какие ассоциации, имена классов или внешних ключей следуют использовать, вам потребуется явно указать их, используя опции :source, :foreign_key или :class_name. Для полного понимания вам потребуется немного практики, но в итоге вы со всем разберетесь. Как правило, если что-то пошло не так, вы получите ошибку вида ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column.

Когда вы поймете, как все работает, по-прежнему будет полезным взглянуть на серверные логи Rails, чтобы увидеть какие соединения были использованы для построения SQL-запроса. Это замечательная возможность посмотреть, что же делают ваши ассоциации за кулисами (потому что в результате, все это сводится к выполнению правильного SQL-запроса).

Полиморфные ассоциации

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

Полиморфные ассоциации сперва могут привести в замешательство, да и применяются не слишком часто, но они отлично решают свой круг задач. Они емко описывают довольно простую концепцию - что, если у вас есть одна модель, которая может принадлежать нескольким различным типам моделей. Допустим, вы создаете сервис вроде Facebook, где пользователи могут комментировать любой из множества типов контента, который выкладывают другие пользователи (вроде текста, фотографий, картинок). Как вы обеспечите возможность комментирования всех этих типов объектов, использую одну лишь модель комментариев (Comment)?

В обычной ситуации, комментарий имел бы ассоциацию belongs_to с моделью поста (Post), изображения (Picture) или видео (Video) (или любой другой комментируемой сущностью). В вашей таблице комментариев (Comments) у вас был бы внешний ключ, названный как-то вроде post_id. Теперь, если мы хотим иметь возможность комментировать несколько типов контента, нам потребуется придумать каким образом работать с внешним ключом, ведь один и тот же внешний ключ может указывать на пост, изображение или видео, и мы не знаем, на что же он указывает... возникла неопределенность. Конечно, вы можете сделать отдельный столбец под каждый тип комментируемого контента, например, post_id, image_id, video_id, но это ужасно уродливо и негибко (представьте, что у вас было бы 100 типов комментируемого контента!). Мы должны использовать один столбец для внешнего ключа.

Мы решим эту задачу с помощью хранения не только внешнего ключа id, но также указания type(типа) модели, к которой он относится. Таким образом, когда бы вам ни понадобилось получить комментарий, указание типа контента, к которому он принадлежит, устраняет неопределенность при его запросе. Обратите внимание - Rails делает это за вас неявно, если знает, что вы работаете с полиморфной ассоциацией.

Нам нужно назвать наш внешний ключ как-то отлично от обычного случая, так как теперь неясно, на какую модель он указывает, и вы не можете использовать post_id или picture_id. По соглашению, для именования ассоциации следует использовать абстрактное понятие, которое описывает выполняемое вами действие. В данном случае мы комментируем контент, следовательно, можем назвать внешний ключ "commentable" (комментируемое). Как видите, able-соглашение вполне справедливо. Таким образом, миграция для этой модели может выглядеть так:

    class CreateComments < ActiveRecord::Migration
      def change
        create_table :comments do |t|
          t.string :title
          t.text :content
          t.integer :commentable_id
          t.string :commentable_type
          t.timestamps
        end
      end
    end

Также "commentable" будет использоваться для установки ассоциации. Вам понадобится указать в модели комментариев, что она является полиморфной, чтобы Rails знал о необходимости проверки столбца commentable_type, когда ассоциация будет использоваться. Это делается очень просто:

    # app/models/comments.rb
    class Comment < ActiveRecord::Base
      belongs_to :commentable, :polymorphic => true
    end

С другой стороны ассоциации, вы обращаетесь с комментарием как с любой другой ассоциацией (которая, порой, имеет нестандартное имя). Вам лишь потребуется указать ассоциацию для каждой модели, которая имеет множество комментариев (has_many). Единственной особенностью тут является то, что из-за использования имени "commentable", вам понадобится указать его как синоним, прямо как в случае с нестандартным именем ассоциации:

    # app/models/post.rb
    class Post < ActiveRecord::Base
      has_many :comments, :as => :commentable
    end

    # app/models/picture.rb
    class Picture < ActiveRecord::Base
      has_many :comments, :as => :commentable
    end

Остальную работу за вас сделает Rails. В любой момент, когда вы запрашиваете все комментарии изображения (Picture.first.comments), Rails вернет только те комментарии, что принадлежат этому изображению, не требуя от вас ничего взамен.

Самоприсоединение (Self Joins)

Довольно часто у вас имеются отношения между моделями одного типа, например, пользователи, которые могут подписываться на других пользователей. В этом случае вам нужно указать обе ассоциации в вашей модели пользователя, но назвать их по-разному. Также вам понадобится указать имя внешнего ключа foreign_key в ассоциации has_many:

    class Employee < ActiveRecord::Base
      has_many :subordinates, class_name: "Employee",
                              foreign_key: "manager_id"

      belongs_to :manager, class_name: "Employee"
    end

Полезные методы

Как было упомянуто во вступлении, ассоциации предоставляют вам некоторые изящные трюки, о которых вы могли не знать.

Создание ассоциированного объекта

Существует пара коротких способов для создания нового ассоциированного объекта. Первый из них - вызов методов #new или #create на ассоциации для автоматического заполнения внешнего ключа. Например, если пользователь (User) и посты (Posts) связаны отношением has_many, а пост принадлежит (belongs_to) пользователю:

# Длинный способ:
> user = User.first
> post = Post.create(:title => "sample", :user_id => user.id)

# Короткий способ:
> user = User.first
> user.posts.create(:title => "sample")

Другой изящный трюк - создание нового объекта, а затем использование оператора << для его добавления в ассоциацию. Это лишь один из примеров того, что ассоциации иногда действуют как массивы:

> user = User.create(:name => "foobar")
> post = Post.new(:title => "sample")
> user.posts << post

Это сохранит пост в базе данных с заполненным внешним ключом пользователя. Грубо говоря, это то же самое, что вызов следующего:

> post.user_id = user.id
> post.save

Если вам сильно этого хочется, вы можете заменить ассоциацию целиком новой коллекцией, присвоив ей новую коллекцию:

> user = User.first
> post1 = Post.find(1)
> post2 = Post.find(2)
> user.posts = [post1, post2]  # posts added to that user's collection

Уничтожение зависимых объектов

Если ваш пользователь создал набор постов, а затем решил удалить свой аккаунт, как вы удалите все связанные посты? Укажите опцию :dependent => :destroy, когда объявляете ассоциацию:

    # app/models/user.rb
    class User < ActiveRecord::Base
      has_many :posts, :dependent => :destroy
    end

Это наиболее распространенная из всех доступных опций для :dependent. Она запустит метод destroy для всех объектов, которые принадлежали пользователю, когда пользователь будет удален.

Задание

  1. Прочитайте руководство Связи Active Record. Начните с беглого осмотра разделов с 1 по 2.7 (они должны уже быть вам знакомы).
  2. Прочтите разделы с 2.8 до конца главы 3.
  3. Пробегитесь по главе 4. Она содержит все методы, которые вы получаете при использовании различных ассоциаций. Вам определенно не стоит заучивать весь список, но взгляните на них. В итоге вы будете использовать большинство из этого списка.

Заключение

В этом уроке мы изучили одни из самых продвинутых техник работы с ассоциациями. Ассоциации встречаются в Rails повсюду и невероятно полезны благодаря множеству новых методов, которые они вам предоставляют. Немного поразмыслив и поняв на что Rails полагается, когда вы создаете ассоциации, вы с легкостью сможете изменять их как вам угодно. Однако, практика оттачивает мастерство, так что продолжайте создавать проекты с ассоциациями, и в конечном итоге вы их познаете.

##Дополнительные ресурсы

Этот раздел содержит полезные ссылки на дополнительные материалы. Они не обязательны, так что расценивайте их как нечто полезное, если вы хотите поглубже погрузиться в тему

Поделиться уроком: