📜 ⬆️ ⬇️

has_many: through => How to quickly access join objects?

You know that when you want to organize many-to-many relationships between two models, the progressive part of humanity uses join-tables and the has_many method with the option :through => :join_model_name . Each connection between two ActiveRecord objects is an ActiveRecord object.

And this is wonderful, because in a join-table you can create useful (so-called "extra") fields with additional information about the relationships between objects.

The question is how beautiful it is to reach these extra attributes.
')
All screencasts and books, unfortunately, operate with simple examples. For example, Article and Category models are friends. Of course, for a join class, the name Categorization or ArticleCategorization is intuitive .

has_many through

Accordingly, if we have two objects - article and category , and we want to find an AR-object (or objects) that personify the connection between them, then the authors of books with a pure heart suggest doing this:

 relations = article.article_categorizations.find_by_category_id(category) 


Life is more complicated. Models often have long compound names, or there is such a connection between models that making a name for each join-model turns into a little torture. Imagine that our models are not Article and Category , but UserGroup and Community , or Preorder and CustomerNotification . What should the binding model be called? Possible options.


Therefore, programmers and somehow pulls to standardize their names within the project, so as not to keep in mind. Templates are chosen according to taste, for example:

1) FirstmodelSecondmodelRelation : ArticleCategoryRelation, UserGroupCommunityRelation or
2) FirstmodelVsSecondmodel : ArticleVsCategory, UserGroupVsCommunity
3) ...

Suppose we chose the first option. And now look at what you have to go to just get to the objects of the connecting model:

 preorder, message = Preorder.first, CustomerNotification.first relations = preorder.preorder_customer_notification_relations.find_by_customer_notification_id(message) 

That is, the "version of the book" looks very verbose. And I would like to see something like:

 preorder.relation_to(message) #      =>   PreorderCustomerNotificationRelation preorder.relations_to(message) #  ,  join-    =>   ActiveRecord::Relation 


similarly, it should work in the opposite direction, symmetrically:

 message.relations_to(preorder).where(:extra_field => "value") 


Why are these features not implemented in Rails? The answer is simple: their names and parameters do not even contain a hint of a link table, and in fact for any two models, the programmer can create as many join tables and links. Which of them should be searched? - unclear.

However, the two functions mentioned above have the right to life and reasonable use.

Because experience suggests:
1) most often between any two models there is only one join-model. And it can be calculated.
2) its objects have to be accessed often, especially if they have extra-attributes.
3) it is not scary to have long names of join-models - if they do not affect the readability of the code.

Add two files to our Rails project:

/lib/ext/active_record/base.rb is the actual ActiveRecord :: Base extension
 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 


I will be glad to any additions and fixes simplifying the code.

I will also be happy to comment "dear, there is a simpler way, do this: ...", because I plan to live for a century and study as much :)

Source: https://habr.com/ru/post/130301/


All Articles