Specifying left join conditions using rails include syntax - sql

Is there a more railsy way to do this query in rails 3?
scope :unblocked_on_invite, joins(
"LEFT JOIN blockers
ON blockers.member_id = members.id
AND blockers.type = 'InviteBlocker'").where("blockers.id IS NULL")

If you use :include it will perform an automatic INNER JOIN. As far as LEFT JOIN goes you are doing exactly what you should be doing. The only way I can see to make this more railsy is to write it like this:
scope :unblocked_on_invite, joins(
"LEFT JOIN blockers
ON blockers.member_id = members.id
AND blockers.type = 'InviteBlocker'").where(:blockers => nil)

Related

Fix sql injection on Rails model scope

There is a model scope I have been trying to refactor, Brakeman is complaining about it so I thought it was a good idea to fix it since we were scanned by bots who are looking for our site vulnerabilities.
scope :cash_deal_aggregated, -> (filter = '') {
select("deals.*")
.from([Arel.sql(
"(SELECT DISTINCT ON (COALESCE(cash_deal_details.cash_deal_id, 0.1*deals.id)) deals.*
FROM deals
INNER JOIN portfolios ON portfolios.id = deals.portfolio_id
LEFT JOIN cash_deal_details ON deals.cash_deal_detail_id = cash_deal_details.id
#{filter}) deals"
)]
)
}
The scope above is used like this:
filter = "WHERE portfolios.client_id = #{client_id}"
deal_records = deal_records = Deal.cash_deal_aggregated(filter)
And it is also used like this:
deal_records = Deal.cash_deal_aggregated
Initially I tried to fix it by adding the filter directly in the query but then got multiple errors.
Appreciate your suggestions for this refactor.
Wrap ActiveRecord's connection.quote(), wrap client_id in this method, e.g., in your case, try this
"WHERE portfolios.client_id = #{connection.quote(client_id)}"
I also got these errors from brakeman earlier and this resolved it.
This is the refactor. Credits to Rajdeep Singh
scope :cash_deal_aggregated, -> (client_id = nil) {
filter = "WHERE portfolios.client_id = #{connection.quote(client_id)}" if client_id
select("deals.*")
.from([Arel.sql(
"(SELECT DISTINCT ON (COALESCE(cash_deal_details.cash_deal_id, 0.1*deals.id)) deals.*
FROM deals
INNER JOIN portfolios ON portfolios.id = deals.portfolio_id
LEFT JOIN cash_deal_details ON deals.cash_deal_detail_id = cash_deal_details.id
#{filter}) deals"
)]
)
}

SQL LEFT JOIN value NOT in either join column

I suspect this is a rather common scenario and may show my ineptitude as a DB developer, but here goes anyway ...
I have two tables: Profiles and HiddenProfiles and the HiddenProfiles table has two relevant foreign keys: profile_id and hidden_profile_id that store ids from the Profiles table.
As you can imagine, a user can hide another user (wherein his profile ID would be the profile_id in the HiddenProfiles table) or he can be hidden by another user (wherein his profile ID would be put in the hidden_profile_id column). Again, a pretty common scenario.
Desired Outcome:
I want to do a join (or to be honest, whatever would be the most efficient query) on the Profiles and HiddenProfiles table to find all the profiles that a given profile is both not hiding AND not hidden from.
In my head I thought it would be pretty straightforward, but the iterations I came up with kept seeming to miss one half of the problem. Finally, I ended up with something that looks like this:
SELECT "profiles".* FROM "profiles"
LEFT JOIN hidden_profiles hp1 on hp1.profile_id = profiles.id and (hp1.hidden_profile_id = 1)
LEFT JOIN hidden_profiles hp2 on hp2.hidden_profile_id = profiles.id and (hp2.profile_id = 1)
WHERE (hp1.hidden_profile_id is null) AND (hp2.profile_id is null)
Don't get me wrong, this "works" but in my heart of hearts I feel like there should be a better way. If in fact there is not, I'm more than happy to accept that answer from someone with more wisdom than myself on the matter. :)
And for what it's worth these are two RoR models sitting on a Postgres DB, so solutions tailored to those constraints are appreciated.
Models are as such:
class Profile < ActiveRecord::Base
...
has_many :hidden_profiles, dependent: :delete_all
scope :not_hidden_to_me, -> (profile) { joins("LEFT JOIN hidden_profiles hp1 on hp1.profile_id = profiles.id and (hp1.hidden_profile_id = #{profile.id})").where("hp1.hidden_profile_id is null") }
scope :not_hidden_by_me, -> (profile) { joins("LEFT JOIN hidden_profiles hp2 on hp2.hidden_profile_id = profiles.id and (hp2.profile_id = #{profile.id})").where("hp2.profile_id is null") }
scope :not_hidden, -> (profile) { self.not_hidden_to_me(profile).not_hidden_by_me(profile) }
...
end
class HiddenProfile < ActiveRecord::Base
belongs_to :profile
belongs_to :hidden_profile, class_name: "Profile"
end
So to get the profiles I want I'm doing the following:
Profile.not_hidden(given_profile)
And again, maybe this is fine, but if there's a better way I'll happily take it.
If you want to get this list just for a single profile, I would implement an instance method to perform effectively the same query in ActiveRecord. The only modification I made is to perform a single join onto a union of subqueries and to apply the conditions on the subqueries. This should reduce the columns that need to be loaded into memory, and hopefully be faster (you'd need to benchmark against your data to be sure):
class Profile < ActiveRecord::Base
def visible_profiles
Profile.joins("LEFT OUTER JOIN (
SELECT profile_id p_id FROM hidden_profiles WHERE hidden_profile_id = #{id}
UNION ALL
SELECT hidden_profile_id p_id FROM hidden_profiles WHERE profile_id = #{id}
) hp ON hp.p_id = profiles.id").where("hp.p_id IS NULL")
end
end
Since this method returns an ActiveRecord scope, you can chain additional conditions if desired:
Profile.find(1).visible_profiles.where("created_at > ?", Time.new(2015,1,1)).order(:name)
Personally I've never liked the join = null approach. I find it counter intuitive. You're asking for a join, and then limiting the results to records that don't match.
I'd approach it more as
SELECT id FROM profiles p
WHERE
NOT EXISTS
(SELECT * FROM hidden_profiles hp1
WHERE hp1.hidden_profile_id = 1 and hp1.profile_id = p.profile_id)
AND
NOT EXISTS (SELECT * FROM hidden_profiles hp2
WHERE hp2.hidden_profile_id = p.profile_id and hp2.profile_id = 1)
But you're going to need to run it some EXPLAINs with realistic volumes to be sure of which works best.

Apply the same chain of arel clauses to different relations

I have two ActiveRecord relations, call them rel1 and rel2. They each get various different joins and where clauses added to them.
I want to apply a certain identical sequence of clauses to each of them, and I don't want to repeat myself.
One way to do this would be to make a function:
def without_orders rel
rel.joins("LEFT JOIN orders ON customers.id = orders.customer_id").where("customers.id IS NULL")
end
rel1 = Customer
rel2 = Customer
# add a bunch of clauses to rel1
# add some other clauses to rel2
rel1 = without_orders(rel1)
rel2 = without_orders(rel2)
Ideally, I wouldn't have without_orders as a separate function. I would somehow put the joins and where in something local to func, and apply that thing to rel1 and rel2.
Is that possible? If not, what is the right approach here?
You could make them all into inidivual scopes:
scope :without_orders, -> { joins("LEFT JOIN orders ON customers.id = orders.customer_id").where(customers: { id: nil }) }
And then you can chain it with other scopes.
Customer.without_orders.where(foo: bar)
This is a good candidate for an activesupport concern
# app/models/concerns/customer_related.rb
module CustomerRelated
extend ActiveSupport::Concern
module ClassMethods
def whithout_orders
joins("LEFT JOIN orders ON customers.id = orders.customer_id").where("customers.id IS NULL")
end
end
end
And then in your models you include it:
include CustomerRelated
And then you can use it like a scope on any model that includes the concern
Rel1.without_orders
or
Rel2.without_orders

using a placeholder with joins

I'm attempting to avoid any SQL injection vulnerabilities by substituting with my params on a join.
Category.joins("LEFT OUTER JOIN incomes ON incomes.category_id = categories.id AND incomes.dept_id = ?", params[:Dept])
This attempts to execute the query with a question mark in it, instead of substituting it for the param. What is the proper way to do this?
EDIT:
Query needs to return this:
SELECT categories.*
FROM "categories"
LEFT OUTER JOIN incomes
ON incomes.category_id = categories.id AND incomes.dept_id = 86
not
SELECT categories.*
FROM "categories"
LEFT OUTER JOIN incomes
ON incomes.category_id = categories.id
WHERE incomes.dept_id = 86
Very different results!
One option is to use the sanitize_sql_array method. It is, however, a protected method so on your Category model you could do:
class Category < ActiveRecord::Base
def self.income_for_dept(dept)
Category.joins(sanitize_sql_array(["LEFT OUTER JOIN incomes ON incomes.category_id = categories.id AND incomes.dept_id = ?", dept]))
end
end
Then you would call it like:
Category.income_for_dept(params[:Dept])
Ruby provides some other methods, if need be, to get at that method without making a class method in Category.
Try
Category.joins(:incomes).where(:incomes => { :dept_id => params[:Dept] })
And check out the Rails documentation for joining tables.

ActiveRecord/ARel modify `ON` in a left out join from includes

I'm wondering if it's possible to specify additional JOIN ON criteria using ActiveRecord includes?
ex: I'm fetching a record and including an association with some conditions
record.includes(:other_record).where(:other_record => {:something => :another})
This gives me (roughly):
select * from records
left outer join other_records on other_records.records_id = records.id
where other_records.something = another
Does anyone know how I can specify an extra join condition so I could achieve something like.
select * from records
left outer join other_records on other_records.records_id = records.id
and other_records.some_date > now()
where other_records.something = another
I want my includes to pull in the other_records but I need additional criteria in my join. Anything using ARel would also be great, I've just never known how to plug a left outer join from ARel into and ActiveRecord::Relation
I can get you close with ARel. NOTE: My code ends up calling two queries behind the scenes, which I'll explain.
I had to work out LEFT JOINs in ARel myself, recently. Best thing you can do when playing with ARel is to fire up a Rails console or IRB session and run the #to_sql method on your ARel objects to see what kind of SQL they represent. Do it early and often!
Here's your SQL, touched up a bit for consistency:
SELECT *
FROM records
LEFT OUTER JOIN other_records ON other_records.record_id = records.id
AND other_records.some_date > now()
WHERE other_records.something = 'another'
I'll assume your records model is Record and other_records is OtherRecord. Translated to ARel and ActiveRecord:
class Record < ActiveRecord::Base
# Named scope that LEFT JOINs OtherRecords with some_date in the future
def left_join_other_in_future
# Handy ARel aliases
records = Record.arel_table
other = OtherRecord.arel_table
# Join criteria
record_owns_other = other[:record_id].eq(records[:id])
other_in_future = other[:some_date].gt(Time.now)
# ARel's #join method lets you specify the join node type. Defaults to InnerJoin.
# The #join_sources method extracts the ARel join node. You can plug that node
# into ActiveRecord's #joins method. If you call #to_sql on the join node,
# you'll get 'LEFT OUTER JOIN other_records ...'
left_join_other = records.join(other, Arel::Nodes::OuterJoin).
on(record_owns_other.and(other_in_future)).
join_sources
# Pull it together back in regular ActiveRecord and eager-load OtherRecords.
joins(left_join_other).includes(:other_records)
end
end
# MEANWHILE...
# Elsewhere in your app
Record.left_join_other_in_future.where(other_records: {something: 'another'})
I bottled the join in a named scope so you don't need to have all that ARel mixed in with your application logic.
My ARel ends up calling two queries behind the scenes: the first fetches Records using your JOIN and WHERE criteria, the second fetches all OtherRecords "WHERE other_records.record_id IN (...)" using a big list of all the Record IDs from the first query.
Record.includes() definitely gives you the LEFT JOIN you want, but I don't know of a way to inject your own criteria into the join. You could use Record.joins() instead of ARel if you wanted to write the SQL yourself:
Record.joins('LEFT OUTER JOIN other_records' +
' ON other_records.record_id = records.id' +
' AND other_records.some_date > NOW()')
I really, really prefer to let the database adapter write my SQL, so I used ARel.
If it were me, I'd consider putting the additional join criterion in the WHERE clause. I assume you're asking because putting the additional criterion on the join makes the query's EXPLAIN look better or because you don't want to deal with NULLs in the other_records.some_date column when there aren't any related other_records.
If you have a simple (equality) extra join condition it could simply be
record.includes(:other_record).where(:other_record => {:something => :another,
:some_date => Time.now})
But if you need the greater than comparison the following should do it.
record.includes(:other_record).where([
'other_records.something = ? and other_records.some_date > ?',
another, Time.now])
Hope that helps.