Custom select on join table - sql

I'm using rails 3.2, and trying to use ActiveRecord to query my database.
I have 2 activerecord models, Admin and Order:
class Admin < ActiveRecord::Base
attr_accessible :email, :name
has_many :orders
class Order < ActiveRecord::Base
attr_accessible :operation
belongs_to :admin
In my case, order.operation is a string, representing order type. I'm trying to build a query giving me three columns:
admin.name, admin.orders.where(:operation => 'bonus').count, admin.orders.where(:operation => 'gift').count
Is there a way to fit it in a single query?
EDITED:
here's raw sql to get what I need:
SELECT t_a.admin_id_com, t_a.name, gb_f_f_gb_f_fi.bonus_count, gb_f_f_gb_f_fi.gifts_count
FROM (
SELECT f_fil.admin_id_gifts, f_fil.gifts_count, f_filt.admin_id_bonus, f_filt.bonus_count
FROM (
SELECT admin_id as admin_id_gifts, count(distinct id) as gifts_count FROM orders WHERE operation = 'new gifts!' GROUP BY admin_id_gifts)
f_fil LEFT OUTER JOIN (
SELECT admin_id as admin_id_bonus, count(distinct id) as bonus_count FROM orders WHERE operation = 'new bonuses!' GROUP BY admin_id_bonus)
f_filt ON (f_fil.admin_id_gifts = f_filt.admin_id_bonus))
gb_f_f_gb_f_fi LEFT OUTER JOIN (
SELECT id AS admin_id_com, t_ad.name FROM admins t_ad) t_a ON (gb_f_f_gb_f_fi.admin_id_gifts = t_a.admin_id_com)
Is it possible to buid a query like that using ActiveRecord?

Try this:
#admins = Admin.joins(:orders).
select("admins.id, admins.name, orders.id,
SUM((orders.operation = 'bonus')::integer) AS bonus_count,
SUM((orders.operation = 'gift')::integer) AS gift_count ").
group("admins.id ")
# access each columns as
admin.name, admin.bonus_count, admin.gift_count
Other option is to use eager loading, it will use two queries but might be faster
#admins = Admin.includes(:orders)
# in admin.rb
def orders_count(type)
# Don't use where here as it would make a separate query instead of using eager loading records
orders.select{|x| x.operation == type}.count
end
# access each columns as
admin.name, admin.orders_count("bonus"), admin.orders_count("gift")

Related

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)

globalize3 - Query translated attribute

