Вы знаете, что когда требуется организовать many-to-many отношения между двумя моделями, прогрессивная часть человечества применяет join-таблицы и метод
И это чудесно, ибо в join-таблице можно насоздавать полезных (так называемых «extra») полей с дополнительной информацией о связях между объектами.
Вопрос в том, как красиво достучаться до этих extra атрибутов.
Все скринкасты и книжки, как назло, оперируют простыми примерами. Например, дружат между собой модели Article и Category. Само собой, для join-класса интуитивно напрашивается имя Categorization или ArticleCategorization.

Соответственно, если у нас есть два объекта — article и category, и мы хотим найти AR-объект (или объекты), олицетворяющий связь между ними, то авторы книжек с чистым сердцем предлагают делать так:
В жизни все сложнее. Модели нередко имеют длинные составные имена, либо между моделями такая связь, что придумывание имени для каждой join-модели превращается в маленькую пытку. Представим, что у нас модели не Article и Category, а UserGroup и Community, или Preorder и CustomerNotification. Как должна называться связующая модель? Возможны варианты.
Поэтому программистов так и тянет как-то стандартизировать их названия в рамках проекта, чтобы не держать в голове. Шаблоны выбирают по вкусу, к примеру:
1) FirstmodelSecondmodelRelation: ArticleCategoryRelation, UserGroupCommunityRelation или
2) FirstmodelVsSecondmodel: ArticleVsCategory, UserGroupVsCommunity
3)…
Предположим мы выбрали первый вариант. А теперь смотрите, на что придется пойти, чтобы всего-то лишь добраться до объектов связующей модели:
То есть «вариант из книжки» выглядит очень многословно. А я бы хотел видеть нечто вроде:
аналогично, должно работать и в обратную сторону, симметрично:
Почему подобные функции не реализованы в Rails? Ответ прост: их имена и параметры не содержат даже намека на связующую таблицу, а ведь для любых двух моделей программист может насоздавать сколько угодно join-таблиц и связей. В каких из них должен производиться поиск? — непонятно.
Тем не менее, две упомянутые выше функции имеют право на жизнь и разумное использование.
Потому что опыт подсказывает:
1) чаще всего между любыми двумя моделями находится лишь одна join-модель. И ее можно вычислить.
2) к ее объектам приходится часто обращаться, особенно если в них есть extra-атрибуты.
3) не страшно иметь длинные имена join-моделей — если они не влияют на читаемость кода.
Добавляем в наш Rails-проект два файла:
/lib/ext/active_record/base.rb — это собственно расширение ActiveRecord::Base
/config/initializers/ext.rb
Буду рад любым дополнениям и фиксам, упрощающим код.
Также буду рад комментариям «уважаемый, есть способ проще, делай так: ...», ибо планирую жить век и столько же учиться :)
has_many с опцией :through => :join_model_name. Каждая связь между двумя ActiveRecord-объектами представляет собой ActiveRecord-объект.И это чудесно, ибо в join-таблице можно насоздавать полезных (так называемых «extra») полей с дополнительной информацией о связях между объектами.
Вопрос в том, как красиво достучаться до этих extra атрибутов.
Все скринкасты и книжки, как назло, оперируют простыми примерами. Например, дружат между собой модели Article и Category. Само собой, для join-класса интуитивно напрашивается имя Categorization или ArticleCategorization.

Соответственно, если у нас есть два объекта — article и category, и мы хотим найти AR-объект (или объекты), олицетворяющий связь между ними, то авторы книжек с чистым сердцем предлагают делать так:
relations = article.article_categorizations.find_by_category_id(category)
В жизни все сложнее. Модели нередко имеют длинные составные имена, либо между моделями такая связь, что придумывание имени для каждой join-модели превращается в маленькую пытку. Представим, что у нас модели не Article и Category, а UserGroup и Community, или Preorder и CustomerNotification. Как должна называться связующая модель? Возможны варианты.
Поэтому программистов так и тянет как-то стандартизировать их названия в рамках проекта, чтобы не держать в голове. Шаблоны выбирают по вкусу, к примеру:
1) FirstmodelSecondmodelRelation: ArticleCategoryRelation, UserGroupCommunityRelation или
2) FirstmodelVsSecondmodel: ArticleVsCategory, UserGroupVsCommunity
3)…
Предположим мы выбрали первый вариант. А теперь смотрите, на что придется пойти, чтобы всего-то лишь добраться до объектов связующей модели:
preorder, message = Preorder.first, CustomerNotification.first relations = preorder.preorder_customer_notification_relations.find_by_customer_notification_id(message)
То есть «вариант из книжки» выглядит очень многословно. А я бы хотел видеть нечто вроде:
preorder.relation_to(message) # это если запись заведомо одна => объект класса PreorderCustomerNotificationRelation preorder.relations_to(message) # если допускается, что join-записей может быть несколько => объект класса ActiveRecord::Relation
аналогично, должно работать и в обратную сторону, симметрично:
message.relations_to(preorder).where(:extra_field => "value")
Почему подобные функции не реализованы в Rails? Ответ прост: их имена и параметры не содержат даже намека на связующую таблицу, а ведь для любых двух моделей программист может насоздавать сколько угодно join-таблиц и связей. В каких из них должен производиться поиск? — непонятно.
Тем не менее, две упомянутые выше функции имеют право на жизнь и разумное использование.
Потому что опыт подсказывает:
1) чаще всего между любыми двумя моделями находится лишь одна join-модель. И ее можно вычислить.
2) к ее объектам приходится часто обращаться, особенно если в них есть extra-атрибуты.
3) не страшно иметь длинные имена join-моделей — если они не влияют на читаемость кода.
Добавляем в наш Rails-проект два файла:
/lib/ext/active_record/base.rb — это собственно расширение ActiveRecord::Base
module MyExtensions module ActiveRecord module Base # применяется в моделях, использующих has_many->through # возвращает объект класса ActiveRecord::Relation либо nil def relations_to(target) return nil unless target.kind_of? ::ActiveRecord::Base reflection = self.class.reflections.find do |r| r[1].instance_of? ::ActiveRecord::Reflection::ThroughReflection and r[1].klass == target.class end.at 1 rescue nil # потому что вернётся Array return nil unless reflection self.send(reflection.through_reflection.name).where(reflection.foreign_key.to_sym => target.id) end def relation_to(target) rels = relations_to(target) if rels.instance_of? ::ActiveRecord::Relation return (rels.count > 0) ? rels.first : nil end rels end end end end class ActiveRecord::Base include MyExtensions::ActiveRecord::Base end
/config/initializers/ext.rb
# Load extensions to existing classes. Dir["lib/ext/**/*.rb"].each do |fn| require File.expand_path( fn ) end
Буду рад любым дополнениям и фиксам, упрощающим код.
Также буду рад комментариям «уважаемый, есть способ проще, делай так: ...», ибо планирую жить век и столько же учиться :)
