problem: activerecord (rails3), chaining scopes with includes - ruby-on-rails

In Rails3 there seems to be a problem when chaining two scopes (ActiveRelations) that each have a different include:
Consider these two scopes, both of which work fine on their own:
First scope:
scope :global_only, lambda { |user|
includes(:country)
.where("countries.area_id <> ?", user.area) }
Work.global_only(user) => (cut list of fields from SQL for legibility)
SELECT * FROM "works" LEFT OUTER JOIN "countries" ON "countries"."id" = "works"."country_id" WHERE (countries.area_id <> 3)
Now the second scope:
scope :not_belonging_to, lambda { |user|
includes(:participants)
.where("participants.user_id <> ? or participants.user_id is null", user) }
Work.not_belonging_to(user) => (cut list of fields from SQL for legibility)
SELECT * FROM "works" LEFT OUTER JOIN "participants" ON "participants"."work_id" = "works"."id" WHERE (participants.user_id <> 6 or participants.user_id is null)
So both of those work properly individually.
Now, chain them together:
Work.global_only(user).not_belonging_to(user)
The SQL:
SELECT (list of fields) FROM "works" LEFT OUTER JOIN "countries" ON "countries"."id" = "works"."country_id" WHERE (participants.user_id <> 6 or participants.user_id is null) AND (countries.area_id <> 3)
As you can see, the join from the second scope is ignored altogether. The SQL therefore fails on 'no such column 'participants.user_id'. If I chain the scopes in the reverse order, then the 'participants' join will be present and the 'countries' join will be lost. It's always the second join that is lost, it seems.
Does this look like a bug with ActiveRecord, or am I doing something wrong, or is this a "feature" :-)
(PS. Yes, I know, I can create a scope that joins both tables and it will correctly yield the result I want. I have that already. But I was trying to make smaller scopes than can be chained together in different ways, which is supposed to be the advantage of activerecord over straight sql.)

As a general rule, use :includes for eager-loading and :joins for conditions. In the second scope, the join SQL must be manually written because a left join is required.
That said, try this:
scope :global_only, lambda { |user|
joins(:country).
where(["countries.area_id != ?", user.area])
}
scope :not_belonging_to, lambda { |user|
joins("left join participants on participants = #{user.id}").
where("participants.id is null")
}
Work.global_only(user).not_belonging_to(user)

Related

how to write a join condition in Ruby on Rails?

