Вы знаете, что когда требуется организовать 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
Буду рад любым дополнениям и фиксам, упрощающим код.
Также буду рад комментариям «уважаемый, есть способ проще, делай так: ...», ибо планирую жить век и столько же учиться :)