Ruby on Rails 3: Ассоциации между моделями ч. 1
апреля 5, 2012 | Published in Ruby on Rails, Ruby on Rails 3, Базы данных | 3 Comments
Ассоциации между моделями ч. 2. Экземпляры моделей представляют собой сущности реального мира, а ассоциации между ними являются ни чем иным как отношениями этих сущностей между собой.
ActiveRecord предоставляет несколько возможных ассоциаций (типов отношений) между моделями и все они представлены ниже.
belongs_to — принадлежность другой модели. Пример:
class Post < ActiveRecord::Base belongs_to :category end
В примере выше мы объявили о том, что Post принадлежит Category. В базе данных такая ассоциация хранится как внешний ключ — category_id в таблице posts. То есть каждая запись соответствующая модели Post хранит id записи модели Category.
Ассоциации всегда двухсторонни и потому в коде модели Category также должна быть определена некая ассоциация с Post, например has_many, о которой мы поговорим ниже. has_many (один ко многим (one-to-many)) — владение другими моделями. Пример:
class Category < ActiveRecord::Base has_many :posts end
Только что мы объявили, что с категорией ассоциируется несколько постов, что вместе с объявлением принадлежности поста к категории дает нам ассоциацию.
Давайте теперь запустим консоль Rails и посмотрим как работают belongs_to и has_many ассоциация.
$ rails c
category = Category.create name: "Rails" #(0.1ms) begin transaction #SQL (119.3ms) INSERT INTO "categories" ("created_at", "name", "updated_at") VALUES (?, ?, ?) [["created_at", Thu, 16 Feb 2012 18:42:21 UTC +00:00], ["name", "Rails"], ["updated_at", Thu, 16 Feb 2012 18:42:21 UTC +00:00]] #(226.2ms) commit transaction #=> #<Category id: 1, name: "Rails", created_at: "2012-02-16 18:42:21", updated_at: "2012-02-16 18:42:21">
Получаем список постов принадлежащих категории category:
category.posts #Post Load (0.9ms) SELECT "posts".* FROM "posts" WHERE "posts"."category_id" = 1 #=> []
Как видите нам был возвращен пустой массив, а это значит, что в таблице posts не найдено постов принадлежащих данной категории. Давайте создадим такие посты:
category.posts.create title: "Rails tutorial", content: "Ololo" #(0.1ms) begin transaction #SQL (42.0ms) INSERT INTO "posts" ("category_id", "content", "created_at", "title", "updated_at") VALUES (?, ?, ?, ?, ?) [["category_id", 1], ["content", "Ololo"], ["created_at", Thu, 16 Feb 2012 18:45:14 UTC +00:00], ["title", "Rais tutorial"], ["updated_at", Thu, 16 Feb 2012 18:45:14 UTC +00:00]] #(153.5ms) commit transaction #=> #<Post id: 1, title: "Rais tutorial", content: "Ololo", category_id: 1, created_at: "2012-02-16 18:45:14", updated_at: "2012-02-16 18:45:14">
Еще раз проверяем посты категории:
category.posts #=> [#<Post id: 1, title: "Rais tutorial", content: "Ololo", category_id: 1, created_at: "2012-02-16 18:45:14", updated_at: "2012-02-16 18:45:14">]
Часто бывает полезно получить идентификаторы всех постов в категории, сделать это можно так:
category.post_ids #=> [1]
проверяем обратную ассоциацию поста с категорией:
post = category.posts[0] #=> #<Post id: 1, title: "Rais tutorial", content: "Ololo", category_id: 1, created_at: "2012-02-16 18:45:14", updated_at: "2012-02-16 18:45:14"> post.category #Category Load (0.7ms) SELECT "categories".* FROM "categories" WHERE "categories"."id" = 1 LIMIT 1 #=> #<Category id: 1, name: "Rails", created_at: "2012-02-16 18:42:21", updated_at: "2012-02-16 18:42:21">
has_one (ассоциация один к одному (one-to-one)) — данная ассоциация призвана создавать связи только между двумя экземплярами моделей. Примеров такой ассоциации так же много, как и с has_many, ну например Human has_one :heart или User has_one :account и т.д. Мы не будем рассматривать пример этой ассоциации поскольку она очень проста и похожа на уже известную has_many.
has_and_belongs_to_many (ассоциация многие ко многим (many-to-many)) — данная ассоциация описывает такое отношение между моделями, когда каждая из ассоциируемых моделей и принадлежит множеству других, и обладает их множеством. Примером такой ассоциации может служить, например облако тегов на сайте, где каждая статья может иметь множество тегов, а каждый тег может иметь множество статей. Для реализации этой ассоциации вам потребуется создать промежуточную таблицу название которой должно соответствовать названиям ассоциируемых моделей во множественном числе идущих в алфавитном порядке, например для Post и Tag это posts_tags, а если мы захотим сделать такую ассоциацию между статьями и постами, то таблицу следует назвать categories_posts. Давайте создадим миграцию, которая создаст таблицу для has_and_belongs_to_many (HABTM) ассоциации:
$ rails g migration CreatePostsTags
#db/migrate/..._create_posts_tags.rb class CreatePostsTags < ActiveRecord::Migration def change create_table :posts_tags do |t| t.integer :post_id t.integer :tag_id end end end
Теперь опишем ассоциацию в моделях Post и Tag:
class Post < ActiveRecord::Base belongs_to :category has_and_belongs_to_many :tags end class Tags < ActiveRecord::Base has_and_belongs_to_many :posts end
А сейчас откроем волшебную консоль Rails и посмотрим как все это работает:
$ rails c
Создаем два тега и еще один пост:
post2 = Post.create title: "Welcome!", content: "Welcome toRubyDev!" tag = Tag.create name:"Ruby" #(0.1ms) begin transaction #SQL (2.0ms) INSERT INTO "tags" ("name") VALUES (?) [["name", "Ruby"]] #(117.1ms) commit transaction #=> #<Tag id: 1, name: "Ruby"> tag2 = Tag.create name:"Rails" #(0.1ms) begin transaction #SQL (0.3ms) INSERT INTO "tags" ("name") VALUES (?) [["name", "Rails"]] #(105.8ms) commit transaction #=> #<Tag id: 2, name: "Rails">
Присваиваем пост сразу двум тегам:
post.tags = [tag, tag2] #Tag Load (0.2ms) SELECT "tags".* FROM "tags" INNER JOIN "posts_tags" ON "tags"."id" = "posts_tags"."tag_id" WHERE "posts_tags"."post_id" = 1 #(0.1ms) begin transaction #(0.4ms) INSERT INTO "posts_tags" ("post_id", "tag_id") VALUES (1, 1) #(0.1ms) INSERT INTO "posts_tags" ("post_id", "tag_id") VALUES (1, 2) #(117.7ms) commit transaction tag.posts #Post Load (0.4ms) SELECT "posts".* FROM "posts" INNER JOIN "posts_tags" ON "posts"."id" = "posts_tags"."post_id" WHERE "posts_tags"."tag_id" = 1 #=> [#<Post id: 1, title: "Rais tutorial", content: "Ololo", category_id: 1, created_at: "2012-02-16 19:58:11", updated_at: "2012-02-16 19:58:11">]
Создадим пост, который сразу ассоциируется с тегом:
tag2.posts.create title: "Welcome!", content: "Welcome to RubyDev!" #(0.1ms) begin transaction #SQL (0.5ms) INSERT INTO "posts" ("category_id", "content", "created_at", "title", "updated_at") VALUES (?, ?, ?, ?, ?) [["category_id", nil], ["content", "Welcome to RubyDev!"], ["created_at", Thu, 16 Feb 2012 20:07:53 UTC +00:00], ["title", "Welcome!"], ["updated_at", Thu, 16 Feb 2012 20:07:53 UTC +00:00]] #(0.2ms) INSERT INTO "posts_tags" ("tag_id", "post_id") VALUES (2, 3) #(120.1ms) commit transaction #=> #<Post id: 3, title: "Welcome!", content: "Welcome to RubyDev!", category_id: nil, created_at: "2012-02-16 20:07:53", updated_at: "2012-02-16 20:07:53">
С HABTM ассоциацией разобрались, давайте перейдем к следующей.
has_many :through => … (HMT) (ассоциация многие ко многим (many-to-many)) — данная ассоциация — разновидность ассоциации has_many, которая выделяется параметром :through. Этот параметр позволяет выполнять ассоциирование через (through) т.н. «промежуточную» модель, таблица ассоциированная с которой содержит id той модели с которой мы хотим ассоциировать текущую в которой используется HMT ассоциация.
Чтобы понять что такое HMT ассоциация мы перепишем нашу HABTM ассоциацию для Post и Tag. Для этого нам необходимо сначала создать промежуточную модель, которую назовем PostTag. Поскольку у нас уже есть таблица posts_tags, то мы будем использовать ее, но имя модели и таблицы не соответствуют друг другу, иначе таблица должна называться post_tags. Для исправления этой проблемы мы можем переименовать таблицу используя миграции или явно прописать в модели PostTag имя таблицы. Лично я выбираю второй вариант поскольку меня устраивают имена и ничего переименовывать я не хочу.
#../app/models/post_tag.rb class PostTag < ActiveRecord::Base #указываем имя таблицы self.table_name = "posts_tags" belongs_to :post belongs_to :tag end class Post < ActiveRecord::Base belongs_to :category has_many :post_tags has_many :tags, through: :post_tags end class Tag < ActiveRecord::Base has_many :post_tags has_many :posts, through: :post_tags end
Проверяем как все работает:
tag = Tag.first #Tag Load (0.2ms) SELECT "tags".* FROM "tags" LIMIT 1 #=> #<Tag id: 1, name: "Ruby"> tag.posts #Post Load (0.3ms) SELECT "posts".* FROM "posts" INNER JOIN "posts_tags" ON "posts"."id" = "posts_tags"."post_id" WHERE "posts_tags"."tag_id" = 1 #=> [#<Post id: 1, title: "Rais tutorial", content: "Ololo", category_id: 1, created_at: "2012-02-16 19:58:11", updated_at: "2012-02-16 19:58:11">]
Ура! Все работает!
Зависимости
Зависимости — это ничто иное, как объявление коллбеков. Что такое коллбеки мы поговорим в отдельной статье, а сейчас кратко поясню смысл зависимостей. Зависимости определяют то, что должно произойти с одной моделью, когда что-то происходит с другой. Самый распространенный пример — это когда мы удаляем статью, а вместе с нею необходимо удалить все комментарии к ней.
Зависимость задается опцией :dependent, например так:
class Post has_many :comments, dependent: :destroy end class Comment belongs_to :post end
Теперь удалив запись Post вы удалите и все комментарии принадлежавшие удаленному посту.
Помимо значения :destroy, опция :dependent может принимать следующие значения:
:destroy — удалить все принадлежащие записи через метод destroy, то есть с выполнением коллбеков.
:delete — удалить напрямую, без вызова метода destroy, то есть без выполнения коллбеков.
:nullify — обнулить у ассоциируемых записей колонку с foreign_key.
:restrict — при удалении будет вызвана ошибка ActiveRecord::DeleteRestrictionError если у текущей записи имеются ассоциируемые записи.
В следующей части статьи мы рассмотрим ассоциации более подробно.
января 8, 2013 at 11:07 (#)
Владимир, а какие ассоциации лучше использовать для тэгов HABTM или HMT? Или в каких случаях оправданно использования HABTM, а в каких HMT? Не совсем понятна функция зависимости nullify, то есть если User has_many :articles, dependent: :nullify, при удалении пользователя обнулится user_id у articles, и статья останется, но не будет привязана к пользователю?
января 13, 2013 at 17:08 (#)
nullify стратегия обнуляет foreign_keys. Это дефолтная стратегия для ассоциаций кроме has_many, в has_many дефолтная стратегия — delete_all.
HMT более гибок, но менее производителен. HMT используют если есть полиморфные ассоциации, сложные джойны (n-way joins), если есть необходимость в промежуточной модели. Многие используют HMT всегда.
августа 5, 2013 at 21:10 (#)
Подскажите, создал генератором модель, затем внес корректироки, как теперь создать миграцию ?