Веб Зображення Новини Групи Блоги Перекладач Gmail Ще »
Групи, які ви переглядали нещодавно | Довідка | Увійти
Головна сторінка Груп Google
Інформація про групу
Русскоязычная документация к ActiveRecord::Associations    

Ассоациации — набор макроподобных методов, связывающих объекты вместе через внешние ключи (FK). Они выражают
отношения типа "У проекта есть один проджект менеджер" или "Проект относится к портфолио". Каждый макрос
динамически (за счет метапрограммных возможностей Ruby) добавляет классу определенное число методов, которые
имеют свое особенное поведение в зависимости от типа связи и дополнительных переданных опций.
Эти макросы во многом схожи с attr_* в Ruby.

Пример:

  class Project < ActiveRecord::Base
    belongs_to              :portfolio
    has_one                 :project_manager
    has_many                :milestones
    has_and_belongs_to_many :categories
  end

Класс Project в этом случае динамически получает следующие методы (на самом деле не только эти):

  • Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?
  • Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?, 
  • Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone), Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find(:all, options), Project#milestones.build, Project#milestones.create
  • Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1), Project#categories.delete(category1)


Краткое предостережение.

Не создавайте ассоации с тем же именем, что и существующие методы ActiveRecord::Base. Так как ассоциации метапрограммно
добавляют методы к модели, базовые методы ActiveRecord::Base будут переопределены и все порушится. Например,
создание ассоциаций #attributes или #connection явно не будет хорошей идеей.

Генерируемые методы

Связь "один к одному"

                                |            |  belongs_to  |
  generated methods             | belongs_to | :polymorphic | has_one
  -------------------------------------------+--------------+---------
  #other                        |     X      |       X      |    X
  #other=(other)                |     X      |       X      |    X
  #build_other(attributes={})   |     X      |              |    X
  #create_other(attributes={})  |     X      |              |    X
  #other.create!(attributes={}) |            |              |    X
  #other.nil?                   |     X      |       X      |   
 

Связь "один ко многим" и "многие ко многим"

                                    |       |          | has_many
  generated methods                 | habtm | has_many | :through 
  ----------------------------------+-------+----------+----------
  #others                           |   X   |    X     |    X
  #others=(other,other,...)         |   X   |    X     |   
  #other_ids                        |   X   |    X     |    X
  #other_ids=(id,id,...)            |   X   |    X     |   
  #others<<                         |   X   |    X     |    X
  #others.push                      |   X   |    X     |    X
  #others.concat                    |   X   |    X     |    X
  #others.build(attributes={})      |   X   |    X     |    X
  #others.create(attributes={})     |   X   |    X     |   
  #others.create!(attributes={})    |   X   |    X     |    X
  #others.size                      |   X   |    X     |    X
  #others.length                    |   X   |    X     |    X
  #others.count                     |       |    X     |    X
  #others.sum(args*,&block)         |   X   |    X     |    X
  #others.empty?                    |   X   |    X     |    X
  #others.clear                     |   X   |    X     |   
  #others.delete(other,other,...)   |   X   |    X     |    X
  #others.delete_all                |   X   |    X     |   
  #others.destroy_all               |   X   |    X     |    X
  #others.find(*args)               |   X   |    X     |    X
  #others.find_first                |   X   |          |   
  #others.uniq                      |   X   |    X     |   
  #others.reset                     |   X   |    X     |    X

Типы связей


Ассоциации в ActiveRecord представляют связи типов "один к одному", "один ко многим" и
"многие ко многим". Каждая модель описывает свою роль в связи при помощи макрометодов.
В каждом случае ассоциация +belongs_to+ используется в модели, содержащей внешний
ключ.

Один к одному

Используйте +has_one+ в основной и +belongs_to+ в связанной модели.

  class Employee < ActiveRecord::Base
    has_one :office
  end
  class Office < ActiveRecord::Base
    belongs_to :employee    # внешний ключ в этом случае employee_id
  end

Один ко многим

Используйте +has_many+ в основной ("один") и +belongs_to+ в ассоциированной ("многие") модели.

  class Manager < ActiveRecord::Base
    has_many :employees
  end
  class Employee < ActiveRecord::Base
    belongs_to :manager     #  внешний ключ в данном случае manager_id
  end

Многие ко многим

