Doing a join operation with a negative condition in Rails - sql

I have two models:
class Member < ActiveRecord::Base
has_many :member_tags
end
and
class MemberTag < ActiveRecord::Base
belongs_to :member
# has a column 'tag'
end
I want to perform the following join:
Member.all(:joins=>:member_tags, :conditions=>"all members that don't have a member_tag with tag="hidden")
How do I do this? I guess it's more of an SQL question, than a rails one :)

I think this could do the trick:
select `members`.*
from `members`
LEFT JOIN `member_tags`
ON `members`.id = `member_tags`.member_id
where `members`.id NOT IN (select `members`.id
from `members`
LEFT JOIN `member_tags`
ON `members`.id = `member_tags`.member_id
where `member_tags`.tag = 'hidden'
);

I cant come up with anything better and this is an ugly method of doing it as it fires (n+1) sql queries for n members
Member.all.select {|member| !(member.member_tags.map(&:tag).include? "hidden")}

Related

How to convert this sql joins/count statements in clean ruby ActiveRecord

I'm trying to convert with no success my find_by_sql statement into a pure ActiveRecord query.
It is:
Corner.find_by_sql('SELECT corners.id, corners.name, count(members.*) FROM corners LEFT JOIN places ON corners.id = places.ubicacion_id LEFT JOIN members ON places.id = members.place_id GROUP BY corners.id,corners.name ORDER BY corners.name;')
nicely formatted, the sql expression would be:
SELECT corners.id,
corners.name,
count(members.*)
FROM corners
LEFT JOIN places ON corners.id = places.ubicacion_id
LEFT JOIN members ON places.id = members.place_id
GROUP BY corners.id,
corners.name
ORDER BY corners.name;
In very old versions of ActiveRecord, my approach would be using the find :all and then passing a hash of options, but this way is deprecated:
Corner.find( :all,
:joins => "LEFT JOIN places ON corners.id = places.ubicacion_id",
:joins => "LEFT JOIN members ON places.id = members.place_id",
:group => "corners.id,corners.name",
:order => "corners.name",
:select => "corners.id, corners.name, count(members.*)"
)
Which one would be the best approach to rewrite in the ActiveRecord way the query? This last snippet works well, but it makes no difference on using it rather than the plain sql one:
Corner.joins("LEFT JOIN places ON corners.id = places.ubicacion_id").joins("LEFT JOIN members ON places.id = members.place_id").group("corners.id,corners.name").order("corners.name").select("corners.id, corners.name, count(members.*)")
Many thanks!
It looks like you probably want to set up some model relationships to make this more ActiveRecord-like. You can find descriptions of how to do this in the Active Record Associations documentation.
Consider these relationships:
class Member < ActiveRecord::Base
belongs_to :place
end
class Place < ActiveRecord::Base
belongs_to :corner
has_many :members
end
class Corner < ActiveRecord::Base
has_many :places, foreign_key: "ubicacion_id"
end
Given those, you should be able to do something like this:
Corner.
select("corners.id, corners.name, count(members.*)").
joins(:places, :members).
group("corners.id, corners.name").
order("corners.name")
Each of the methods chained in the query will refine the query incrementally, much like building a native SQL statement. You can find the official documentation for these methods in the Active Record Query Interface

No such column on ordering by association count

I'm having some issues with trying to order a model by association count. The method I'm using was described here and it works in console and whenever I pause it by using byebug. Other times it doesn't execute the select statement at all and just tries to do the group and order. This is my class:
class Course < ActiveRecord::Base
has_many :course_videos, foreign_key: :course_id
has_many :videos, through: :course_videos
scope :ordered_by_video_count, -> {
joins(:videos)
.select("courses.*, count(videos.id) as video_count")
.group("courses.id")
.order("video_count desc")
}
end
And the exact error I'm getting is this:
SQLite3::SQLException: no such column: video_count: SELECT COUNT(*) AS count_all, courses.id AS courses_id FROM "courses" INNER JOIN "course_videos" ON "course_videos"."course_id" = "courses"."id" INNER JOIN "videos" ON "videos"."id" = "course_videos"."video_id" GROUP BY courses.id ORDER BY video_count desc
Thanks!
Stefan
not sure if it makes a difference, but try
.order(video_count: :desc)
that's the syntax I alway use.

Query using condition within an array

I have 2 models, user and centre, which have a many to many relationship.
class User < ActiveRecord::Base
attr_accessible :name
has_and_belongs_to_many :centres
end
class Centre < ActiveRecord::Base
attr_accessible :name, :centre_id, :city_id, :state_id
has_and_belongs_to_many :users
end
Now I have an user with multiple centres, and I want to retrieve all the centres that have the same "state_id" as that user.
This is what I am doing now
state_id_array = []
user.centres.each do |centre|
state_id_array << centre.state_id
end
return Centre.where("state_id IN (?)", state_id_array).uniq
It works, but it's very ugly. Is there a better way for achieving this? Ideally a one line query.
UPDATE
Now I have
Centre.where('centres.state_id IN (?)', Centre.select('state_id').joins(:user).where('users.id=(?)', user))
The subquery work by itself, but when I tried to execute the entire query, I get NULL for the inner query.
Centre.select('state_id').joins(:user).where('users.id=(?)', user)
will generate
SELECT state_id FROM "centres" INNER JOIN "centres_users" ON "centres_users"."centre_id" = "centres"."id" INNER JOIN "users" ON "users"."id" = "centres_users"."user_id" WHERE (users.id = (5))
Which return 'SA', 'VIC', 'VIC'
but the whole query will generate
SELECT DISTINCT "centres".* FROM "centres" WHERE (centres.state_id IN (NULL,NULL,NULL))
Does user also has state_id column if yes then try this,
User.joins("LEFT OUTER JOIN users ON users.state_id = centers.state_id")
else
try User.joins(:center)
Solved.
.select(:state_id)
will retrieve a model with only the state_id column populated. To retrieve a field, use
.pluck(:state_id)
Below is the final query I had
Centre.where('centres.state_id IN (?)', Centre.joins(:user).where('users.id=(?)', user).pluck('state_id').uniq)

Rails: where not in on has_many through

I have the following model
class Recipe < ActiveRecord::Base
has_many :recipe_allergens
has_many :allergens, through: :recipe_allergens
end
I'm trying to find all the recipes that do not have a given set of allergens, so I tried joins(:allergens).where.not(allergens: { id: allergens }).
Unfortunately, this does not account for the null case, where a Recipe may not have any associated Allergen.
I thought the LEFT OUTER JOIN that includes does would handle this.
How are you supposed to write this query?
EDIT:
I got this working with the following, though it seems really gross:
joins("LEFT OUTER JOIN recipe_allergens ON recipe_allergens.recipe_id = recipes.id")
.joins("LEFT OUTER JOIN allergens ON allergens.id = recipe_allergens.allergen_id")
.where(
Allergen.arel_table[:id].not_in(allergen_ids)
.or(Allergen.arel_table[:id].eq(nil))
)
Please tell me there's a better way!
I would do it this way:
allergenes = Allergene.where #....
allergene_ids = allergenes.pluck(:id)
bad_recipe_ids = RecipeAllergenes.where(allergene_id: allergene_ids)
.pluck(:recipe_id)
recipes = Recipe.where('id NOT IN ?', bad_recipe_ids)

Rails activerecord inner join a custom object

I have classes:
class Want < ActiveRecord::Base
has_many :cached_buy_offers, dependent: :destroy
end
class CachedBuyOffer < ActiveRecord::Base
belongs_to :want
end
So, I can do
Want.joins(:cached_buy_offers)
which works as expected.
I want to generate following sql:
select * from wants
inner join
(select cached_buy_offers.want_id, max(buy_offer_cents) as max_buy_offer, count(cached_buy_offers.want_id) as buy_offer_count
from cached_buy_offers
where cached_buy_offers.want_id in (1,2,3,4)
group by cached_buy_offers.want_id
order by max_buy_offer) as cached_buy_offers
on cached_buy_offers.want_id = wants.id
Inner sql query can be generated using:
ids = [1,2,3,4]
CachedBuyOffer.select('cached_buy_offers.want_id, max(buy_offer_cents) as max_buy_offer, count(cached_buy_offers.want_id) as buy_offer_count').where('cached_buy_offers.want_id in (?)',ids).group('cached_buy_offers.want_id').order('max_buy_offer')
But when I try to do this:
Want.joins(CachedBuyOffer.select ..... the above activerecord inner query)
throws an error RuntimeError: unknown class: CachedBuyOffer
How can I generate the required sql?
You can use Arel.sql.
ids = [1,2,3,4]
cached_buy_offer_subquery = CachedBuyOffer
.select('cached_buy_offers.want_id,
max(buy_offer_cents) as max_buy_offer,
count(cached_buy_offers.want_id) as buy_offer_count')
.where('cached_buy_offers.want_id in (?)',ids)
.group('cached_buy_offers.want_id')
.order('max_buy_offer').to_sql
Want.joins("INNER JOIN (#{Arel.sql(cached_buy_offer_subquery)}) cached_buy_offers ON cached_buy_offers.want_id = wants.id")
.joins takes an association key as an argument, such as Want.joins(:cached_buy_offer). You can chain queries off of that!