Translating a (slightly complex) raw SQL query to ActiveRecord/Arel? - ruby-on-rails

I have a very simple Rails app with a very simple relational database: Category has many Samples. I'd simply like to load the categories that have X number of samples.
In plain SQL I would do something like this:
SELECT
categories.*
FROM
categories
JOIN
(SELECT
category_id, COUNT(*) as sample_count
FROM
samples
GROUP BY
category_id
) AS subselect
ON
categories.id=subselect.category_id
WHERE
subselect.sample_count = X; -- where X is whatever
That works just fine, by the way, but it's not terribly Rails-like to use raw SQL. And obviously I'd like to get those categories as model instances, so:
How would I go about re-writing something like that to an ActiveRecord or Arel query? Is it even feasible, or should I go with the plain SQL? Is there perhaps an altogether simpler way of doing it?

A possible nice way would be to use counter_cache, as described on this page: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Add a column named samples_count to your Category model:
add_column :categories, :samples_count, :integer
In your Sample model update belongs_to as follows:
belongs_to :category , :counter_cache => true
You can now use the count as a condition, for example:
Category.where(:samples_count => 7)

Related

Rails order results with multiple joins to same table

--Edit--
I wanted to simplify this question.
With this model structure:
has_one :pickup_job, class_name: 'Job', source: :job
has_one :dropoff_job, class_name: 'Job', source: :job
What I want to do is:
Package.joins(:dropoff_job, :pickup_job).order(pickup_job: {name: :desc})
This is obviously not valid, the actual syntax that should be used is:
.order('jobs.name desc')
However the way that rails joins the tables means that I cannot ensure what the table alias name will be (if anything), and this will order by dropoff_job.name instead of pickup_job.name
irb(main):005:0> Package.joins(:dropoff_job, :pickup_job).order('jobs.name desc').to_sql
=> "SELECT \"packages\".* FROM \"packages\" INNER JOIN \"jobs\" ON \"jobs\".\"id\" = \"packages\".\"dropoff_job_id\" INNER JOIN \"jobs\" \"pickup_jobs_packages\" ON \"pickup_jobs_packages\".\"id\" = \"packages\".\"pickup_job_id\" ORDER BY jobs.name desc"
Also I am not in control of how the tables are joined, so I cannot define the table alias.
--UPDATE--
I have had a play with trying to extract the alias names from the current scope using something like this:
current_scope.join_sources.select { |j| j.left.table_name == 'locations' }
But am still a little stuck and really feel like there should be a MUCH simpler solution.
--UPDATE--
pan's answer works in some scenarios but I am looking for a solution that is a bit more dynamic.
Use the concatenation of association name in plural and current table name as a table alias name in the order method:
Package.joins(:dropoff_job, :pickup_job).order('pickup_jobs_packages.name desc')
You can try something like this
Package.joins("INNER JOIN locations as dropoff_location ON dropoff_location.id = packages.dropoff_location_id INNER JOIN locations as pickup_location ON pickup_location.id = packages.pickup_location_id)
Idea is to create your own alias while joining the table

Mean SQL in Rails 4 without SQL using joins and where

A location belongs to one or more entities. An entity can have one or more locations.
I try to get all other locations that have the same entity like the current location.
I have the following model:
class Location < ActiveRecord::Base
has_many :location_assignments
has_many :entities, :through => :location_assignments
accepts_nested_attributes_for :location_assignments
end
class Entity < ActiveRecord::Base
has_many :location_assignments
has_many :locations, through: :location_assignments
accepts_nested_attributes_for :location_assignments
end
This is in SQL what I want
SELECT DISTINCT l.* FROM locations l, location_assignments la, entities e
WHERE l.id = la.location_id
AND la.entity_id = e.id
AND e.id in ( SELECT ee.id from entities ee, location_assignments laa
WHERE ee.id = laa.entity_id
AND laa.location_id = 1)
But I don't want to use SQL.
This is what I tried with Rails
Location.joins(:entities => :locations).where(:locations => {:id => location.id})
It gives me several times the current location. The amount of rows is the same like the SQL (without distinct to get the current location only ones).
Any thoughts?
One way is to mimic your SQL which uses a subquery and just use standard ActiveRecord querying without dropping down to AREL. Let's start with the subquery:
Entity.joins(:location_assignments).where(:location_assignments => {:location_id => location.id})
This returns a relation which contains all the entities which have the location represented by location.id associated with them, just like your SQL subquery.
Then the main query, with the subquery as xxxxx for now for readability:
Location.joins(:entities).where(:entities => {:id => xxxxx})
This is the equivalent of your main query. Plugging in the subquery which returns what is basically an array of entities (ok, a relation, but same effect in this case) prompts ActiveRecord to turn the WHERE condition into an IN rather than just an =. ActiveRecord is also smart enough to use the id of each entity. So, plug in the subquery:
Location.joins(:entities).where(:entities => {:id => Entity.joins(:location_assignments).where(:location_assignments => {:location_id => location.id})})
Note that like your query, this returns the original location you started with, as well as all the others which share the same entities.
I think this should be equivalent to your SQL, but see how it goes on your data!
If you want to make the query more efficient using a self join (which means that instead of having 2 joins and a subquery with another join, you can just have 2 joins) but without using SQL fragment strings, I think you might need to drop down to AREL something like this:
l = Arel::Table.new(:locations)
la = Arel::Table.new(:location_assignments)
starting_location_la = la.alias
l_joined_la = l.join(la).on(l[:id].eq(la[:location_id]))
filtered_and_projected = l_joined_la.join(starting_location_la).on(starting_location_la[:entity_id].eq(la[:entity_id])).where(starting_location_la[:location_id].eq(location.id)).project(location[Arel.star])
Location.find_by_sql(filtered_and_projected)
This just joins all locations to their location assignments and then joins again with the location assignments using entity id, but only with those belonging to your starting location object so it acts like a filter. This gives me the same results as the previous approach using standard ActiveRecord querying, but again, see how it goes on your data!

ActiveRecord query array intersection?

I'm trying to figure out the count of certain types of articles. I have a very inefficient query:
Article.where(status: 'Finished').select{|x| x.tags & Article::EXPERT_TAGS}.size
In my quest to be a better programmer, I'm wondering how to make this a faster query. tags is an array of strings in Article, and Article::EXPERT_TAGS is another array of strings. I want to find the intersection of the arrays, and get the resulting record count.
EDIT: Article::EXPERT_TAGS and article.tags are defined as Mongo arrays. These arrays hold strings, and I believe they are serialized strings. For example: Article.first.tags = ["Guest Writer", "News Article", "Press Release"]. Unfortunately this is not set up properly as a separate table of Tags.
2nd EDIT: I'm using MongoDB, so actually it is using a MongoWrapper like MongoMapper or mongoid, not ActiveRecord. This is an error on my part, sorry! Because of this error, it screws up the analysis of this question. Thanks PinnyM for pointing out the error!
Since you are using MongoDB, you could also consider a MongoDB-specific solution (aggregation framework) for the array intersection, so that you could get the database to do all the work before fetching the final result.
See this SO thread How to check if an array field is a part of another array in MongoDB?
Assuming that the entire tags list is stored in a single database field and that you want to keep it that way, I don't see much scope of improvement, since you need to get all the data into Ruby for processing.
However, there is one problem with your database query
Article.where(status: 'Finished')
# This translates into the following query
SELECT * FROM articles WHERE status = 'Finished'
Essentially, you are fetching all the columns whereas you only need the tags column for your process. So, you can use pluck like this:
Article.where(status: 'Finished').pluck(:tags)
# This translates into the following query
SELECT tags FROM articles WHERE status = 'Finished'
I answered a question regarding general intersection like queries in ActiveRecord here.
Extracted below:
The following is a general approach I use for constructing intersection like queries in ActiveRecord:
class Service < ActiveRecord::Base
belongs_to :person
def self.with_types(*types)
where(service_type: types)
end
end
class City < ActiveRecord::Base
has_and_belongs_to_many :services
has_many :people, inverse_of: :city
end
class Person < ActiveRecord::Base
belongs_to :city, inverse_of: :people
def self.with_cities(cities)
where(city_id: cities)
end
# intersection like query
def self.with_all_service_types(*types)
types.map { |t|
joins(:services).merge(Service.with_types t).select(:id)
}.reduce(scoped) { |scope, subquery|
scope.where(id: subquery)
}
end
end
Person.with_all_service_types(1, 2)
Person.with_all_service_types(1, 2).with_cities(City.where(name: 'Gold Coast'))
It will generate SQL of the form:
SELECT "people".*
FROM "people"
WHERE "people"."id" in (SELECT "people"."id" FROM ...)
AND "people"."id" in (SELECT ...)
AND ...
You can create as many subqueries as required with the above approach based on any conditions/joins etc so long as each subquery returns the id of a matching person in its result set.
Each subquery result set will be AND'ed together thus restricting the matching set to the intersection of all of the subqueries.

How to write complex query in Ruby

Need advice, how to write complex query in Ruby.
Query in PHP project:
$get_trustee = db_query("SELECT t.trustee_name,t.secret_key,t.trustee_status,t.created,t.user_id,ui.image from trustees t
left join users u on u.id = t.trustees_id
left join user_info ui on ui.user_id = t.trustees_id
WHERE t.user_id='$user_id' AND trustee_status ='pending'
group by secret_key
ORDER BY t.created DESC")
My guess in Ruby:
get_trustee = Trustee.find_by_sql('SELECT t.trustee_name, t.secret_key, t.trustee_status, t.created, t.user_id, ui.image FROM trustees t
LEFT JOIN users u ON u.id = t.trustees_id
LEFT JOIN user_info ui ON ui.user_id = t.trustees_id
WHERE t.user_id = ? AND
t.trustee_status = ?
GROUP BY secret_key
ORDER BY t.created DESC',
[user_id, 'pending'])
Option 1 (Okay)
Do you mean Ruby with ActiveRecord? Are you using ActiveRecord and/or Rails? #find_by_sql is a method that exists within ActiveRecord. Also it seems like the user table isn't really needed in this query, but maybe you left something out? Either way, I'll included it in my examples. This query would work if you haven't set up your relationships right:
users_trustees = Trustee.
select('trustees.*, ui.image').
joins('LEFT OUTER JOIN users u ON u.id = trustees.trustees_id').
joins('LEFT OUTER JOIN user_info ui ON ui.user_id = t.trustees_id').
where(user_id: user_id, trustee_status: 'pending').
order('t.created DESC')
Also, be aware of a few things with this solution:
I have not found a super elegant way to get the columns from the join tables out of the ActiveRecord objects that get returned. You can access them by users_trustees.each { |u| u['image'] }
This query isn't really THAT complex and ActiveRecord relationships make it much easier to understand and maintain.
I'm assuming you're using a legacy database and that's why your columns are named this way. If I'm wrong and you created these tables for this app, then your life would be much easier (and conventional) with your primary keys being called id and your timestamps being called created_at and updated_at.
Option 2 (Better)
If you set up your ActiveRecord relationships and classes properly, then this query is much easier:
class Trustee < ActiveRecord::Base
self.primary_key = 'trustees_id' # wouldn't be needed if the column was id
has_one :user
has_one :user_info
end
class User < ActiveRecord::Base
belongs_to :trustee, foreign_key: 'trustees_id' # relationship can also go the other way
end
class UserInfo < ActiveRecord::Base
self.table_name = 'user_info'
belongs_to :trustee
end
Your "query" can now be ActiveRecord goodness if performance isn't paramount. The Ruby convention is readability first, reorganizing code later if stuff starts to scale.
Let's say you want to get a trustee's image:
trustee = Trustee.where(trustees_id: 5).first
if trustee
image = trustee.user_info.image
..
end
Or if you want to get all trustee's images:
Trustee.all.collect { |t| t.user_info.try(:image) } # using a #try in case user_info is nil
Option 3 (Best)
It seems like trustee is just a special-case user of some sort. You can use STI if you don't mind restructuring you tables to simplify even further.
This is probably outside of the scope of this question so I'll just link you to the docs on this: http://api.rubyonrails.org/classes/ActiveRecord/Base.html see "Single Table Inheritance". Also see the article that they link to from Martin Fowler (http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html)
Resources
http://guides.rubyonrails.org/association_basics.html
http://guides.rubyonrails.org/active_record_querying.html
Yes, find_by_sql will work, you can try this also:
Trustee.connection.execute('...')
or for generic queries:
ActiveRecord::Base.connection.execute('...')

Join and select multiple column

I'm trying to select multiple columns after doing a join. I couldn't find a way to do so using ActiveRecord without writing SQL between quotation marks in the query (Thing I'd like to avoid)
Exploring Arel, I've found I could select multiple columns using "project", however I'm not quite sure if I should use Arel directly or if there was a way to achieve the same with AR.
These is the code in Arel:
l = Location.arel_table
r = Region.arel_table
postcode_name_with_region_code = l.where(l[:id].eq(location_id)).join(r).on(l[:region_id].eq(r[:id])).project(l[:postcode_name], r[:code])
After running this query I'd like to return something along the lines of:
(Pseudo-code)
"#{:postcode_name}, #{:code}"
Is there a way to achieve the same query using AR?
If I stick to Arel, how can I get the values out of the SelectManager class the above query returns.
Thanks in advance,
Using AR, without writing any SQL and assuming your models and associations are:
models/region.rb
class Region < ActiveRecord::Base
has_many :locations
end
model/location.rb
class Location < ActiveRecord::Base
belongs_to :region
end
You can certainly do:
postcode_name_with_region_code = Location.where(:id=>location_id).includes(:region).collect{|l| "#{l.postcode_name}, #{l.region.code}"}
This will do the query and then use Ruby to format your result (note that it will return an array since I'm assuming there could be multiple records returned). If you only want one item of the array, you can use the array.first method to reference it.
You could also eager load the association and build your string from the result:
my_loc = Location.find(location_id, :include=>:region)
postcode_name_with_region_code = "#{my_loc.postcode_name}, #{my_loc.region.code}"
predicate = Location.where(:id=>location_id).includes(:region)
predicate.ast.cores.first.projections.clear
predicate.project(Location.arel_table[:postcode_name], Region.arel_table[:code])

Resources