Rails activerecord inner join a custom object - sql

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!

Related

Get most recent records from deeply nested model

Say I have 3 models:
ModelA has many ModelB
ModelB has many ModelC
I'm querying ModelA, but in ModelC I have multiple ones of the same type, let's say I have 3 but I only need the most recently one.
I tried to do something like this...
records = ModelA.where(some query).includes ModelB includes ModelC
// convert activerecord collection to array
records = records.to_a
records.each do |record|
record.modelBs.each do |modelB|
filter the modelCs i don't need
modelB.modelCs = filteredModelCs
end
end
return records
but instead of merely returning the array of records, an UPDATE sql query is run and the db records are modified. this is a surprise because i never used the .save method and i thought i had converted the collection from an active record collection to an array
How can I filter deeply nested records without modifying the db records? then i can return the filtered result
Assigning a list of instances to a has_many collection with = will immediately persist the changes to the database.
Instead, I would try to solve this with more specific associations like this:
class A
has_many :bs
has_many(:cs, through: :bs)
has_one :recent_c, -> { order(created_at: :desc).limit(1) }, source: :cs
class B
has_many :cs
With those associations, I would expect the following to work:
as = A.where(some query).includes(:recent_c)
as.each do |a|
a.recent_c # returns the most recent c for this a
end
If I got you right, you want to get a collection of latest Cs, which are connected to Bs, which are connected to certain A-relation? If so, you can do something like that (considering you have tables as, bs and cs):
class A < ApplicationRecord
has_many :bs
end
class B < ApplicationRecord
belongs_to :a
has_many :cs
end
class C < ApplicationRecord
belongs_to :b
scope :recent_for_bs, -> { joins(
<<-sql
INNER JOIN (SELECT b_id, MAX(id) AS max_id FROM cs GROUP BY b_id) recent_cs
ON cs.b_id = recent_cs.b_id AND cs.id = recent_cs.max_id
sql
) }
end
And then you would query Cs like that:
C.recent_for_bs.joins(b: :a).merge(A.where(some_query))
You get recent Cs, inner join them with Bs and As and then get records connected to your A-relation by merging it.

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.

ActiveRecord .merge not working on two relations

I have the following models in my app:
class Company < ActiveRecord::Base
has_many :gallery_cards, dependent: :destroy
has_many :photos, through: :gallery_cards
has_many :direct_photos, class_name: 'Photo'
end
class Photo < ActiveRecord::Base
belongs_to :gallery_card
belongs_to :company
end
class GalleryCard < ActiveRecord::Base
belongs_to :company
has_many :photos
end
As you can see, Company has_many :photos, through: :gallery_cards and also has_many :photos. Photo has both a gallery_card_id and a company_id column.
What I want to be able to do is write a query like #company.photos that returns an ActiveRecord::Relation of all the company's photos. In my Company model, I currently have the method below, but that returns an array or ActiveRecord objects, rather than a relation.
def all_photos
photos + direct_photos
end
I've tried using the .merge() method (see below), but that returns an empty relation. I think the reason is because the conditions that are used to select #company.photos and #company.direct_photos are different. This SO post explains it in more detail.
#company = Company.find(params[:id])
photos = #company.photos
direct_photos = #company.direct_photos
direct_photos.merge(photos) = []
photos.merge(direct_photos) = []
I've also tried numerous combinations of .joins and .includes without success.
this might be a candidate for a raw SQL query, but my SQL skills are rather basic.
For what it's worth, I revisited this and came up (with help) another query that grabs everything in one shot, rather than building an array of ids for a second query. This also includes the other join tables:
Photo.joins("
LEFT OUTER JOIN companies ON photos.company_id = #{id}
LEFT OUTER JOIN gallery_cards ON gallery_cards.id = photos.gallery_card_id
LEFT OUTER JOIN quote_cards ON quote_cards.id = photos.quote_card_id
LEFT OUTER JOIN team_cards ON team_cards.id = photos.team_card_id
LEFT OUTER JOIN who_cards ON who_cards.id = photos.who_card_id
LEFT OUTER JOIN wild_cards ON wild_cards.id = photos.wild_card_id"
).where("photos.company_id = #{id}
OR gallery_cards.company_id = #{id}
OR quote_cards.company_id = #{id}
OR team_cards.company_id = #{id}
OR who_cards.company_id = #{id}
OR wild_cards.company_id = #{id}").uniq
ActiveRecord's merge returns the intersection not the union of the two queries – counterintuitively IMO.
To find the union, you need to use OR, for which ActiveRecord has poor built-in support. So I think you're correct that its best to write the conditions in SQL:
def all_photos
Photo.joins("LEFT OUTER JOIN gallery_cards ON gallery_cards.id = photos.gallery_card_id")
.where("photos.company_id = :id OR gallery_cards.company_id = :id", id: id)
end
ETA The query associates the gallery_cards to photos with a LEFT OUTER JOIN, which preserves those photo rows without associated gallery card rows. You can then query based on either photos columns or on associated gallery_cards columns – in this case, company_id from either table.
You can leverage ActiveRecord scope chaining to join and query from additional tables:
def all_photos
Photo.joins("LEFT OUTER JOIN gallery_cards ON gallery_cards.id = photos.gallery_card_id")
.joins("LEFT OUTER JOIN quote_cards ON quote_cards.id = photos.quote_card_id")
.where("photos.company_id = :id OR gallery_cards.company_id = :id OR quote_cards.company_id = :id", id: id)
end

Query a 3-way relationship in Active Record

I'm trying to figure out how to query this relationship without using find_by_sql
class User < ActiveRecord::Base
has_many :lists
end
class List < ActiveRecord::Base
has_many :list_items
belongs_to :user
end
class ListItem < ActiveRecord::Base
belongs_to :list
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :list_items
end
this should be what we are using but How would I do this not by find_by_sql
in user.rb
def self.find_users_who_like_by_item_id item_id
find_by_sql(["select u.* from users u, lists l, list_items li where l.list_type_id=10 and li.item_id=? and l.user_id=u.id and li.list_id=l.id", item_id])
end
I've tried several different includes / joins / merge scenarios but am not able to get at what I'm trying to do.
thx
It's a bit difficult to tell exactly what query you're trying to do here, but it looks like you want the user records where the user has a list with a particular list_type_id and containing a particular item. That would look approximately like this:
User.joins(:lists => [:list_items]).where('lists.list_type_id = ? and list_items.item_id = ?', list_type_id, item_id)
This causes ActiveRecord to execute a query like the following:
SELECT "users".* FROM "users" INNER JOIN "lists" ON "lists"."user_id" = "users"."id" INNER JOIN "list_items" ON "list_items"."list_id" = "lists"."id" WHERE (lists.list_type_id = 10 and list_items.item_id = 6)
and return the resulting collection of User objects.

Doing a join operation with a negative condition in Rails

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")}