Ruby on Rails with sqlite, trying to query and return results from the last 7 days? - ruby-on-rails

Noob here, I'm trying to query my SQLite database for entries that have been made in the last 7 days and then return them.
This is the current attempt
user.rb
def featuredfeed
#x = []
#s = []
Recipe.all.each do |y|
#x << "SELECT id FROM recipes WHERE id = #{y.id} AND created_at > datetime('now','-7 days')"
end
Recipe.all.each do |d|
#t = "SELECT id FROM recipes where id = #{d.id}"
#x.each do |p|
if #t = p
#s << d
end
end
end
#s
end
This code returns each recipe 6(total number of objects in the DB) times regardless of how old it is.
#x should only be 3 id's
#x = [13,15,16]
if i run
SELECT id FROM recipes WHERE id = 13 AND created_at > datetime('now','-7 days')
1 Rows returned with id 13 is returned
but if look for an id that is more than 7 days old such as 12
SELECT id FROM recipes WHERE id = 12 AND created_at > datetime('now','-7 days')
0 Rows returned
I'm probably over complicating this but I've spent way too long on it at this point.
the return type has to be Recipe.

To return objects created within last 7 days just use where clause:
Recipe.where('created_at >= ?', 1.week.ago)
Check out docs for more info on querying db.
Edit according to comments:
Since you are using acts_as_votable gem, add the votes caching, so that filtering by votes score is straightforward:
Recipe.where('cached_votes_total >= ?', 10)

Ruby is expressive. I would take the opportunity to use a scope. With Active Record Scopes, this query can be represented in a meaningful way within your code, using syntactic sugar.
scope :from_recent_week, -> { where('created_at >= ?', Time.zone.now - 1.week) }
This allows you to chain your scoped query and enhance readability:
Recipe.from_recent_week.each do
something_more_meaningful_than_a_SQL_query
end
It looks to me that your problem is database abstraction, something Rails does for you. If you are looking for a function that returns the three ids you indicate, I think you would want to do this:
#x = Recipe.from_recent_week.map(&:id)
No need for any of the other fluff, no declarations necessary. I also would encourage you to use a different variable name instead of #x. Please use something more like:
#ids_from_recent_week = Recipe.from_recent_week.map(&:id)

Related

Rails .where query chained to sql function, is there a way to call it on the results without converting them to an array?

I have a method that ranks user's response rates in our system called ranked_users
def ranked_users
User.joins(:responds).group(:id).select(
"users.*, SUM(CASE WHEN answers.response != 3 THEN 1 ELSE 0 END ) avg, RANK () OVER (
ORDER BY SUM(CASE WHEN answers.response != 3 THEN 1 ELSE 0 END ) DESC, CASE WHEN users.id = '#{
current_user.id
}' THEN 1 ELSE 0 END DESC
) rank"
)
.where('users.active = true')
.where('answers.created_at BETWEEN ? AND ?', Time.now - 12.months, Time.now)
end
result = ranked_users
I then take the top three with top_3 = ranked_users.limit(3)
If the user is not in the top 3, I want to append them with their rank to the list:
user_rank = result.find_by(id: current_user.id)
Whenever I call user_rank.rank it returns 1. I know this is because it's applying the find_by clause first and then ranking them. Is there a way to enforce the find_by clause happens only on the result of the first query? I tried doing result.load.find_by(...) but had the same issue. I could convert the entire result into an array but I want the solution to be highly scalable.
If you expect lots of users with lots of answers and high load on your rating system - you can create a materialized view for the ranking query with (user_id, avg, rank, etc.) and refresh it periodically instead of calculating rank every time (say, a few times per day or even less often). There's gem scenic for this.
You can even have indexes on rank and user id on the view and your query will be two simple fast reads from it.

Ordering by specific value in Activerecord

