Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best code structure for Rails associations

The Stage

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

Problem Statement

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.

Method 1

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.

Method 2

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.

Method 3

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:

  • User model remains clutter free.
  • The process function is called 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.

The Answer I am looking for

  • Explains what is the best way to handle such associations. How do people do it normally? and if there are any obvious design flaws in Method 3.
like image 779
Naya Bonbo Avatar asked Mar 13 '23 16:03

Naya Bonbo


1 Answers

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/

like image 74
Jordan Running Avatar answered Mar 24 '23 06:03

Jordan Running