ActiveRecord предлагает два пути для создания связи типа "многие ко многим". Первый заключается в использовании +has_many+ с опцией :through и, таким образом, иметь именованную связь.

  class Assignment < ActiveRecord::Base
    belongs_to :programmer  # FK: programmer_id
    belongs_to :project     # FK: project_id
  end
  class Programmer < ActiveRecord::Base
    has_many :assignments
    has_many :projects, :through => :assignments
  end
  class Project < ActiveRecord::Base
    has_many :assignments
    has_many :programmers, :through => :assignments
  end

Вторым вариантом будет использование +has_and_belongs_to_many+ в обоих моделях. Это также потребует создания
join таблицы без primary key (обратите внимание, без!).

  class Programmer < ActiveRecord::Base
    has_and_belongs_to_many :projects       # FK в join таблице
  end
  class Project < ActiveRecord::Base
    has_and_belongs_to_many :programmers    # FK в join таблице
  end

Сделать выбор между этими вариантами не всегда просто. Если вам необходимо работать с именованной связью (с самой
ее сущностью), используйте has_many :through. +has_and_belongs_to_many+ обычно используется при работе со
старыми базами или когда сущность связи совершенно никак не используется (с ресурсами второй вариант живет
хуже первого).

 

 +belongs_to+ или +has_one+?


Оба метода представляют собой связь с одним объектом. Разница в том, в таблице какой модели находится внешний ключ. Там
и необходимо использовать +belongs_to+. Пример:

  class User < ActiveRecord::Base
    # Ссылается на модель Account.
    belongs_to :account
  end

  class Account < ActiveRecord::Base
    # На меня ссылается один User.
    has_one :user
  end

Таблицы для этого случая вылядят примерно следующим образом:

  CREATE TABLE users (
    id int(11) NOT NULL auto_increment,
    account_id int(11) default NULL,
    name varchar default NULL,
    PRIMARY KEY  (id)
  )

  CREATE TABLE accounts (
    id int(11) NOT NULL auto_increment,
    name varchar default NULL,
    PRIMARY KEY  (id)
  )

Ассоциации и работа с несохраненными объектами.


Вы можете работать с несохраненными объектами и их ассоциациями, однако, в этом есть своя специфика, которую надо
понимать. В основном это касатеся политики сохранения объектов.
 

Связь "один к одному"

  • Присвоение объекта при помощи методов, генерируемых +has_one+ (то есть, присвоении дочернего объекта, имеющего +belongs_to+) автоматически сохраняет объект с ассоциацией, чтобы обновить внешний ключ. Исключением является случай, когда родительский объект (имеющий +has_many+) еще не сохранен (new_record? == true).
  • Если один из объектов не может быть сохранен (потому как он невалиден) присвоение вернет +false+ будет отменено.
  • Если вы хотите присвоить объект ассоциации +has_one+ без сохранения, используйте метод #association.build, описанный далее.
  • Присвоение объекта ассоциации +belongs_to+ не сохраняет объекты, так как в этом нет необходимости (новое значение первичного ключа хранится у несохраненного объекта и никуда не денется).


Множественные связи


* Добавление объекта в коллекцию (ассоциация может быть указана как +has_many+, так и +has_and_belongs_to_many+) автоматически
  сохраняет добавленный объект, за исключением случая, когда у родительской сущности еще нет первичного ключа, то есть, она
  сама еще не сохранена.
