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.
Related
I'm having an issue where a many-to-many relationship with an "extension" is generating incorrect SQL.
class OrderItem < ApplicationRecord
belongs_to :buyer, class_name: :User
belongs_to :order
belongs_to :item, polymorphic: true
end
class User < ApplicationRecord
has_many :order_items_bought,
-> { joins(:order).where.not(orders: { state: :expired }).order(created_at: :desc) },
foreign_key: :buyer_id,
class_name: :OrderItem
has_many :videos_bought,
-> { joins(:orders).select('DISTINCT ON (videos.id) videos.*').reorder('videos.id DESC') },
through: :order_items_bought,
source: :item,
source_type: :Video do
def confirmed
where(orders: { state: :confirmed })
end
end
end
user.videos_bought.confirmed generates this SQL:
Video Load (47.0ms) SELECT DISTINCT ON (videos.id) videos.* FROM
"videos" INNER JOIN "order_items" "order_items_videos_join" ON
"order_items_videos_join"."item_id" = "videos"."id" AND
"order_items_videos_join"."item_type" = $1 INNER JOIN
"orders" ON "orders"."id" = "order_items_videos_join"."order_id" INNER JOIN
"order_items" ON "videos"."id" = "order_items"."item_id" WHERE
"order_items"."buyer_id" = $2 AND ("orders"."state" != $3) AND "order_items"."item_type" = $4 AND
"orders"."state" = $5 ORDER BY videos.id DESC, "order_items"."created_at" DESC LIMIT $6
Which returns some Video records which are joined with orders that do NOT have state confirmed. I would expect all orders to have state confirmed.
If I use raw SQL everything works fine:
has_many :videos_bought,
-> {
joins('INNER JOIN orders ON orders.id = order_items.order_id')
.select('DISTINCT ON (videos.id) videos.*')
.reorder('videos.id DESC')
},
through: :order_items_bought,
source: :item,
source_type: :Video do
def confirmed
where(orders: { state: :confirmed })
end
end
Now user.videos_bought.confirmed generates this SQL:
Video Load (5.4ms) SELECT DISTINCT ON (videos.id) videos.* FROM
"videos" INNER JOIN "order_items" ON
"videos"."id" = "order_items"."item_id" INNER JOIN orders ON
orders.id = order_items.order_id WHERE
"order_items"."buyer_id" = $1 AND ("orders"."state" != $2) AND
"order_items"."item_type" = $3 AND "orders"."state" = $4 ORDER BY
videos.id DESC, "order_items"."created_at" DESC LIMIT $5
Which seems more succinct because it avoids the auto generated order_items_videos_join name. It also only returns orders that have state confirmed.
Any idea what is going on? Does ActiveRecord just generate faulty SQL sometimes?
Using rails 5.1.5. Upgrading to latest made no difference.
I'm hoping to get an explanation on why Rails generates the order_items_videos_join string in the first case but not in the second case. Also, why the second SQL query produces incorrect results. I can edit the question with more code and data samples if needed.
ActiveRecord does not just generate faulty SQL sometimes, but there's a little nuance to it such that starting simple is best when it comes to defining relationships. For example, let's rework your queries to get that DISTINCT ON out of there. I've never seen a need to use that SQL clause.
Before chaining highly customized association logic, let's just see if there's simpler way to query first, and then check to see whether there's a strong case for turning your queries into associations.
Looks like you've got a schema like this:
User
id
Order
id
state
OrderItem
id
order_id
buyer_id (any reason this is on OrderItem and not on Order?)
item_type (Video)
item_id (videos.id)
Video
id
A couple of tidbits
No need to create association extensions for query conditions that would make perfectly good scopes on the model. See below.
A perfectly good query might look like
Video.joins(order_item: :order).
where(order_items: {
buyer_id: 123,
order: {
state: 'confirmed'
}).
# The following was part of the initial logic but
# doesn't alter the query results.
# where.not(order_items: {
# order: {state: 'expired'}
# }).
order('id desc')
Here's another way:
class User < ActiveRecord::Base
has_many :order_items, foreign_key: 'buyer_id'
def videos_purchased
Video.where(id: order_items.videos.confirmed.pluck(:id))
end
end
class OrderItem < ActiveRecord::Base
belongs_to :order, class_name: 'User', foreign_key: 'buyer_id'
belongs_to :item, polymorphic: true
scope :bought, -> {where.not(orders: {state: 'cancelled'})}
scope :videos, -> {where(item_type: 'Video')}
end
class Video < ActiveRecord::Base
has_many :order_items, ...
scope :confirmed, -> {where(orders: {state: 'confirmed'})}
end
...
user = User.first()
user.videos_purchased
I might have the syntax a little screwy when it comes to table and attribute names, but this should be a start.
Notice that I changed it from one to two queries. I suggest running with that until you really notice that you have a performance problem, and even then you might have an easier time just caching queries, than trying to optimize complex join logic.
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!
I'm trying to do some fairly complicated record sorting that I was having a bit of trouble with. I have three models:
class User < ActiveRecord::Base
has_many :registers
has_many :results, :through => :registers
#Find all the Users that exist as registrants for a tournament
scope :with_tournament_entrees, :include => :registers, :conditions => "registers.id IS NOT NULL"
end
Register
class Register < ActiveRecord::Base
belongs_to :user
has_many :results
end
Result
class Result < ActiveRecord::Base
belongs_to :register
end
Now on a Tournament result page I list all users by their total wins (wins is calculated through the results table). First thing first I find all users who have entered a tournament with the query:
User.with_tournament_entrees
With this I can simply loop through the returned users and query each individual record with the following to retrieve each users "Total Wins":
user.results.where("win = true").count()
However I would also like to take this a step further and order all of the users by their "Total Wins", and this is the best I could come up with:
User.with_tournament_entrees.select('SELECT *,
(SELECT count(*)
FROM results
INNER JOIN "registers"
ON "results"."register_id" = "registers"."id"
WHERE "registers"."user_id" = "users.id"
AND (win = true)
) AS total_wins
FROM users ORDER BY total_wins DESC')
I think it's close, but it doesn't actually order by the total_wins in descending order as I instruct it to. I'm using a PostgreSQL database.
Edit:
There's actually three selects taking place, the first occurs on User.with_tournament_entries which just performs a quick filter on the User table. If I ignore that and try
SELECT *, (SELECT count(*) FROM results INNER JOIN "registers" ON "results"."register_id" = "registers"."id" WHERE "registers"."user_id" = "users.id" AND (win = true)) AS total_wins FROM users ORDER BY total_wins DESC;
it fails in both PSQL and the ERB console. I get the error message:
PGError: ERROR: column "users.id" does not exist
I think this happens because the inner-select occurs before the outer-select so it doesn't have access to the user id before hand. Not sure how to give it access to all user ids before than inner select occurs but this isn't an issue when I do User.with_tournament_entires followed by the query.
In your SQL, "users.id" is quoted wrong -- it's telling Postgres to look for a column named, literally, "users.id".
It should be "users"."id", or, just users.id (you only need to quote it if you have a table/column name that conflicts with a postgres keyword, or have punctuation or something else unusual).
I'm trying join in Rails 3 for the first time, and can't get the most basic thing to work, so must be missing something major. Given
class Member < ActiveRecord::Base
belongs_to :status
...
end
class Status ActiveRecord::Base
has_many :members
...
end
when I try to use a join, testing in the console an example adapted from the Rails Guides
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Status.joins(:members).where('member.created_at' => time_range)
I get an error message,
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'member.created_at' in
'where clause': SELECT `statuses`.* FROM `statuses` INNER JOIN `members` ON
`members`.`status_id` = `statuses`.`id` WHERE (`member`.`created_at` BETWEEN
'2011-03-26 23:00:00' AND '2011-03-27 23:00:00')
Both models do have created_at columns and this works: Member.where(:created_at => time_range). I've tried several permutations and used different columns but get the same Unknown column error every time I try the join.
It should be members.created_at :) When you explicitly use the db table name, remember that it's in plural by rails convention.
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")}