Lets talk about the most common type of association we encounter.
I have a User which :has_many
Post(s)
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end
I want to do some (very light and quick) processing on all the posts of a user. I am looking for the best way to structure my code to achieve it. Below are a couple of ways and why they work or don't work.
Do it in the User
class itself.
class User < ActiveRecord::Base
has_many :posts
def process_posts
posts.each do |post|
# code of whatever 'process' does to posts of this user
end
end
end
Post class remains the same:
class Post < ActiveRecord::Base
belongs_to :user
end
The method is called as:
User.find(1).process_posts
Why doesn't this look the best way to do it
The logic of doing something with the posts of the user should really belong to the Post class. In a real world scenario, a user might also have :has_many
relations with a lot of other classes e.g. orders
, comments
, children
etc.
If we start adding similar process_orders
, process_comments
, process_children
(yikes) methods to the User class, it'll result in one giant file with lots of code much of which could (and should) be distributed to where it belongs i.e. the target associations.
Proxy Associations and Scopes
Both of these constructs require addition of methods/code to the User class which again makes it bloated. I'd rather have all implementation shifted to the target classes.
Class Method on target Class
Create class methods in the target class and call those methods on the User object.
class User < ActiveRecord::Base
has_many :comments
# all target specific code in target classes
end
class Post < ActiveRecord::Base
belongs_to :user
# Class method
def self.process
Post.all.each do |post| # see Note 2 below
# code of whatever 'process' does to posts of this user
end
end
end
The method is called as:
User.find(1).posts.process # See Note 1 below
Now, this looks and feels better than Method 1 and 2 because:
process
instead of process_posts
. Now we can have a process
for other classes as well and invoke them as: User.find(1).orders.process
etc. instead of User.find(1).process_orders
(Method 1).Note 1:
Yes you can call a class method like this on a association. Read why here. TL;DR is that User.find(1).posts
returns a CollectionProxy object which has access to class methods of the target (Post) class. It also conveniently passes a scope_attributes
which stores the user_id of the user which called posts.process
. This comes handy. See Note 2 below.
Note 2:
For people not sure whats going on when we do a Post.all.each
in the class method, it returns all the posts of the user this method was called on as against all the posts in the database.
So when called as User.find(99).posts.process
, Post.all
executes:
SELECT "notes".* FROM "posts" WHERE "posts"."user_id" = $1 [["user_id", 99]]
which are all the posts for User ID: 99.
Per @Jesuspc's comment below, Post.all.each
can be succinctly written as all.each
. Its more idiomatic and doesn't make it look like we are querying all posts in the database.
There's a fourth option. Move this logic out of the model entirely:
class PostProcessor
def initialize(posts)
@posts = posts
end
def process
@posts.each do |post|
# ...
end
end
end
PostProcessor.new(User.find(1).posts).process
This is sometimes called the Service Object pattern. A very nice bonus of this approach is that it makes writing tests for this logic really simple. Here's a great blog post on this and other ways to refactor "fat" models: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With