Rails 3.2 and getting AR to properly order this query - sql

I currently have a scope where I am attempting to find last record created in an association and select it if a particular boolean value is false
IE Foo has_many Bar's and Bar's has a boolean column named bazzed
scope :no_baz, joins(:bars).order("bars.id DESC").limit(1).where("bars.bazzed = 'f'")
The problem with this is that rails turns this query into something like this
SELECT "foos".* FROM "foos" INNER JOIN "bars" ON "bars"."foo_id" = "foos"."id" WHERE (bars.bazzed = 'f') ORDER BY bars.id DESC LIMIT 1
the problem lies that rails is calling the order and limit after the where clause, what i'm looking for is to do the order and limit first to try and find the last bar that has bazzed set to false.
Is there a native AR way to perform the query I am attempting to accomplish?
EDIT
I am trying to grab the foo's that have a bar where the last bar they have has bazzed set to false and only if the last bar that that foo has has a false bazzed.

Ok, I would suggest this for the query on the "foo" model:
Foo.bars.where("bars.bazzed = ?", 'f').all( :order => "created_at DESC").first
Note: 'f' can be replaced by false, depending on the value you use in your "bazzed" column, of course.
[Edit]
Ok, as I think I better understand the problem, here is a suggestion, but for a public method and not a scoped query.
def no_baz
all_no_baz_foos = Array.new
Foo.all.each do |foo|
last_bar = foo.bars.all.order("bars.id DESC").first
if last_bar.bazzed == 'f'
all_no_baz_foos << foo
end
end
return all_no_baz_foos
end
This method will return an Array with all the no_baz_foos record in it. As I did not test my code, you may have to change few things for it to work, but I think you get the idea.
For the "scope" method, I just can't find a way to chain correctly the queries to have the desired result. If anyone else knows how to achieve that using a scope, I'll be glad to hear the solution too.

Using a class method for now but the problem with that lies that it returns an array object and not an active record relation which is what i'm trying to return. Still attempting to get the query correctly done.

Related

How to select each model which has the maximum value of an attribute for any given value of another attribute?

I have a Work model with a video_id, a user_id and some other simple fields. I need to display the last 12 works on the page, but only take 1 per user. Currently I'm trying to do it like this:
def self.latest_works_one_per_user(video_id=nil)
scope = self.includes(:user, :video)
scope = video_id ? scope.where(video_id: video_id) : scope.where.not(video_id: nil)
scope = scope.order(created_at: :desc)
user_ids = works = []
scope.each do |work|
next if user_ids.include? work.user_id
user_ids << work.user_id
works << work
break if works.size == 12
end
works
end
But I'm damn sure there is a more elegant and faster way of doing it especially when the number of works gets bigger.
Here's a solution that should work for any SQL database with minimal adjustment. Whether one thinks it's elegant or not depends on how much you enjoy SQL.
def self.latest_works_one_per_user(video_id=nil)
scope = includes(:user, :video)
scope = video_id ? scope.where(video_id: video_id) : scope.where.not(video_id: nil)
scope.
joins("join (select user_id, max(created_at) created_at
from works group by created at) most_recent
on works.user_id = most_recent.user_id and
works.created_at = most_recent.created_at").
order(created_at: :desc).limit(12)
end
It only works if the combination of user_id and created_at is unique, however. If that combination isn't unique you'll get more than 12 rows.
It can be done more simply in MySQL. The MySQL solution doesn't work in Postgres, and I don't know a better solution in Postgres, although I'm sure there is one.

ZF2 how to avoid sql query limit to add quotes in subquery