what I'm trying to do is to write something like the next query:
SELECT *
FROM Customers c
LEFT JOIN CustomerAccounts ca
ON ca.CustomerID = c.CustomerID
AND c.State = 'NY'
Notice that I'm not using any WHERE clause, but I need to my JOIN have a condition. I cannot make it work in Ruby on Rails.
Can you help me out?
You can join the tables with LEFT JOIN. Just pass the join condition in joins and you will get the expected result
Customer.joins("LEFT JOIN CustomerAccounts
ON CustomerAccounts.CustomerID = Customers.CustomerID
AND Customers.State = 'NY'")
#=> SELECT * FROM Customers LEFT JOIN CustomerAccounts ON CustomerAccounts.CustomerID = Customers.CustomerID AND Customers.State = 'NY'
Note: just .joins() does INNER JOIN so you need to specify the join with condition
Your SQL code, translated to activerecord, would look as follows (using joins):
Customer.where(state: 'NY').joins(:customer_accounts)
The code assumes, you have the association set up:
class Customer
has_many :customer_accounts
end

Eager loaing with LEFT OUTER JOIN and multiple conditions in the ON clause

I have word's senses and translations in different languages, here is schema http://sqlfiddle.com/#!15/cba5e/2
I need to get all senses and only Spain translations (code "es"), so query should looks like
SELECT *
FROM senses s
LEFT OUTER JOIN translations t ON
s.id = t.sense_id AND t.language_code = 'es';
Can I do it with eager_load/includes/..?
Code like
Sense.includes(:translations).where(translations: { language_code: 'es' })
doesn't work, because it specify conditions in the WHERE clause and generate query like
SELECT *
FROM senses s
LEFT OUTER JOIN translations t ON s.id = t.sense_id
WHERE t.language_code = 'es';
and I get only senses with "es" translations. But I need all senses + spain translations, see the difference: http://sqlfiddle.com/#!15/cba5e/2 and http://sqlfiddle.com/#!15/cba5e/3
From https://www.postgresql.org/docs/current/static/queries-table-expressions.html:
Restriction placed in the ON clause is processed before the join, while a restriction placed in the WHERE clause is processed after the join
Found a way to fulfill the eager loading with left outer join. it is done by using association with condition.
Sense (model)
has_many es_translations -> { where(language_code: 'es') },
:class_name => "Translations",
:foreign_key => "foreign_key_column"
# references is used to ensure eager load
Sense.includes(:es_translations).references(:es_translations)
with this it will create a left outer join with condition on the ON query.
This will do
Sense.includes(:translations).where('translations.language_code = ?', 'es')
includes does LEFT OUTER JOIN

How can I merge two active record relation with OR condition in rails 3 and return result also an active relation not array?

I have two associations like surgical_diseases and eye_disease.I want to get the Ored result of the two active relation.But the below code gave me an array.
has_many :surgical_diseases
has_many :eye_disease
scope :all_disease ->(name) { joins(:surgical_diseases).where('surgical_diseases.name IN (?)') | joins(:eye_disease).where('eye_disease.name IN (?)') }
I have seen active-record-union gem but that would only work with active-record 4.I am currently using rails 3.2 so not able to use that.
I also saw that this functionality will come with rails5 with dhh's commit.But not sure how will I fix this with rail3 now.
I tried my best to make understanding of my question.Please let me know if anything else information is require.
Thanks in advance!
You would probably need to get the ids using find_by_sql and then find those ids to get ActiveRecord::Relation.
scope :all_disease ->(name) {
ids = YourTable.find_by_sql <<-SQL
SELECT your_table.id FROM your_table INNER JOIN surgical_diseases sd ON sd.your_table_id=your_table.id WHERE sd.name IN (#{name})
UNION
SELECT your_table.id FROM your_table INNER JOIN eye_diseases ed ON ed.your_table_id=your_table.id WHERE ed.name IN (#{name})
SQL
YourTable.where(id: ids)
}
Perhaps, left outer join can help you:
scope :all_disease ->(name) {
joins('LEFT OUTER JOIN surgical_diseases ON surgical_diseases.whatever_table_for_your_models_id = whatever_table_for_your_models.id')
.joins('LEFT OUTER JOIN eye_diseases ON eye_diseases.whatever_table_for_your_models_id = .whatever_table_for_your_models.id')
.where('surgical_diseases.name IN (?) OR eye_diseases.name IN (?)', name)

Scope with "WHERE ... LIKE" on a related table

I'm trying to get data from a Postgresql table (table1) filtered by a field (property) of an other related table (table2).
In pure SQL I would write the query like this:
SELECT * FROM table1 JOIN table2 USING(table2_id) WHERE table2.property LIKE 'query%'
This is working fine:
scope :my_scope, ->(query) { includes(:table2).where("table2.property": query) }
But what I really need is to filter with a LIKE operator rather than strict equality. However this is not working:
scope :my_scope, ->(query) { includes(:table2).where("table2.property LIKE ?", "#{query}%") }
As I am getting this error:
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "table2" LINE 1: ...ble2" WHERE "table1"."user_id" = $1 AND (tabl... ^ : SELECT "table1".* FROM "table1" WHERE "table1"."user_id" = $1 AND (table2.property LIKE 'query%') ORDER BY last_used_at DESC
What am I doing wrong here?
.includes() usually runs 2 separate queries unless it can find that your conditions forces a single LEFT OUTER JOIN query, but it fails to do so in your case as the references are in a string (see this example).
You can force the single query behaviour by specifing .references(:table2):
scope :my_scope, ->(query) { includes(:table2)
.references(:table2)
.where("table2.property LIKE ?", "#{query}%") }
Or you can you can just use .eager_load():
scope :my_scope, ->(query) { eager_load(:table2)
.where("table2.property LIKE ?", "#{query}%") }
Try by this way, By adding [] in query.
scope :my_scope, ->(query) { includes(:table2).where(["table2.property LIKE (?)", "#{query}%"]) }
Also try by adding (?).

Merge 2 relations on OR instead of AND

I have these two pieces of code that each return a relation inside the Micropost model.
scope :including_replies, lambda { |user| where("microposts.in_reply_to = ?", user.id)}
def self.from_users_followed_by(user)
followed_user_ids = user.followed_user_ids
where("user_id IN (?) OR user_id = ?", followed_user_ids, user)
end
When I run r1 = Micropost.including_replies(user) I get a relation with two results with the following SQL:
SELECT `microposts`.* FROM `microposts` WHERE (microposts.in_reply_to = 102) ORDER BY
microposts.created_at DESC
When I run r2 = Micropost.from_users_followed_by(user) I get a relation with one result with the following SQL:
SELECT `microposts`.* FROM `microposts` WHERE (user_id IN (NULL) OR user_id = 102) ORDER
BY microposts.created_at DESC
Now when I merge the relations like so r3 = r1.merge(r2) I got zero results but was expecting three. The reason for this is that the SQL looks like this:
SELECT `microposts`.* FROM `microposts` WHERE (microposts.in_reply_to = 102) AND
(user_id IN (NULL) OR user_id = 102) ORDER BY microposts.created_at DESC
Now what I need is (microposts.in_reply_to = 102) OR (user_id IN (NULL) OR user_id = 102)
I need an OR instead of an AND in the merged relation.
Is there a way to do this?
Not directly with Rails. Rails does not expose any way to merge ActiveRelation (scoped) objects with OR. The reason is that ActiveRelation may contain not only conditions (what is described in the WHERE clause), but also joins and other SQL clauses for which merging with OR is not well-defined.
You can do this either with Arel directly (which ActiveRelation is built on top of), or you can use Squeel, which exposes Arel functionality through a DSL (which may be more convenient). With Squeel, it is still relevant that ActiveRelations cannot be merged. However Squeel also provides Sifters, which represent conditions (without any other SQL clauses), which you can use. It would involve rewriting the scopes as sifters though.

Resources