gusa
gusa

Reputation: 833

Rails includes with conditions

Is is possible in Rails > 3.2 to add conditions to the join statement generated by the includes method?

Let's say I have two models, Person and Note. Each person has many notes and each note belong to one person. Each note has an attribute important.

I want to find all the people preloading only the notes that are important. In SQL that will be:

SELECT *
FROM people
LEFT JOIN notes ON notes.person_id = people.id AND notes.important = 't'

In Rails, the only similar way to do that is using includes (note: joins won't preload notes) like this:

Person.includes(:notes).where(:important, true)

However, that will generate the following SQL query which returns a different result set:

SELECT *
FROM people
LEFT JOIN notes ON notes.person_id = people.id
WHERE notes.important = 't'

Please, notice that the first resultset includes all the people and the second one only the people associated to important notes.

Also notice that :conditions are deprecated since 3.1.

Upvotes: 76

Views: 80404

Answers (9)

Elamine
Elamine

Reputation: 1

you can try

Person.includes(:notes).where(notes: { important: 't' })

make sure that the Note table name is notes and in person model the association like this : has_many :notes or you can change it Person.includes(:asoociation).where(table_name: { important: 't' })

Upvotes: 0

stevec
stevec

Reputation: 52268

A really simple example:

User.includes(:posts).where(posts: { name: 'John' })

Notes:

  • If your association is has_one, replace :posts with :post (watch the plural/singular).

  • More info here.


To take it one association further, if you had

thing.other_things.includes(:even_more_things).where(other_things: {col1: 'value'})

but you wanted to filter on even_more_things, you could use:

thing.other_things.joins(:even_more_things).where(even_more_things: { attribute: value })

Upvotes: 1

Daniel Loureiro
Daniel Loureiro

Reputation: 5333

Rails 4.2+:

Option A - "preload": multiple selects, uses "id IN (...)"

class Person < ActiveRecord::Base
  has_many :notes
  has_many :important_notes, -> { where(important: true) }, class_name: "Note"
end

Person.preload(:important_notes)

SQL:

SELECT "people".* FROM "people"

SELECT "notes".* FROM "notes" WHERE "notes"."important" = ? AND "notes"."person_id" IN (1, 2)

Option B - "eager_load": one huge select, uses "LEFT JOIN"

class Person < ActiveRecord::Base
  has_many :notes
  has_many :important_notes, -> { where(important: true) }, class_name: "Note"
end

Person.eager_load(:important_notes)

SQL:

SELECT "people"."id" AS t0_r0, "people"."name" AS t0_r1, "people"."created_at" AS t0_r2, "people"."updated_at" AS t0_r3, "notes"."id" AS t1_r0, "notes"."person_id" AS t1_r1, "notes"."important" AS t1_r2 
FROM "people" 
LEFT OUTER JOIN "notes" ON "notes"."person_id" = "people"."id" AND "notes"."important" = ?

Upvotes: 24

Yuki Inoue
Yuki Inoue

Reputation: 3777

Same was discussed in Japanese stackoverflow. Quite hacky, but following seems to work, at least on rails 5.

Person.eager_load(:notes).joins("AND notes.important = 't'")

One important aspect is that by this way, you can write arbitrary join condition. Down side is that you cannot use placeholder so you need to be careful when using params as the join condition.

https://ja.stackoverflow.com/q/22812/754

Upvotes: 11

Graham Slick
Graham Slick

Reputation: 6870

Rails 5+ syntax:

Person.includes(:notes).where(notes: {important: true})

Nested:

Person.includes(notes: [:grades]).where(notes: {important: true, grades: {important: true})

Upvotes: 42

Yoko
Yoko

Reputation: 803

For people interested, I tried this where a record attribute was false

Lead.includes(:contacts).where("contacts.primary" => false).first

and this doesn't work. Somehow for booleans only true works, so I turned it around to include where.not

Lead.includes(:contacts).where.not("contacts.primary" => true).first

This works perfectly

Upvotes: 2

juliangonzalez
juliangonzalez

Reputation: 4381

I was unable to use the includes with a condition like Leo Correa's answer. Insted I neeed to use:

Lead.includes(:contacts).where("contacts.primary" =>true).first

or you can also

Lead.includes(:contacts).where("contacts.primary" =>true).find(8877)

This last one will retrieve the Lead with id 8877 but will only include its primary contact

Upvotes: 5

kirstu
kirstu

Reputation: 1

One way is to write the LEFT JOIN clause yourself by using joins:

Person.joins('LEFT JOIN "notes" ON "notes"."person_id" = "people.id" AND "notes"."important" IS "t"')

Not pretty, though.

Upvotes: 0

Leo Correa
Leo Correa

Reputation: 19789

According to this guide Active Record Querying

You can specify conditions on includes for eager loading like this

Person.includes(:notes).where("notes.important", true)

It recommends to use joins anyway.

A workaround for this would be to create another association like this

class Person < ActiveRecord::Base
  has_many :important_notes, :class_name => 'Note', 
           :conditions => ['important = ?', true]
end

You would then be able to do this

Person.find(:all, include: :important_notes)

Upvotes: 48

Related Questions