* Если сохранение добавляемого (при помощи метода <tt>#push</tt> ассоциации или прочих вроде <<) не удается, вызов
  <tt>#push</tt> (или <<) вернет +false+.
* Добавить объект в коллекцию без автоматического сохранения возможен с помощью метода <tt>#collection.build</tt>,
  описанного далее.
* Все несохраненные (<tt>new_record? == true</tt>) объекты в коллекции автоматически сохраняются при сохранении родительского
  объекта.

=== Ассоциации и коллбэки (обработчики событий)

Подобно "обычным" коллбэкам (обработчикам событий), коллбэки есть и для ассоциаций, например, вызываемые при добавлении
или удалении объекта. Пример:

  class Project
    has_and_belongs_to_many :developers, :after_add => :evaluate_velocity

    def evaluate_velocity(developer)
      ...
    end
  end

Коллбэки можно группировать, передав массив:

  class Project
    has_and_belongs_to_many :developers, :after_add => [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
  end

Для ассоциаций возможны следующие коллбэки: +before_add+, +after_add+, +before_remove+ и +after_remove+.

Если какой-то из обработчиков +before_add+ вызывает исключение, объект в коллекцию не добавляется. То же самое с
+before_remove+: если обработчик в цепочке вызвал исключение, объект из коллекции не удаляется.

=== Расширение ассоциаций.

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

  class Account < ActiveRecord::Base
    has_many :people do
      def find_or_create_by_name(name)
        first_name, last_name = name.split(" ", 2)
        find_or_create_by_first_name_and_last_name(first_name, last_name)
      end
    end
  end

  person = Account.find(:first).people.find_or_create_by_name("David Heinemeier Hansson")
  person.first_name # => "David"
  person.last_name  # => "Heinemeier Hansson"

Если метод хочется расшарить между разными моделями, используйте модули. Пример:

  module FindOrCreateByNameExtension
    def find_or_create_by_name(name)
      first_name, last_name = name.split(" ", 2)
      find_or_create_by_first_name_and_last_name(first_name, last_name)
    end
  end

  class Account < ActiveRecord::Base
    has_many :people, :extend => FindOrCreateByNameExtension
  end

  class Company < ActiveRecord::Base
    has_many :people, :extend => FindOrCreateByNameExtension
  end

Если хочется использовать несколько расширяющих модулей, поможет опция :extend. В случае конфликта имен (например,
анонимные модули на самом деле получают генерируемое имя, и теоретически вы можете добавить свой модуль с тем же именем)
будет использоваться позже добавленный модуль с тем же именем. Пример:

  class Account < ActiveRecord::Base
    has_many :people, :extend => [FindOrCreateByNameExtension, FindRecentExtension]
  end

Некоторые расширения возможно сделать только зная внутреннее устройство прокси-объектов. Помогут следующие accessors:

* +proxy_owner+      - вернет объект, на котором объявлена ассоциация.
* +proxy_reflection+ - возвращает объект, описывающий тип связи.
* +proxy_target+     - возвращает связанный объект в случае с +belongs_to+ и +has_one+, или коллекцию объектов в случае +has_many+
                       и +has_and_belongs_to_many+.

=== Связывающие модели (Join models)

Ассоциации типа Has Many могут использовать промежуточную именованную связь, для этого используется опция <tt>:through</tt>.
Механизм работы подобен случаю с +has_and_belongs_to_many+. Большое преимущество в том, что связь именованная, то есть, вы
можете добавлять на нее валидации, использовать более богатый набор коллбэков и любую нужную логику в этой связующей модели.

Представим следующую структуру:

  class Author < ActiveRecord::Base
    has_many :authorships
    has_many :books, :through => :authorships
  end

  class Authorship < ActiveRecord::Base
    belongs_to :author
    belongs_to :book
  end

  @author = Author.find :first
  @author.authorships.collect { |a| a.book } # выбираем все книги через перебор связанных Авторств.
  @author.books                              # выбираем все книги через дополнительный JOIN в запросе.

Можно использовать :though и с +has_many+:

  class Firm < ActiveRecord::Base
    has_many   :clients
    has_many   :invoices, :through => :clients
  end
 
  class Client < ActiveRecord::Base
    belongs_to :firm
    has_many   :invoices
  end
 
  class Invoice < ActiveRecord::Base
    belongs_to :client
  end

  @firm = Firm.find :first
  @firm.clients.collect { |c| c.invoices }.flatten
  @firm.invoices                                 

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

Полиморфные ассоциации не ограничивают тип связанного объекта. Вместо этого они указывают именованный интерфейс, которому
эти объекты должны соответствовать.

  class Asset < ActiveRecord::Base
    belongs_to :attachable, :polymorphic => true
  end

  class Post < ActiveRecord::Base
    has_many :assets, :as => :attachable         # Опция :as как раз указывает на используемый полиморфный интерфейс.
  end

  @asset.attachable = @post

Чтобы этот механизм работал, необходимо хранить тип объекта в дополнительной колонке. В случае с моделью Asset выше,
вам потребуется иметь в таблице этой модели колонки +attachable_id+ (целое число) +attachable_type+ (строка).

Использование полиморфных ассоциаций с однотабличным наследованием (STI) имеет свою специфику. Необходимо
убедиться в том, что в поле типа полиморфного объекта хранится базовый класс. Продолжая с тем же примером, что был
рассмотрен выше, представим, что у нас есть два вида постов: гостевые и от зарегистрированных пользователей.
На каждый тип используется собственная модель, но при этом используется однотабличное наследование.
При этом в таблице модели Post должна быть специальная колонка +type+.

  class Asset < ActiveRecord::Base
    belongs_to :attachable, :polymorphic => true
   
    def attachable_type=(sType)
       super(sType.to_s.classify.constantize.base_class.to_s)
    end
  end

  class Post < ActiveRecord::Base
    # Так как в attachable_type мы сохраняем "Post", :dependent => :destroy будет работать
    has_many :assets, :as => :attachable, :dependent => :destroy
  end

  class GuestPost < Post
  end

  class MemberPost < Post
  end

== Кеширование объектов

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

  project.milestones             # делает выборку
  project.milestones.size        # использует кеш
  project.milestones.empty?      # использует кеш
  project.milestones(true).size  # делает выборку
  project.milestones             # использует кеш

== Eager loading (выборка объектов вместе с их ассоциациями)

Eager loading является распространенной техникой выборки объектов с их связями с помощью одного запроса к базе. Это позволяет устранить
проблему 1+N запросов, например, когда необходимость показать 100 постов с их автором вызывает 101 запрос. При использовании
eager loading будет выполнен один единственный запрос. Пример:

  class Post < ActiveRecord::Base
    belongs_to :author
    has_many   :comments
  end

Представим следующую ситуацию:

  for post in Post.find(:all)
    puts "Post:            " + post.title
    puts "Written by:      " + post.author.name
    puts "Last comment on: " + post.comments.first.created_on
  end

Обойдя 100 постов мы сделаем 201 запрос к базе. Сначала уменьшим это число одновременно выбирая автора:

  for post in Post.find(:all, :include => :author)

Здесь мы используем символ, переданный методу +belongs_to+ для построения ассоциации, то есть, <tt>:author</tt>, и получаем JOIN в
итоговом запрос: <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Этим мы снижаем число запросов с 201 до 101.

Однако, можно еще улучшить ситуацию, выбирая сразу обе ассоциации:

  for post in Post.find(:all, :include => [ :author, :comments ])

Это создает еще один join: <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt>. В этом случае мы делаем уже всего один запрос.

Для выборки цепочки ассоциаций, используйте хеш:

  for post in Post.find(:all, :include => [ :author, { :comments => { :author => :gravatar } } ])

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

Все эти возможности не должны вводить вас в заблуждение по поводу того, что большие объемы данных можно выбрать очень быстро просто за
счет того, что вы снижаете число запросов к базе. На создание объектов все равно тратится немало времени, поэтому это совершенно не
серебряная пуля для решение проблем с производительностью. И тем не менее, это хорошее подспорье.

Так как eager loading делает выборки из нескольких таблиц сразу, необходимо явно указывать колонки, по которым, например, производится
сортировка:

<tt>:order => "posts.id DESC"</tt> сработает, в то время как <tt>:order => "id DESC"</tt> нет. Так как eager loading создает запрос
на выборку, опция <tt>:select</tt> игнорируется.

Можно использовать eager loading множества связей из одной таблицы, но их нельзя использовать в сортировках и условиях, так как
на данный момент ActiveRecord не имеет средств для разграничения их имен. Eager loading не выбирает дополнительные аттрибуты
объектов помимо их колонок, поэтому ожидать выборки +has_and_belongs_to_many+ ассоциации не стоит.

== Алиасы для таблиц

ActiveRecord создает алиасы для таблиц, которые используются в JOIN несколько раз. Если таблица используется лишь однажды, тогда
просто подставляется ее имя. При создании алиаса используется схема <tt>#{reflection_name}_#{parent_table_name}</tt> с индексами при
каждом последующем вхождении имени таблицы.

  Post.find :all, :include => :comments
  # => SELECT ... FROM posts LEFT OUTER JOIN comments ON ...
  Post.find :all, :include => :special_comments # STI
  # => SELECT ... FROM posts LEFT OUTER JOIN comments ON ... AND comments.type = 'SpecialComment'
  Post.find :all, :include => [:comments, :special_comments] # special_comments is the reflection name, posts is the parent table name
  # => SELECT ... FROM posts LEFT OUTER JOIN comments ON ... LEFT OUTER JOIN comments special_comments_posts

Пример с использованием плагина Acts as tree (в Rails 2.0 вынесен в отдельный плагин):

  TreeMixin.find :all, :include => :children
  # => SELECT ... FROM mixins LEFT OUTER JOIN mixins childrens_mixins ...
  TreeMixin.find :all, :include => {:children => :parent} # using cascading eager includes
  # => SELECT ... FROM mixins LEFT OUTER JOIN mixins childrens_mixins ...
                              LEFT OUTER JOIN parents_mixins ...
  TreeMixin.find :all, :include => {:children => {:parent => :children}}
  # => SELECT ... FROM mixins LEFT OUTER JOIN mixins childrens_mixins ...
                              LEFT OUTER JOIN parents_mixins ...
                              LEFT OUTER JOIN mixins childrens_mixins_2

Has and Belongs to Many следует той же идее, лишь добавляя суффикс <tt>_join</tt>:

  Post.find :all, :include => :categories
  # => SELECT ... FROM posts LEFT OUTER JOIN categories_posts ... LEFT OUTER JOIN categories ...
  Post.find :all, :include => {:categories => :posts}
  # => SELECT ... FROM posts LEFT OUTER JOIN categories_posts ... LEFT OUTER JOIN categories ...
                             LEFT OUTER JOIN categories_posts posts_categories_join LEFT OUTER JOIN posts posts_categories
  Post.find :all, :include => {:categories => {:posts => :categories}}
  # => SELECT ... FROM posts LEFT OUTER JOIN categories_posts ... LEFT OUTER JOIN categories ...
                             LEFT OUTER JOIN categories_posts posts_categories_join LEFT OUTER JOIN posts posts_categories
                             LEFT OUTER JOIN categories_posts categories_posts_join LEFT OUTER JOIN categories categories_posts

Если вы хотите добавить что-то в JOIN, используйте опцию <tt>:joins</tt>:

  Post.find :all, :include => :comments, :joins => "inner join comments ..."
  # => SELECT ... FROM posts LEFT OUTER JOIN comments_posts ON ... INNER JOIN comments ...
  Post.find :all, :include => [:comments, :special_comments], :joins => "inner join comments ..."
  # => SELECT ... FROM posts LEFT OUTER JOIN comments comments_posts ON ...
                             LEFT OUTER JOIN comments special_comments_posts ...
                             INNER JOIN comments ...

эта опция имеет приоритет выше, чем :include.

Алиасы автоматически обрезаются согласно максимальной длине алиаса, которую разрешает используемая СУБД.

== Модули

По умолчанию ассоциации ищут классы связанных моделей в текущей области видимости модуля. Имея

  module MyApplication
    module Business
      class Firm < ActiveRecord::Base
         has_many :clients
       end

      class Company < ActiveRecord::Base; end
    end
  end

при вызове <tt>Firm#clients</tt> будет использоваться метод <tt>MyApplication::Business::Company.find(firm.id)</tt>. Если вы хотите
связать ассоциацию с другим методом, надо указать класс явно:

  module MyApplication
    module Business
      class Firm < ActiveRecord::Base; end
    end

    module Billing
      class Account < ActiveRecord::Base
        belongs_to :firm, :class_name => "MyApplication::Business::Firm"
      end
    end
  end

== Проверки типов ассоциаций и <tt>ActiveRecord::AssociationTypeMismatch</tt>

При попытке присвоения связи объекта, чей тип отличается от ожидаемого по соглашениям или явно указанного с помощью <tt>:class_name</tt>,
возбуждается исключение типа <tt>ActiveRecord::AssociationTypeMismatch</tt>. Полиморфные связи, конечно же, являются исключением.

== Опции

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


Версія: 
Найостанніші 3 дописи про цю сторінку (всього 10) - переглянути все обговорення
12 Грд 2007 автором Maxim Kulkin
Да, уже понял что ступил..
Извините
12 Грд 2007 автором Michael Klishin
ну само собой, push зовется на
ассоциации, как и <<

@post.comments.push


MK

Можно лучше:
novemberain.com/tags/TiaBWTDI
12 Грд 2007 автором Maxim Kulkin
А, извините, откуда это следует ? Что-то ни разу не видел, чтобы
на самом объекте #push звали.
Да, пожалуйста.
ще 7 дописів »
Створити групу - Групи Google - Домашня сторінка Google - Правила користування послугою - Заява про конфіденційність і нерозголошення інформації
©2009 Google