In Ruby on Rails, I'm trying to order the matches of a player by whether the current user is the winner.
The sort order would be:
Sort by whether the current user is the winner
Then sort by created_at, etc.
I can't figure out how to do the equivalent of :
Match.all.order('winner_id == ?', #current_user.id)
I know this line is not syntactically correct but hopefully it expresses that the order must be:
1) The matches where the current user is the winner
2) the other matches
You can use a CASE expression in an SQL ORDER BY clause. However, AR doesn't believe in using placeholders in an ORDER BY so you have to do nasty things like this:
by_owner = Match.send(:sanitize_sql_array, [ 'case when winner_id = %d then 0 else 1 end', #current_user.id ])
Match.order(by_owner).order(:created_at)
That should work the same in any SQL database (assuming that your #current_user.id is an integer of course).
You can make it less unpleasant by using a class method as a scope:
class Match < ActiveRecord::Base
def self.this_person_first(id)
by_owner = sanitize_sql_array([ 'case when winner_id = %d then 0 else 1 end', id])
order(by_owner)
end
end
# and later...
Match.this_person_first(#current_user.id).order(:created_at)
to hide the nastiness.
This can be achived using Arel without writing any raw SQL!
matches = Match.arel_table
Match
.order(matches[:winner_id].eq(#current_user.id).desc)
.order(created_at: :desc)
Works for me with Postgres 12 / Rails 6.0.3 without any security warning
If you want to do sorting on the ruby side of things (instead of the SQL side), then you can use the Array#sort_by method:
query.sort_by(|a| a.winner_id == #current_user.id)
If you're dealing with bigger queries, then you should probably stick to the SQL side of things.
I would build a query and then execute it after it's built (mostly because you may not have #current_user. So, something like this:
query = Match.scoped
query = query.order("winner_id == ?", #current_user.id) if #current_user.present?
query = query.order("created_at")
#results = query.all

Rails named scope for left joined model

I have a table named acts, and I'll like to run a query that rolls up act values for a whole week. I'd like to make sure the query always returns one row for each day of the week, even if there are no records for that day. Right now I'm doing it like this:
def self.this_week_totals
sunday = Time.now.beginning_of_week(:sunday).strftime("%Y-%m-%d")
connection.select_values(<<-EOQ)
SELECT COALESCE(SUM(end_time - start_time), '0:00:00') AS total_time
FROM generate_series(0, 6) AS s(t)
LEFT JOIN acts
ON acts.day = '#{sunday}'::date + s.t
WHERE deleted_at IS NULL
GROUP BY s.t
ORDER BY s.t
EOQ
end
Is there any way I could make this a named scope on the Act class so it can be combined with other conditions, for example to filter the Acts by a client_id? Since acts isn't in my FROM, but is part of the LEFT JOIN, I'm guessing not, but perhaps someone out there knows a way.
Edit: Just to clarify, the goal is for this method to always return exactly 7 Act objects, regardless of what's in the database.
if you want your query object to be chainable it must be an ActiveRelation object
where, select, order and the other Arel objects return ActiveRelation objects that are chainable, so if the below works you can chain off of the returned query object
note in rails 3 and up having a class method that returns an ActiveRelation is basically the same as a scope, they are both chainable query objects
class Act
def self.this_week_totals
sunday = Time.now.beginning_of_week(:sunday).strftime("%Y-%m-%d")
select("COALESCE(SUM(end_time - start_time), '0:00:00') AS total_time")
.from("generate_series(0, 6) AS s(t)")
.joins("LEFT JOIN acts ON acts.day = '#{sunday}'::date + s.t")
.where("deleted_at IS NULL")
.group("s.t")
.order("s.t")
end
# ...
end
client_id = params[:client_id]
Act.this_week_totals.where("client_id = ?", client_id)
http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-from
Although I really thought I could use the solution from #house9, I don't see any way to avoid compromising on at least one of these goals:
Always yield 7 Act objects.
Return an ActiveRelation so I can compose this method with other scopes.
Permit joining to the clients table.
Here is the part-SQL/part-Ruby solution I'm actually using, which sadly gives up on point #2 above and also returns tuples rather than Acts:
def self.this_week(wk=0)
sunday = Time.now.beginning_of_week(:sunday)
sunday += wk.weeks
not_deleted.where("day BETWEEN ? AND ?", sunday, sunday + 7.days)
end
scope :select_sum_total_hours,
select("EXTRACT(EPOCH FROM COALESCE(SUM(end_time - start_time), '0:00:00'))/3600 AS total_hours")
scope :select_sum_total_fees,
joins(:client).
select("SUM(COALESCE(clients.rate, 0) * EXTRACT(EPOCH FROM COALESCE(end_time - start_time, '0:00:00'))/3600) AS total_fees")
def self.this_week_totals_by_day(wk=0)
totals = Hash[
this_week(wk)
.select("EXTRACT(DAY FROM day) AS just_day")
.select_sum_total_hours
.select_sum_total_fees
.group("day")
.order("day")
.map{|act| [act.just_day, [act.total_hours.to_f, act.total_fees.to_money]]}
]
sunday = Time.now.beginning_of_week(:sunday)
sunday += wk.weeks
(0..6).map do |x|
totals[(sunday + x.days).strftime("%d")] || [0, 0.to_money]
end
end
That could be DRYed up a bit, and it would produce errors if there were ever a month with fewer than 7 days, but hopefully it shows what I'm doing. The scopes for this_week, select_sum_total_hours, and select_sum_total_fees are used elsewhere, so I want to pull them out into scopes rather than repeating them in several big raw SQL strings.

Ruby on Rails 3 Check In/Check Out ranges by hour

I'm using Ruby on Rails 3 and I have a "visit" model which stores a check_in and check_out datetime and I need to search through visits in a general date range and count the number of "visitors present" grouped by all hours of the day.
...i.e. I need something like:
8:00am - 8:59am : 12 visitors
9:00am - 9:59am : 5 visitors
10:00am - 10:59am : 4 visitors
...given a table of visits with a check in and check out time stored.
The idea is to take check-in and check-out times for "visits" and then determine how many visitors (assuming each visit logs one visitor, which it does by policy) were visiting during any given hour of the day in order to find out peak visiting times.
I've tried setting up queries like:
eight_am_visits = Visit.where("EXTRACT(HOUR_MINUTE FROM check_in) <= 859").where("EXTRACT(HOUR_MINUTE FROM check_out) >= 800")
...and haven't quite hit on it because Rails stores dates in such an odd fashion (in UTC, which it will convert on database query) and it doesn't seem to be doing that conversion when I use something like EXTRACT in SQL...
...any idea how I can do this?
Looks like you're not actually interested in the Visit objects at all. If you just want a simple summary then push AR out of the way and let the database do the work:
# In visit.rb
def self.check_in_summary(date)
connection.select_rows(%Q{
select extract(hour from check_in), count(*)
from visits
where cast(check_in as date) = '#{date.iso8601}'
group by extract(hour from check_in)
}).inject([ ]) do |a, r|
a << { :hour => r[0].to_i, :n => r[1].to_i }
end
end
Then a = Visit.check_in_summary(Date.today - 1) will give you the summary for yesterday without doing any extra work. That demo implementation will, of course, have holes in the array for hours without any checkins but that is easy to resolve (if desired):
def self.check_in_summary(date)
connection.select_rows(%Q{
select extract(hour from check_in), count(*)
from visits
where cast(check_in as date) = '#{date.iso8601}'
group by extract(hour from check_in)
}).each_with_object([0]*24) do |r, a| # Don't forget the arg order change!
a[r[0].to_i] = r[1].to_i
end
end
That version returns an array with 24 elements (one for each zero-based hour) whose values are the number of checkins within that hour.
Don't be afraid to drop down to SQL when it is convenient, AREL is just one tool and you should have more than one tool in your toolbox. Also, don't be afraid to add extra data mangling and summarizing methods to your models, your models should have an interface that allows you to clearly express your intent in the rest of your code.
Maybe something like that?!
t = Time.now
eight_am_visits = Visit.all(:conditions => ['check_in > ? and check_in < ?', Time.utc(t.year, t.month, t.day, 8), Time.utc(t.year, t.month, t.day, 8, 59)])
EDIT:
Or you can grab all visits by day and filter it in Rails:
t = Time.now
visits = Visit.all(:conditions => ['created_at > ? and created_at < ?', Time.utc(t.year, t.month, t.day - 1), Time.utc(t.year, t.month, t.day + 1)])
visits_by_hour = []
(0..23).each do |h|
visits_by_hour << visits.map {|e| e if e.created_at > Time.utc(t.year, t.month, t.day, h) && e.created_at < Time.utc(t.year, t.month, t.day, h, 59)}.count
end
And in view:
<% visits_by_hour.each_with_index do |h, v| %>
<%= "#{h}:00 - #{h}:59: #{v} visitors" %>
<% end %>
Thanks for your help Olexandr and mu, I managed to figure something out with the insight you gave me here.
I came up with this, and it seems to work:
#grab the data here, this is nice because
#I can get other stats out of it (which I don't show here)
#visits = Visit.where(:check_in => #start_date..#end_date, :check_out => #start_date..#end_date).where("check_out IS NOT NULL");
#Here we go
#visitors_present_by_hour = {}
(0..23).each do |h|
# o.o Ooooooh.... o_o Hee-hee! ^_^
#visitors_present_by_hour[h] = #visits.collect{|v| v.id if v.check_in.hour <= h and v.check_out.hour >= h}.compact.count
end
Then I can just dump out that hash in my view.
It seems the solution was a bit simpler than I thought, and doing it this way actually makes rails do the time conversions from UTC.
So, I could just collect all the visits which have hours in the hour range, then compact out the nils and count what's left. I was surprised once I hit on it. I didn't need any custom SQL at all as I thought I would (unless this is completely wrong, but it seems to be working with some test data).
Thanks guys!

Sorting by a virtual attribute in Rails 3

BACKGROUND: I have a set of Posts that can be voted on. I'd like to sort Posts according to their "vote score" which is determined by the following equation:
( (#post.votes.count) / ( (Time.now - #post.created_at) ** 1 ) )
I am currently defining the vote score as such:
def vote_score(x)
( (x.votes.count) / ( (Time.now - x.created_at) ** 1 ) )
end
And sorting them as such:
#posts = #posts.sort! { |a,b| vote_score((b) <=> vote_score((a) }
OBJECTIVE: This method takes a tremendous toll on my apps load times. Is there a better, more efficient way to accomplish this kind of sorting?
If you are using MySQL you can do the entire thing using a query:
SELECT posts.id,
(COUNT(votes.id)/(TIME_TO_SEC(NOW()) - TIME_TO_SEC(posts.created_at))) as score
FROM posts INNER JOIN votes ON votes.post_id = posts.id
GROUP BY posts.id
ORDER BY score DESC
Or:
class Post
scope :with_score, select('posts.*')
.select('(COUNT(votes.id)/(TIME_TO_SEC(NOW()) - TIME_TO_SEC(posts.created_at))) as score')
.joins(:votes)
.group('posts.id')
.order('score DESC')
end
Which would make your entire query:
#posts = Post.with_score.all
P.S: You can then modify your Post class to use the SQL version of score if it is present. You can also make the score function cached in an instance so you don't have to re-calculate it every time you ask for a post's score:
class Post
def score
#score ||= self[:score] || (votes.count/(Time.now.utc - x.created_at.utc)
end
end
P.S: The SQLLite3 equivalent is:
strftime('%s','now') - strftime('%s',posts.created_at)
You shouldn't use sort! if you are going to assign to the same variable (it is wrong in this case), you should change the sort to:
#posts.sort!{|a, b| vote_score(b) <=> vote_score(a) }
It looks like you are counting the votes for Post each time you call another Post which is hitting the database quite a bit and probably the source of the toll on your load times, you can use a counter_cache to count each time a vote is made and store that in the posts table. This will make it so you only do one db query to load from the posts table.
http://guides.rubyonrails.org/association_basics.html

Resources