Order a model with conditions in Rails (3) - ruby-on-rails

Lets say there is a model called Event. I want to display important and current events first, so i have the attributes important (boolean) and enddate (date).
Now I want to get all events where important == true and where enddate >= today first, all others should be ordered by created_at.
I want to avoid doing events = important_events + not_important_events as this would return an array insted of an activerecord. Does anyone knows an elegant way to order a model in rails?

Try (for ActiveRecord 5+):
#events = Event.where(:important => true, 'enddate >= ?', Date.today).or(Event.where.not(:important => true, 'enddate >= ?', Date.today).order(:created_at => :desc))

Try this, It will return newest record first and fulfil your conditions.
important == true
enddate >= today
#events1 = Event.where(:important => true).where('enddate >= ?', Date.today).order(:created_at => :desc)
#events2 = Event.where(:important => false).where('enddate <= ?', Date.today).order(:created_at => :desc)
#events = #events1.or(#events2)
"OR" works only in ActiveRecod(5+).

Related

Query on fetched activerecord data

I want to know that is it possible to make a query on fetched data from a model, without creating a database query.
Let there be a User model having type A and B
#users = User.where('created_at > ?', Time.now - 1.day)
type_a = #users.where(:type => A).limit(100)
type_b = #users.where(:type => B).limit(10)
Currently, it is executing 2 queries. But I want to run a single query to fetch data, and then extract type_a and type_b from it.
Is there any other method to do that.
Possible by select method
#users = User.where('created_at > ?', Time.now - 1.day)
type_a = #users.select {|u| u.type == 'A'}.first(100)
type_b = #users.select {|u| u.type == 'B'}.first(10)

Better solution for "one, other or both" cases

I was checking some code, and something similar to the following showed up:
def between_dates(date_1, date_2)
if date_1 && date_2
conditions "created_at >= date_1 AND created_at <= date_2"
elseif date_1
conditions "created_at >= date_1"
elseif date_2
conditions "created_at <= date_2"
end
end
It looked the kind of code that could be improved, but I couldn't find a more elegant solution for such a trivial and common conditional statement.
I'm looking for a better answer for this problem when we must return a value for one, other or both.
Rails lets you build a query dynamically. Here's an example using scopes and a class method. Since scopes always return an ActiveRecord::Relation object (even if the block returns nil), they are chainable:
class Event < ApplicationRecord
scope :created_before, -> (date) { where('created_at <= ?', date) if date }
scope :created_after, -> (date) { where('created_at >= ?', date) if date }
def self.created_between(date_1, date_2)
created_after(date_1).created_before(date_2)
end
end
Example usage:
Event.created_between(nil, Date.today)
# SELECT `events`.* FROM `events` WHERE (created_at <= '2018-05-15')
Event.created_between(Date.yesterday, nil)
# SELECT `events`.* FROM `events` WHERE (created_at >= '2018-05-14')
Event.created_between(Date.yesterday, Date.today)
# SELECT `events`.* FROM `events` WHERE (created_at >= '2018-05-14') AND (created_at <= '2018-05-15')
I'd use something like this:
def between_dates(date_1, date_2)
parts = []
if date_1
parts << "created_at >= date_1"
end
if date_2
parts << "created_at <= date_2"
end
full = parts.join(' AND ')
conditions(full)
end
This can be further prettified in many ways, but you get the idea.
def between_dates(date_1, date_2)
date_conditions = []
date_conditions << 'created_at >= date_1' if date_1
date_conditions << 'created_at <= date_2' if date_2
conditions date_conditions.join(' AND ') unless date_conditions.empty?
end
I am not sure if this is more elegant, but I always do reduce everything to avoid typos:
[[date_1, '>='], [date_2, '<=']].
select(&:first).
map { |date, sign| "created_at #{sign} #{date}" }.
join(' AND ')

How to combine ActiveRecord Relations? Merge not working?

