Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ARel mimic includes with find_by_sql

I've got a fairly complex sql query that I'm pretty sure I can't accomplish with ARel (Rails 3.0.10)

Check out the link, but it has a few joins and a where exists clause, and that I'm pretty sure is too complex for ARel.

My problem however is that, before this query was so complex, with ARel I could use includes to add other models that I needed to avoid n+1 issues. Now that I'm using find_by_sql, includes don't work. I still want to be able to fetch these records and attach them to my model instances, the way includes does, but I'm not quite sure how to achieve this.

Can someone point me in the right direction?

I haven't tried joining them in the same query yet. I'm just not sure how they would be mapped to objects (ie. if ActiveRecord would properly map them to the proper class)

I know that when using includes ActiveRecord actually makes a second query, then somehow attaches those rows to the corresponding instances from the original query. Can someone instruct me on how I might do this? Or do I need to join in the same query?

like image 543
brad Avatar asked Aug 19 '11 01:08

brad


People also ask

What is an Arel?

Arel is a SQL abstraction that ActiveRecord uses to build SQL queries. Arel wraps each component of the SQL query language with Ruby objects and provides an expressive DSL for composing SQL queries. When using Arel, you're mainly interacting with tables ( Arel::Table ) and nodes ( Arel::Nodes::Node subclasses).

What is Arel query?

Arel is a quite powerful SQL AST manager that lets you appropriately combine selection statements for simple to very complicated queries.


3 Answers

Let's pretend that the SQL really can't be reduced to Arel. Not everything can, and we happen to really really want to keep our custom find_by_sql but we also want to use includes.

Then preload_associations is your friend: (Updated for Rails 3.1)

class Person
  def self.custom_query
    friends_and_family = find_by_sql("SELECT * FROM people")
# Rails 3.0 and lower use this: 
#        preload_associations(friends_and_family, [:car, :kids])
# Rails 3.1 and higher use this: 
    ActiveRecord::Associations::Preloader.new(friends_and_family, [:car, :kids]).run
    friends_and_family
  end
end

Note that the 3.1 method is much better, b/c you can apply the eager-loading at any time. Thus you can fetch the objects in your controller, and then just before rendering, you can check the format and eager-load more associations. That's what happens for me - html doens't need the eager loading, but the .json does.

That help?

like image 133
Rob Avatar answered Nov 11 '22 20:11

Rob


I am pretty sure that you can do even the most complex queries with Arel. Maybe you are being over-skeptical about it.

Check these:

  • Rails 3: Arel for NOT EXISTS?

  • How to do "where exists" in Arel

like image 41
Pedro Rolo Avatar answered Nov 11 '22 20:11

Pedro Rolo


@pedrorolo thanks for the heads up on that not exists arel query, helped me achieve what I needed. Here's the final solution (they key is the final .exists on the GroupChallenge query:

class GroupChallenge < ActiveRecord::Base
  belongs_to :group
  belongs_to :challenge  

  def self.challenges_for_contact(contact_id, group_id=nil)
    group_challenges = GroupChallenge.arel_table
    group_contacts = GroupContact.arel_table
    challenges = Challenge.arel_table
    groups = Group.arel_table

    query = group_challenges.project(1).
              join(group_contacts).on(group_contacts[:group_id].eq(group_challenges[:group_id])).
              where(group_challenges[:challenge_id].eq(challenges[:id])).
              where(group_challenges[:restrict_participants].eq(true)).
              where(group_contacts[:contact_id].eq(contact_id))

    query = query.join(groups).on(groups[:id].eq(group_challenges[:group_id])).where(groups[:id].eq(group_id)) if group_id

    query
  end
end

class Challenge < ActiveRecord::Base
  def self.open_for_participant(contact_id, group_id = nil)
    open.
      joins("LEFT OUTER JOIN challenge_participants as cp ON challenges.id = cp.challenge_id AND cp.contact_id = #{contact_id.to_i}").
        where(['cp.accepted != ? or cp.accepted IS NULL', false]).
      where(GroupChallenge.challenges_for_contact(contact_id, group_id).exists.or(table[:open_to_all].eq(true)))
  end
end
like image 42
brad Avatar answered Nov 11 '22 19:11

brad