I'm trying to set up a subquery in ZendFramework 2 and I got an issue with the limit function for a Select object. Whatever I do, numeric value is put between quotes and makes my query fails : I should get LIMIT 1 and instead I get LIMIT '1'.
Seems this is not the first time this issue has been encountered, I saw some have asked about this issue before (like 8 months ago) but without getting any proper answer.
I also saw this issue has been marker as resolved in 2012 (https://github.com/zendframework/zf2/pull/2775) so I really don't understand what's happening there.
Here's my code in ZF2 :
$resultSet = $this->tableGateway->select( function (Select $select) use ($params) {
$sub = new Select();
$sub->from(array('temp' => 'scores'))
->columns(array(new \Zend\Db\Sql\Expression("id AS id")))
->where(array('temp.glitch' => array('None', 'Glitch')))
->where('temp.zone=scores.zone')
->order('temp.multi DESC, temp.score DESC')
->limit(1);
$select->join('players', 'player=players.id', array('player_name' => 'name', 'player_url' => 'name_url'))
->join('countries', 'players.country=countries.id', array('country_name' => 'name', 'country_iso' => 'iso'))
->join('cars', 'car=cars.id', array('car_name' => 'name'), 'left')
->join('zones', 'zone=zones.id', array('zone_name' => 'name'));
$select->where(array('scores.id' => $sub));
$select->order('scores.zone ASC');
print_r($select->getSqlString());
});
This should render the following query (which I get right except LIMIT '1' instead of LIMIT 1) :
SELECT "scores".*, "players"."name" AS "player_name", "players"."name_url" AS "player_url", "countries"."name" AS "country_name", "countries"."iso" AS "country_iso", "cars"."name" AS "car_name", "zones"."name" AS "zone_name"
FROM "scores" INNER JOIN "players" ON "player"="players"."id"
INNER JOIN "countries" ON "players"."country"="countries"."id"
LEFT JOIN "cars" ON "car"="cars"."id"
INNER JOIN "zones" ON "zone"="zones"."id"
WHERE "scores"."id" = (SELECT id AS id FROM "scores" AS "temp" WHERE "temp"."glitch" IN ('None', 'Glitch')
AND temp.zone=scores.zone ORDER BY "temp"."multi" DESC, "temp"."score" DESC LIMIT 1)
ORDER BY "scores"."zone" ASC
Since this doesn't seem to work this way, is there another way I could proceed to get my limit (using Mysql 5 database) ?
EDIT :
Thanks for your help. Finally I figured out a way to get things done the way I want and to remove the quotes by simply remove the subquery construction and to write it directly in the where function :
$select->where('scores.id = (SELECT id FROM scores AS lookup WHERE lookup.zone = scores.zone ORDER BY multi DESC , score DESC LIMIT 1)');
Although I can continue my dev with this, I feel more like using a poor trick to get rid of this issue and so I will let this question unanswered until someone comes with a real solution there.
Anyway there might be no solution at all, since it might be an issue in ZF2 core itself.
Change the line -
$select->where(array('scores.id' => $sub));
with
$select->where(array('scores.id' => new \Zend\Db\Sql\Expression("({$sub->getSqlString($this->tableGateway->adapter->getPlatform())})"));
Try with just above change.
And if it still doesn't work then make changes to the core Select class file located at -
PROJECT_FOLDER/vendor/zendframework/zendframework/library/Zend/Db/Sql/Select.php
Line No. 921 -
Change $sql = $platform->quoteValue($limit); with $sql = $limit;
Line No. 940 -
Change return array($platform->quoteValue($offset)); with return array($offset);
I have come across the issue from github and wondered as why it is still not working with the latest ZF2 files. I know the solution given above doesn't look like the proper one but I had to somehow make it work. I have tried it and it works.
Its only a quick fix before the actual solution comes into picture.

Scope with association and ActiveRecord

I have an app that records calls. Each call can have multiple units associated with it. Part of my app has a reports section which basically just does a query on the Call model for different criteria. I've figured out how to write some scopes that do what I want and chain them to the results of my reporting search functionality. But I can't figure out how to search by "unit". Below are relevant excerpts from my code:
Call.rb
has_many :call_units
has_many :units, through: :call_units
#Report search logic
def self.report(search)
search ||= { type: "all" }
# Determine which scope to search by
results = case search[:type]
when "open"
open_status
when "canceled"
cancel
when "closed"
closed
when "waitreturn"
waitreturn
when "wheelchair"
wheelchair
else
scoped
end
#Search results by unit name, this is what I need help with. Scope or express otherwise?
results = results. ??????
results = results.by_service_level(search[:service_level]) if search[:service_level].present?
results = results.from_facility(search[:transferred_from]) if search[:transferred_from].present?
results = results.to_facility(search[:transferred_to]) if search[:transferred_to].present?
# If searching with BOTH a start and end date
if search[:start_date].present? && search[:end_date].present?
results = results.search_between(Date.parse(search[:start_date]), Date.parse(search[:end_date]))
# If search with any other date parameters (including none)
else
results = results.search_by_start_date(Date.parse(search[:start_date])) if search[:start_date].present?
results = results.search_by_end_date(Date.parse(search[:end_date])) if search[:end_date].present?
end
results
end
Since I have an association for units already, I'm not sure if I need to make a scope for units somehow or express the results somehow in the results variable in my search logic.
Basically, you want a scope that uses a join so you can use a where criteria in against the associated model? Is that correct?
So in SQL you're looking for something like
select * from results r
inner join call_units c on c.result_id = r.id
inner join units u on u.call_unit_id = c.id
where u.name = ?
and the scope would be (from memory, I haven't debugged this) something like:
scope :by_unit_name, lambda {|unit_name|
joins(:units).where('units.unit_name = ?', unit_name)
}
units.name isn't a column in the db. Changing it to units.unit_name didn't raise an exception and seems to be what I want. Here's what I have in my results variable:
results = results.by_unit_name(search[:unit_name]) if search[:unit_name].present?
When I try to search by a different unit name no results show up. Here's the code I'm using to search:
<%= select_tag "search[unit_name]", options_from_collection_for_select(Unit.order(:unit_name), :unit_name, :unit_name, selected: params[:search].try(:[], :unit_name)), prompt: "Any Unit" %>

ActiveRecord condition with count less than for association

I have a User that has_many messages.
I need a create a query that will
'Get me all users who's (message.opened == false) count < 3'
Right now, I am using User.all, iterating through all users, and counting manually. I understand that this isn't very efficient and it can be all done in one query, but I am new to SQL/ActiveRecord so need some help here.
Thanks
Assuming Rails 3 syntax. You can do something like:
User.joins(:messages).where(:messages => {:opened => false}).group(:user_id).having("COUNT(messages.id) < 3)
This should work:
User.includes(:messages).group("users.id").where("messages.opened = 0").having("count(messages.id) < 3")
This will create two queries, one for the grouped query, and one for eager loading the resulting users and messages with a join.
Here is solution to your problem
User.includes(:messages).group("users.id").where("messages.opened = 0").having("count(messages.id) < 3")
but what else you can do is to create a scope for this
scope :not_opened_less_three_count, includes(:messages).group("users.id").where("messages.opened = 0").having("count(messages.id) < 3")
And then you can use it anywhere you needed as follow
User.not_opened_less_three_count
Try this
User.includes(:messages).group('users.id').having('SUM(IFNULL(messages.opened = 0, 1)) < 3')
It works at least on MySQL, AND assuming your boolean true are 1 in database.
EDIT I had reversed the condition
PS IFNULL is there to handle if messages.opened can be NULL

Making a Safe sql query

The code below is from a Sinatra app (that uses DataMappe), which I am trying to convert to a Rails 3 application. It is a class method in the Visit class.
def self.count_by_date_with(identifier,num_of_days)
visits = repository(:default).adapter.query("SELECT date(created_at) as date, count(*) as count FROM visits where link_identifier = '#{identifier}' and created_at between CURRENT_DATE-#{num_of_days} and CURRENT_DATE+1 group by date(created_at)")
dates = (Date.today-num_of_days..Date.today)
results = {}
dates.each { |date|
visits.each { |visit| results[date] = visit.count if visit.date == date }
results[date] = 0 unless results[date]
}
results.sort.reverse
end
My problem is with this part
visits = repository(:default).adapter.query("SELECT date(created_at) as date, count(*) as count FROM visits where link_identifier = '#{identifier}' and created_at between CURRENT_DATE-#{num_of_days} and CURRENT_DATE+1 group by date(created_at)")
Rails (as far as I know) doesn't have this repository method, and I would expect a query to be called on an object of some sort, such as Visit.find
Can anyone give me a hint how this would best be written for a Rails app?
Should I do
Visit.find_by_sql("SELECT date(created_at) as date, count(*) as count FROM visits where link_identifier = '#{identifier}' and created_at between CURRENT_DATE-#{num_of_days} and CURRENT_DATE+1 group by date(created_at)")
Model.connection.execute "YOUR SQL" should help you. Something like
class Visit < Activerecord::Base
class << self
def trigger(created_at,identifier,num_of_days)
sql = "SELECT date(created_at) as date, count(*) as count FROM visits where link_identifier = '#{identifier}' and created_at between CURRENT_DATE-#{num_of_days} and CURRENT_DATE+1 group by date(created_at)"
connection.execute sql
end
end
end
I know you already accepted an answer, but you asked for the best way to do what you asked in Rails. I'm providing this answer because Rails does not recommend building conditions as pure query strings.
Building your own conditions as pure strings can leave you vulnerable to SQL injection exploits. For example, Client.where("first_name LIKE '%#{params[:first_name]}%'") is not safe.
Fortunately, Active Record is incredibly powerful and can build very complex queries. For instance, your query can be recreated with four method calls while still being easy to read and safe.
# will return a hash with the structure
# {"__DATE__" => __COUNT__, ...}
def self.count_by_date_with(identifier, num_of_days)
where("link_identifier = ?", identifier)
.where(:created_at => (num_of_days.to_i.days.ago)..(1.day.from_now))
.group('date(created_at)')
.count
end
Active Record has been built to turn Ruby objects into valid SQL selectors and operators. What makes this so cool is that Rails can turn a Ruby Range into a BETWEEN operator or an Array into an IN expression.
For more information on Active Record check out the guide. It explains what Active Record is capable of and how to use it.