Initially when I was trying to build a histogram of all Items that have an Order start between a given set of dates based on exactly what the item was (:name_id) and the frequency of that :name_id, I was using the following code:
dates = ["May 27, 2016", "May 30, 2016"]
items = Item.joins(:order).where("orders.start >= ?", dates.first).where("orders.start <= ?", dates.last)
histogram = {}
items.pluck(:name_id).uniq.each do |name_id|
histogram[name_id] = items.where(name_id:name_id).count
end
This code worked FINE.
Now, however, I'm trying to build a histogram that's more expansive. I still want to capture frequency of :name_id over a period of time, but now I want to bound that time by Order start and end. I'm having trouble however, combining the ActiveRecord Relations that follow the queries. Specifically, if my queries are as follows:
items_a = Item.joins(:order).where("orders.start >= ?", dates.first).where("orders.start <= ?", dates.last)
items_b = Item.joins(:order).where("orders.end >= ?", dates.first).where("orders.end <= ?", dates.last)
How do I join the 2 queries so that my code below that acts on query objects still works?
items.pluck(:name_id).each do |name_id|
histogram[name_id] = items.where(name_id:name_id).count
end
What I've tried:
+, but of course that doesn't work because it turns the result into an Array where methods like pluck don't work:
(items_a + items_b).pluck(:name_id)
=> error
merge, this is what all the SO answers seem to say... but it doesn't work for me because, as the docs say, merge figures out the intersection, so my result is like this:
items_a.count
=> 100
items_b.count
=> 30
items_a.merge(items_b)
=> 15
FYI currently, I've monkey-patched this with the below, but it's not very ideal. Thanks for the help!
name_ids = (items_a.pluck(:name_id) + items_b.pluck(:name_id)).uniq
name_ids.each do |name_id|
# from each query object, return the ids of the item objects that meet the name_id criterion
item_object_ids = items_a.where(name_id:name_id).pluck(:id) + items_b.where(name_id:name_id).pluck(:id) + items_c.where(name_id:name_id).pluck(:id)
# then check the item objects for duplicates and then count up. btw I realize that with the uniq here I'm SOMEWHAT doing an intersection of the objects, but it's nowhere near as severe... the above example where merge yielded a count of 15 is not that off from the truth, when the count should be maybe -5 from the addition of the 2 queries
histogram[name_id] = item_object_ids.uniq.count
end
You can combine your two queries into one:
items = Item.joins(:order).where(
"(orders.start >= ? AND orders.start <= ?) OR (orders.end >= ? AND orders.end <= ?)",
dates.first, dates.last, dates.first, dates.last
)
This might be a little more readable:
items = Item.joins(:order).where(
"(orders.start >= :first AND orders.start <= :last) OR (orders.end >= :first AND orders.end <= :last)",
{ first: dates.first, last: dates.last }
)
Rails 5 will support an or method that might make this a little nicer:
items_a = Item.joins(:order).where(
"orders.start >= :first AND orders.start <= :last",
{ first: dates.first, last: dates.last }
).or(
"orders.end >= :first AND orders.end <= :last",
{ first: dates.first, last: dates.last }
)
Or maybe not any nicer in this case
Maybe this will be a bit cleaner:
date_range = "May 27, 2016".to_date.."May 30, 2016".to_date
items = Item.joins(:order).where('orders.start' => date_range).or('orders.end' => date_range)

Rails - Building Conditions for Use in a Query

I usually like to build a conditions hash like this below:
conditions = {}
conditions[:color] = "black"
conditions[:doors] = 4
conditions[:type] = "sedan"
Cars.find(:all, :conditions=>conditions)
But how would I add a date range into this for something like:
year >= '2011-01-01' and year < '2011-02-01'
I am assuming you are using Rails 2.3.x and year is a date column.
conditions = {}
conditions[:color] = "black"
conditions[:doors] = 4
conditions[:type] = "sedan"
# create a date range
conditions[:year] = (Date.parse("2011-01-01")...Date.parse("2011-02-01"))
Car.all(:conditions => conditions)
If you want to do even more complex queries in 2.3.x use the AR Extensions gem.
Read this article for more details.
If you're on Rails 3, why not use AREL?
Cars.where(:color => "black").
where(:doors => 4).
where(:type => "sedan").
where("year >= '2011-01-01'").
where("year < '2011-02-01'")
Btw, don't use :type as a field name. Rails uses this for STI.
On Rails 2.3, I'd just build up conditions as a String instead.
You can build a up query through relations. The query will not be executed until it needs to be evaluated. This is nice for searches where some parameters are optional.
#cars = Cars.where(:color => "black")
#cars = #cars.where(:doors => 4)
#cars = #cars.where("year >= '2011-01-01'")
#cars = #cars.where("year <= '2011-02-01'")
Or you could just merge all that together into one:
Cars.where(["color=? AND doors=? AND year >= ? AND year <= ?", "black", 4, "2011-01-01", "2011-02-01"]
UPDATE:
For Rails < 3
#cars = Cars.scoped(:conditions => {:color => "black"})
#cars = #cars.scoped(:conditions => {:doors => 4})
#cars = #cars.scoped(:conditions => "year >= '2011-01-01'")
#cars = #cars.scoped(:conditions => "year <= '2011-02-01'")
OR
Cars.all(:conditions => ["color=? AND doors=? AND year >= ? AND year <= ?", "black", 4, "2011-01-01", "2011-02-01"]

How do I search trough query results, to prevent making to many requests

I have the below set of queries, but I'm sure this isn't DRY. However, I can't find out how to filter trough the deals var instead of querying again for each var. Is it possible?
deals = Deal.all
won = Deal.find( :all, :conditions => ["status = 'won'"] ).count
pending = Deal.find( :all, :conditions => ["status = 'pending'"] ).count
lost = Deal.find( :all, :conditions => ["status = 'lost'"] ).count
Use GROUP BY SQL clause:
Hash[Deal.all(:select => 'status, count(*) as count', :group => 'status').map{|e|
[e.status, e.count]
}]
Edit: I forgot that you already have all the records loaded. In that case, you can get counts per status this way:
Hash[deals.group_by(&:status).map{|k,v| [k,v.count]}]
You can use following:-
Deal.find(:all, :select => 'status, count(id) as deal_count', :group => 'status')
You can use Array#select:
deals = Deal.all
won = deals.select { |deal| deal.status == 'won' }.length
# similar for pending and lost
I think you can use Ruby's inject function for this:
won = deals.inject(0) {|total, deal| deal.status == 'won' ? total + 1 : total }
if your Deal objects are ActiveRecord objects (which is typically the case for models), you can launch the count on the data base:
won = Deal.count_by_sql("select count(*) from deals where status = 'won'")
Another way to do it would be to write the sql query that would do the all the count for you, and group them by status:
count_by_status = Deal.find_by_sql("select status,count(*) from deals group by status;")
Then you can use the result (which will be an array of hashes I think).

Resources