Query intersection with activerecord - mysql

I'd really like to do the following query with the help with active record
(select *
from people p join cities c join services s
where p.city_id = c.id and p.id = s.person_id and s.type = 1)
intersect
(select *
from people p join cities c join services s
where p.city_id = c.id and p.id = s.person_id and s.type = 2)
Problem is, first of all, mysql doesn't support intersect. However, that can be worked around of. The thing is that I can get active record to output anything even close to that.
In active record the best I could do was to issue multiple queries then use reduce :& to join them, but then I get an Array, not a Relation. That's a problem for me because I want to call things like limit, etc. Plus, I think it would be better to the intersection to be done by the database, rather than ruby code.

Your question is probably solvable without intersection, something like:
Person.joins(:services).where(services: {service_type: [1,2]}).group(
people: :id).having('COUNT("people"."id")=2')
However 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
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.
UPDATE
For those using AR4 where scoped was removed, my other answer provides a semantically equivalent scoped polyfil which all is not an equivalent replacement for despite what the AR documentation suggests. Answer here: With Rails 4, Model.scoped is deprecated but Model.all can't replace it

I was struggling with the same issue, and found only one solution: multiple joins against the same association. This may not be too rails-ish since I'm constructing the SQL string for the joins, but I haven't found another way. This will work for an arbitrary number of service types (cities doesn't seem to factor in, so that join was omitted for clarity):
s = [1,2]
j = ''
s.each_index {|i|
j += " INNER JOIN services s#{i} ON s.person_id = people.id AND s#{i}.type_id = #{s[i]}"
}
People.all.joins(j)

Related

How to use the distinct method in Rails with Arel Table?

I am looking to run the following query in Rails (I have used the scuttle.io site to convert my SQL to rails-friendly syntax):
Here is the original query:
SELECT pools.name AS "Pool Name", COUNT(DISTINCT stakings.user_id) AS "Total Number of Users Per Pool" from stakings
INNER JOIN pools ON stakings.pool_id = pools.id
INNER JOIN users ON users.id = stakings.user_id
INNER JOIN countries ON countries.code = users.country
WHERE countries.kyc_flow = 1
GROUP BY (pools.name);
And here is the scuttle.io query:
<%Staking.select(
[
Pool.arel_table[:name].as('Pool_Name'), Staking.arel_table[:user_id].count.as('Total_Number_of_Users_Per_Pool')
]
).where(Country.arel_table[:kyc_flow].eq(1)).joins(
Staking.arel_table.join(Pool.arel_table).on(
Staking.arel_table[:pool_id].eq(Pool.arel_table[:id])
).join_sources
).joins(
Staking.arel_table.join(User.arel_table).on(
User.arel_table[:id].eq(Staking.arel_table[:user_id])
).join_sources
).joins(
Staking.arel_table.join(Country.arel_table).on(
Country.arel_table[:code].eq(User.arel_table[:country])
).join_sources
).group(Pool.arel_table[:name]).each do |x|%>
<p><%=x.Pool_Name%><p>
<p><%=x.Total_Number_of_Users_Per_Pool%>
<%end%>
Now, as you may notice, sctuttle.io does not include the distinct parameter which I need. How in the world can I use distinct here without getting errors such as "method distinct does not exist for Arel Node?" or just syntax errors?
Is there any way to write the above query using rails ActiveRecord? I am sure there is, but I am really not sure how.
Answer
The Arel::Nodes::Count class (an Arel::Nodes::Function) accepts a boolean value for distinctness.
def initialize expr, distinct = false, aliaz = nil
super(expr, aliaz)
#distinct = distinct
end
The #count expression is a shortcut for the same and also accepts a single argument
def count distinct = false
Nodes::Count.new [self], distinct
end
So in your case you could use either of the below options
Arel::Nodes::Count.new([Staking.arel_table[:user_id]],true,'Total_Number_of_Users_Per_Pool')
# OR
Staking.arel_table[:user_id].count(true).as('Total_Number_of_Users_Per_Pool')
Suggestion 1:
The Arel you have seems a bit overkill. Given the natural relationships you should be able to simplify this a bit e.g.
country_table = Country.arel_table
Staking
.joins(:pools,:users)
.joins( Arel::Nodes::InnerJoin(
country_table,
country_table.create_on(country_table[:code].eq(User.arel_table[:country])))
.select(
Pool.arel_table[:name],
Staking.arel_table[:user_id].count(true).as('Total_Number_of_Users_Per_Pool')
)
.where(countries: {kyc_flow: 1})
.group(Pool.arel_table[:name])
Suggestion 2: Move this query to your controller. The view has no business making database calls.

Active Record Insert Into

I am trying to allow my Rails App some advanced functionality in way of creating relationships. I am running MySQL as the back-end. The query below runs just fine in PHPMyAdmin, but upon attempted execution in rails, it does nothing, no errors and no database writes....
I use active record to create the relationships between Campaign and Location, and then the query below should work to build afterward in campaign_metros table:
class CampaignLocation < ActiveRecord::Base
belongs_to :campaign
belongs_to :location
after_touch :create_campaign_metro
private
def create_campaign_metro
#sql_insert="INSERT INTO `campaign_metros` (`campaign_id`,`metro_id`, `created_at`,`updated_at`)
SELECT f.`campaign_id`, f.`city_id`, NOW(), NOW()
FROM (SELECT d.`campaign_id`, d.`city_id`
FROM (SELECT c.`campaign_id`, c.`city_id`
FROM (SELECT distinct a.`campaign_id`, b.`city_id`
FROM `campaign_locations` a
INNER JOIN `locations` b
ON a.`location_id` = b.`id`) c) d
LEFT JOIN `campaign_metros` e
ON e.`campaign_id` = d.`campaign_id` and
e.`metro_id` = d.`city_id`
WHERE e.`metro_id` is null and e.`campaign_id` is null) f;"
ActiveRecord::Base.connection.execute(#sql_insert)
end
end
I know from MySQL DELETE FROM with subquery as condition, in answer number two by CodeReaper, that MySQL requires another level of aliases when dealing with subqueries, but running my query in Rails does nothing at all.
How can I get this to work?

Complex Sql Query to RoR ORM

I have a very complex sql query that I would like to convert to RoR's ORM.
SELECT c.* FROM (SELECT companies.* FROM companies WHERE city = "?" AND country = "?") AS c INNER JOIN tagsForCompany AS tc ON c.id = tc.Company INNER JOIN tags AS t ON t.id = tc.TID WHERE t.Name REGEXP '?'
I have defined the models like this:
companies.rb
class Company < ActiveRecord::Base
# ... Some code that doesn't matter
has_and_belongs_to_many :tags
# ... Some other code
end
and tags.rb
class Tag < ActiveRecord::Base
has_and_belongs_to_many :company
end
I need a function in the companies controller that searches for the companies like the query above.
Options:
First: find_by_sql()
Description: Allows you to put any query you want on it.
http://apidock.com/rails/ActiveRecord/Base/find_by_sql/class
Second: Combination of .where() and .joins() method.
But be careful, if you call .joins() after a .where() with a nil return, you will get an error of undefined method. The solution here would be to first test if .where() returned anything, then you can join with another table.
Possible ways to use joins:
joins(:tags) Creates a Inner Join
joins('Left join foo...') Enables you to use left outter joins
joins(tagsForCompanies: :tags) Nested Joins if you have N to N associations.
See API:
http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-where
http://guides.rubyonrails.org/active_record_querying.html#joining-tables

Ruby on rails mysql searches

I am totally new to Ruby on Rails and I am trying to search through some relational database tables, I am trying to search for a given ID number in a Customer table then from the results look at who the sales_rep for that customer is. With this
#salesrepcust = Customer.find(:all, :conditions => ["id = ?",#data])
I am able to get back the correct customer given there ID number but I dont see how in ruby on rails to then pull from those results just one column value, in this it would be the value for sales_rep, and then use that as my #result for
#salesrepcustdata = Salesrep.find(:all, :conditions => ["id = ?", #result])
I have searched for this but i guess im not wording it correctly because i am not able to find anything specifically on this, can anyone help?
It's pretty straightforward to select a single column; you can try something like this:
#salesrepcustids = Customer.where(id: #data).select(:id)
This will generate a SELECT id FROM ... statement.
And now you can do this:
#salesrepcustdata = Salesrep.where(id: #salesrepcustids)
This will generate an SELECT...IN statement with those ids.
(You might find it easier to set up proper ActiveRecord has_many and belongs_to relationships in your models, or whatever relationship is appropriate.)
Assuming the sales rep is represented in the Customer table as sales_rep_id you can just do:
Salesrep.find(Customer.find(#data).sales_rep_id)
The find method assumes you're looking for id and if there's just one item with that id, there's no need to specify :all.
This is all discussed in http://guides.rubyonrails.org/active_record_querying.html
That Customer query could be simplified to just:
#customer = Customer.find(#data)
You don't mention if you've setup a relationship between Customer and Salesrep but here goes:
# app/models/customer.rb
class Customer < ActiveRecord::Base
belongs_to :salesrep, class_name: 'Salesrep' # => the customers table should have a salesrep_id column
end
# app/models/salesrep.rb
class Salesrep < ActiveRecord::Base
has_many :customers
end
customer_id = 1
#customer = Customer.find(customer_id)
#salesrep = #customer.salesrep
# from the other way, assuming you need both the salesrep and customer:
salesrep_id = 10
#salesrep = Salesrep.find(salesrep_id)
# the following will only find the customer if it's owned by the Salesrep
#customer = #salesrep.customers.find(customer_id)

Sort based on last has_many record in Rails

I have a Student model and a Gpa model. Student has_many Gpa. How would I sort students based on their most recently created gpa record's value attribute?
NOTE: I don't want to sort an individual student's GPAs based on the created date. I would like to pull ALL students and sort them based on their most recent GPA record
class Student < ActiveRecord::Base
has_many :gpas
end
#students = Student.order(...)
assuming the gpas timestamp is updated_at
Student.joins(:gpas).order('gpas.updated_at DESC').uniq
To include students without gpas
#references is rails 4; works in rails 3 without it
Student.includes(:gpas).order('gpas.updated_at DESC').references(:gpas).uniq
if you dont like the distinct that uniq creates, you can use some raw sql
Student.find_by_sql("SELECT students.* FROM students
INNER JOIN gpas ON gpas.student_id = students.id
LEFT OUTER JOIN gpas AS future ON future.student_id = gpas.student_id
AND future.updated_at > gpas.updated_at
WHERE future.id IS NULL ORDER BY gpas.updated_at DESC")
# or some pretty raw arel
gpa_table = Gpa.arel_table
on = Arel::Nodes::On.new(
Arel::Nodes::Equality.new(gpa_table[:student_id], Student.arel_table[:id])
)
inner_join = Arel::Nodes::InnerJoin.new(gpa_table, on)
future_gpa_table = Gpa.arel_table.alias("future")
on = Arel::Nodes::On.new(
Arel::Nodes::Equality.new(future_gpa_table[:student_id], gpa_table[:student_id]).\
and(future_gpa_table[:updated_at].gt(gpa_table[:updated_at])
)
)
outer_join = Arel::Nodes::OuterJoin.new(future_gpa_table, on)
# get results
Student.joins(inner_join).joins(outer_join).where("future.id IS NULL").\
order('gpas.updated_at DESC')
I'm not sure that there's a way of achieving this in any kind of convenient and mostly-ruby way. The SQL required for an efficient implementation probably requires an order based on join -- something like ...
select
...
from
students
order by
( select gpas.value
from gpas
where gpas.student_id = student.id
order by gpas.as_of_date desc
limit 1)
I'm not sure if that's legal in MySQL, but if it is you could probably just:
Student.order("(select gpas.value from gpas where gpas.student_id = student.id order by gpas.as_of_date desc limit 1)")
On the other hand, it seems like the last value would be an important one, so you might like to implement a callback on gpas to set a "last_gpa_id" or "last_gpa_value" in the students table to make this common join more efficient.
Then of course the implementation would be trivial.
#students = Student.includes(:gpas).order('gpas.value DESC')
Still it's important to note that this will include Students, who has got no gpas. But you can filter that easly out with #students.delete_if{ |s| s.gpas.blank? }
Probably something like
Student.joins(:gpas).order("gpas.value DESC")
You could try adding some options to the relationships in your models.
Something like:
class Student < ActiveRecord::Base
has_many :gpas, order: "value DESC", conditions: "foo = bar" #<-whatever conditions you want here
end
class Gpa < ActiveRecord::Base
belongs_to :student
end
Using options, all you have to do make a call and let Rails do most of the heavy lifting.
If you get stumped, there are several more options here: http://guides.rubyonrails.org/association_basics.html
Just do a keyword search on the page for "The has_and_belongs_to_many association supports these options:"
This should work for you.
Try this SQL query
SELECT * FROM students WHERE id IN
(SELECT student_id
FROM gpa
GROUP BY student_id
ORDER BY created_at DESC);
I think, you could try this method:
class Student < ActiveRecord::Base
attr_accessible :name
has_many :gpas
def self.by_last_gpas
sql = <<-SQL
select students.*,
(
select gpas.created_at from gpas where student_id=students.id
order by gpas.created_at desc
limit 1
) as last_created_at
from students
order by last_created_at desc
SQL
Student.find_by_sql(sql)
end
end
Quick and dirty:
You can somehow implement a query like this to fetch AR objects you need:
select s.* from students s, gpas g
where s.id = gpas.student_id
and gpas.id in (select max(id) from gpas group by student_id)
order by gpas.value
Assuming that id is higher for records with higher created_at.
OR
Nicer way:
I assume, you'll need student's last GPA score very often. Why not add a new column :last_gpa_score to Student model?
You can use callback to keep this field consistent and autofilled, i. e.:
class Gpa < ActiveRecord::Base
belongs_to :student
after_save :update_student_last_score
private
def update_student_last_score
self.student.update_last_gpa_score
end
end
class Student < ActiveRecord::Base
has_many :gpas
def update_last_gpa_score
self.last_gpa_score = self.gpas.order("created_at DESC").first.value
end
end
Then you can do whatever you like with last_gpa_score field on student.
You'll have to adjust the table names and column names to match your DB.
SELECT s.*, g.*
FROM (SELECT studentId, MAX(gpaDate) as gpaDate FROM gpas GROUP BY studentId) maxgpa
JOIN gpas g
ON g.studentid = maxgpa.studentId
AND g.gpaDate = maxgpa.gpaDate
JOIN students s
ON s.studentId = g.studentId
ORDER g.gpaDate DESC
Create a last_gpa method to get the last GPA created and then perform a standard Ruby sort.
def last_gpa
a.gpas.order(:order=>"created_at DESC").first
end
Student.all.sort { |a, b| a.last_gpa <=> b.last_gpa }