Ruby on Rails 3: Ассоциации между моделями ч. 1

апреля 5, 2012  |  Published in Ruby on Rails, Ruby on Rails 3, Базы данных  |  3 Comments

ruby on rails tutorial Ассоциации между моделями ч. 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 если у текущей записи имеются ассоциируемые записи.

В следующей части статьи мы рассмотрим ассоциации более подробно.

Tags: , , ,

Responses

  1. abasin says:

    января 8, 2013 at 11:07 (#)

    Владимир, а какие ассоциации лучше использовать для тэгов HABTM или HMT? Или в каких случаях оправданно использования HABTM, а в каких HMT? Не совсем понятна функция зависимости nullify, то есть если User has_many :articles, dependent: :nullify, при удалении пользователя обнулится user_id у articles, и статья останется, но не будет привязана к пользователю?

  2. admin says:

    января 13, 2013 at 17:08 (#)

    nullify стратегия обнуляет foreign_keys. Это дефолтная стратегия для ассоциаций кроме has_many, в has_many дефолтная стратегия — delete_all.

    HMT более гибок, но менее производителен. HMT используют если есть полиморфные ассоциации, сложные джойны (n-way joins), если есть необходимость в промежуточной модели. Многие используют HMT всегда.

  3. maxim says:

    августа 5, 2013 at 21:10 (#)

    Подскажите, создал генератором модель, затем внес корректироки, как теперь создать миграцию ?

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.