I have 2 models:
class Gender < ActiveRecord::Base
translates :name
has_many :products
end
class Product < ActiveRecord::Base
translates :description
belongs_to :gender
end
After integrating with globalize3 I cannot figure out how to get query that joins to work, for example:
Product.joins(:gender).where(genders: { name: 'male' })
which generates this sql query:
SELECT "products".* FROM "products"
INNER JOIN "genders" ON "genders"."id" = "products"."gender_id"
WHERE "genders"."name" = 'male'`
But I think I need a sql query that looks like this?
SELECT * FROM products
INNER JOIN genders on genders.id = products.gender_id
INNER JOIN gender_translations on gender_translations.gender_id = genders.id
WHERE gender_translations.name = 'male';
So how does one do the rails equivalent of this sql query?
Something along the lines of Product.joins(gender: :translations).where(gender: { translations: { name: 'male' }}) should do the trick I believe.
The joins(gender: :translations) is a nested inner join. So you're joining products-> genders, and genders -> gender_translations.
The where hash syntax is just an attempt to generate the SQL: where("gender_translations.prompt = 'male'"). If the hash syntax isn't correct/fights you, I'd just revert to the raw SQL just mentioned. It's arguably more clear anyways!

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!

Rails 3 query matching attribute of has_one association that is a subset of has_many association

The title is confusing, but allow me to explain. I have a Car model that has multiple datapoints with different timestamps. We are almost always concerned with attributes of its latest status. So the model has_many statuses, along with a has_one to easily access it's latest one:
class Car < ActiveRecord::Base
has_many :statuses, class_name: 'CarStatus', order: "timestamp DESC"
has_one :latest_status, class_name: 'CarStatus', order: "timestamp DESC"
delegate :location, :timestamp, to: 'latest_status', prefix: 'latest', allow_nil: true
# ...
end
To give you an idea of what the statuses hold:
loc = Car.first.latest_location # Location object (id = 1 for example)
loc.name # "Miami, FL"
Let's say I wanted to have a (chainable) scope to find all cars with a latest location id of 1. Currently I have a sort of complex method:
# car.rb
def self.by_location_id(id)
ids = []
find_each(include: :latest_status) do |car|
ids << car.id if car.latest_status.try(:location_id) == id.to_i
end
where("id in (?)", ids)
end
There may be a quicker way to do this using SQL, but not sure how to only get the latest status for each car. There may be many status records with a location_id of 1, but if that's not the latest location for its car, it should not be included.
To make it harder... let's add another level and be able to scope by location name. I have this method, preloading statuses along with their location objects to be able to access the name:
def by_location_name(loc)
ids = []
find_each(include: {latest_status: :location}) do |car|
ids << car.id if car.latest_location.try(:name) =~ /#{loc}/i
end
where("id in (?)", ids)
end
This will match the location above with "miami", "fl", "MIA", etc... Does anyone have any suggestions on how I can make this more succinct/efficient? Would it be better to define my associations differently? Or maybe it will take some SQL ninja skills, which I admittedly don't have.
Using Postgres 9.1 (hosted on Heroku cedar stack)
All right. Since you're using postgres 9.1 like I am, I'll take a shot at this. Tackling the first problem first (scope to filter by location of last status):
This solution takes advantage of PostGres's support for analytic functions, as described here: http://explainextended.com/2009/11/26/postgresql-selecting-records-holding-group-wise-maximum/
I think the following gives you part of what you need (replace/interpolate the location id you're interested in for the '?', naturally):
select *
from (
select cars.id as car_id, statuses.id as status_id, statuses.location_id, statuses.created_at, row_number() over (partition by statuses.id order by statuses.created_at) as rn
from cars join statuses on cars.id = statuses.car_id
) q
where rn = 1 and location_id = ?
This query will return car_id, status_id, location_id, and a timestamp (called created_at by default, although you could alias it if some other name is easier to work with).
Now to convince Rails to return results based on this. Because you'll probably want to use eager loading with this, find_by_sql is pretty much out. There is a trick I discovered though, using .joins to join to a subquery. Here's approximately what it might look like:
def self.by_location(loc)
joins(
self.escape_sql('join (
select *
from (
select cars.id as car_id, statuses.id as status_id, statuses.location_id, statuses.created_at, row_number() over (partition by statuses.id order by statuses.created_at) as rn
from cars join statuses on cars.id = statuses.car_id
) q
where rn = 1 and location_id = ?
) as subquery on subquery.car_id = cars.id order by subquery.created_at desc', loc)
)
end
Join will act as a filter, giving you only the Car objects that were involved in the subquery.
Note: In order to refer to escape_sql as I do above, you'll need to modify ActiveRecord::Base slightly. I do this by adding this to an initializer in the app (which I place in app/config/initializers/active_record.rb):
class ActiveRecord::Base
def self.escape_sql(clause, *rest)
self.send(:sanitize_sql_array, rest.empty? ? clause : ([clause] + rest))
end
end
This allows you to call .escape_sql on any of your models that are based on AR::B. I find this profoundly useful, but if you've got some other way to sanitize sql, feel free to use that instead.
For the second part of the question - unless there are multiple locations with the same name, I'd just do a Location.find_by_name to turn it into an id to pass into the above. Basically this:
def self.by_location_name(name)
loc = Location.find_by_name(name)
by_location(loc)
end

Rails 3 Order Records By Grand-child Count

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).