lllllll
lllllll

Reputation: 4835

"Append" two different ActiveRecord::Relation into a single ActiveRecord::Relation

I am in a similar situation as in ICTylor's post here.

So I have:

user1=User.find(1);
user2=User.find(2);
written=Micropost.where("user_id=2");
written.class #=> ActiveRecord::Relation
written.length   #=> 50
replied=Micropost.where("response = 2 ")  #=> 1
replied.class #=> ActiveRecord::Relation

Now if I:

alltogether= written + replied;
alltogether.class #=> Array
alltogether.length #=> 51

However I would like something that would be equivalent to doing:

all_sql= Micropost.where("user_id = 2  OR response = 2")
all_sql.class #=> ActiveRecord::Relation
all_sql.length #=> 51

In other words I would like to somehow append the records found by one Micropost.where(...) to the ones found by the other Micropost.where(...) into an ActiveRecord::Relation object. Resulting in an equivalent of all_sql but reached in two steps.

A bit of explanation. This part of the application is designed to provide a twitter-like functionality for reply messages.

E.g.: When the user with User.id = 1 sends this message to User.id=2:

@2: hey this is a reply.

The application will create a Micropost with the following parameters:

@post= Micropost.create(user:1, content:"@2: hey this is a reply", response: 2)

So response simply indicates the receiver id of the reply. In the case when the message is not of reply-type, then response = nil.

Following this idea, I would like to be able to:

def replies_to(user)
  Micropost.where("response = #{user.id}")
end

def written_posts_by(user)
  Micropost.where("user_id = #{user.id}")
end

def interesting_posts(user)
  replies= replies_to(user)
  written= written_posts_by(user)
  #And now the question arises!

  new_relation= replies union written#<---How can I do this!?
end

Upvotes: 2

Views: 209

Answers (3)

Billy Chan
Billy Chan

Reputation: 24815

The design itself has some problems, say what if a post replied to two or more receivers? Also there will be too much null data in table which is not good.

Anyway, based current design allowing one receiver only, some changes is necessary at model.

class User < ActiveRecord::Base
  has_many :posts
  has_many :replies, class_name: 'Post', foreign_key: 'response_id'

  def written_posts
    posts
  end

  def posts_replied_to_me
    replies
  end
end

Notes of the above changes:

  1. The APIs are better in User table so they don't need arguments like (user)
  2. With association, the above two public methods are actually unnecessary and for present purpose only. You can access these posts directly from association API.

Now for the interesting_posts. Due to the above refactoring you no longer rely the above method to build an aggregated query as they have different structure. Patching as Mori mentioned is a solution but I myself prefer not to touch libs' if possible.

I would prefer a method to aggregate the queries dedicated to this case.

def interested_posts
  Post.where(interesting_criteria)
end

private
def interesting_criteria
  conditions = []
  conditions << written_by_me
  conditions << replied_to_me
  conditions << foo_bar
  conditions.join(' AND ')
end

def written_by_me
  "user_id = #{self.id}"
end

def replied_to_me
  "response_id = #{self.id}"
end

def foo_bar
  "feel free to add more"
end

Upvotes: 1

dyanisse
dyanisse

Reputation: 124

I don't think there is a way built-in Rails to do unions on ActiveRecord::Relation as of today . You're better off writing a query with OR even if it is not so DRY.

With arel:

def interesting_posts(user)
  posts = Micropost.arel_table
  Micropost.where(posts[:response].eq(user.id).or(posts[:user_id].eq(user.id)))
end

With SQL:

def interesting_posts(user)
  Micropost.where('response = ? OR user_id = ?', user.id, user.id)
end

Upvotes: 1

Mori
Mori

Reputation: 27789

This blog post discusses this issue and offers a patch to enable chainable unions in ActiveRecord.

Upvotes: 1

Related Questions