My Account model has the following two associations:
has_many :expenses,
:order => 'expenses.dated_on DESC',
:dependent => :destroy
has_many :recent_expenses,
:class_name => 'Expense',
:conditions => "expenses.dated_on <= '#{Date.today}'",
:order => 'dated_on DESC',
:limit => 5
In one of my views I'm rendering recent expenses like so:
<% #account.recent_expenses.each do |expense| %>
...
<% end %>
On my development machine, on the staging server (which runs in production mode) and also on the production console, #account.recent_expenses returns the correct list. However, on our live production server, the most recent expenses are not returned.
If I replace #account.recent_expenses with #account.expenses in the view, the most recent expenses are displayed, so my guess is that the #{Date.today} part of the conditions clause is somehow being cached the first time it is executed. If I restart the production Mongrel cluster, all the latest expenses are returned correctly.
Can anyone think why this would occur and how might I change the :recent_expenses query to prevent this from happening?
I'm using Rails 2.1.0.
Rails is building the query when loaded and then will re-use that query every time you call #account.recent_expenses which is exactly what you're experiencing.
If you're using Rails 2.1 you can use named_scope to achieve what you're looking for.
in your Expense model, put the following:
named_scope :recent, lambda { {:conditions => ["expenses.dated_on <= ?", Date.today], :order => 'dated_on DESC', :limit => 5 } }
The lambda is used to ensure rails rebuilds the query each time.
from your Account remove:
has_many :recent_expenses ...
and then call:
<% #account.expenses.recent.each do |expense| %>
...
<% end %>
Like Andrew said, association macros are read at application startup, so any dynamic bits (like Date.today) are parsed at that point. His solution works if you're on Rails 2.1 or later; for earlier versions, you can use the has_finder gem, or just create the following method on the Expense model:
def self.recent
find(:all, :conditions => ['dated_on <= ?', Date.today], :limit => 5, :order => 'dated_on DESC')
end
Class methods like this are exposed to associations, and are properly scoped - the difference between them and named_scopes is that you can't chain them.
Here is what it would look like in Rails 3:
scope :recent, lambda { where("expenses.dated_on <= ?", Date.today).order('dated_on DESC').limit(5) }
WARNING: Watch out for chained scopes that are not wrapped in lambda. You may think that part of the chain is evaluated at runtime, but it may be evaluated at app server startup. Make sure you lambda wrap any scope that will use these other scopes. Explanation here:
http://www.slashdotdash.net/2010/09/25/rails-3-scopes-with-chaining/
Related
I am new to Rail and trying to make a query in ActiveRecord. I am trying to get all of the records with the status of 'Landed', that are over 60 days old. My query works up to the point of getting all of the projects with the status of 'Landed'. When I add in the last condition of "created_at < ? ", then I always get an empty relation. I know that I have projects that fit that description, so I am doing something wrong in my query and dont understand. I believe my error is in the date comparison, but I am not sure.
1. Projects
belongs_to :status
has_many :project_status_histories
2. Status
has_many :projects
has_many :project_status_histories
3. Project_Status_Histories
belongs_to :status
belongs_to :project
Project.find(:all, :joins => [:project_status_histories, :status], :conditions => {:projects => {:status_id => Status.where(:name => 'Landed').first.id }, :project_status_histories => {:created_at => ["created_at < ?", (Date.today - 60.days)]}})
I have tried to build the query, step by step, with the dbconsole and am not having any luck. Thanks for all the help in advance.
I don't think it's the date arithmetic. One nice way to do this would be with named scopes. Add the following to project.rb:
scope :landed, joins(:status).where('statuses.name' => 'Landed')
scope :recent, lambda \
{ joins(:project_status_histories) \
.where('project_status_histories.created_at < ?', Date.today - 60.days) }
Then you can retrieve the relevant records/objects with:
Project.landed.recent
This worked for me in my test. You should also check out the rails guide, from which I stole most of this:
http://guides.rubyonrails.org/active_record_querying.html#scopes
Your query is a little bit complicated...
I would rather do it like this:
Project.where("status.name = ? AND project_status_histories.created_at < ?", "Landed", Time.now.day - 60.days)
I think this should work better. Let me know if it doesn't, maybe I've wrote something wrong, unfortunately I can't test it right now...
[Edit]
You also might want to see what is the generated SQL, use the "explain" method for that, just add it to the end of your query and print the result, for instance with your query:
Project.find(:all, :joins => [:project_status_histories, :status], :conditions => {:projects => {:status_id => Status.where(:name => 'Landed').first.id }, :project_status_histories => {:created_at => ["created_at < ?", (Date.today - 60.days)]}}).explain
I am working on a Rails 2.3.9 app and my question involves both a self referencial relationship and a named_scope. This application allows users to log and share workouts. A workout can be public or private and that is designated by #workout.public == 1.
I allow users to 'follow' people. So on a current_user's dashboard I display all public workouts from users that current_user follows with the following code:
/dashboard/index.html.erb
<% current_user.friends_workouts.each do |workout| %>
<%= link_to (workout.title), workout %> <br/>
by <%= link_to (workout.user.username), workout.user %> - <%= time_ago_in_words(workout.created_at)%> ago</p>
<% end %>
user.rb
def friends_workouts
#friends_workouts ||= Workout.current.public_workouts.find_all_by_user_id(self.friends.map(&:id), :order => "created_at DESC", :limit => 3)
end
workout.rb
named_scope :public_workouts, :conditions => {:public => 1 }, :order => "created_at DESC"
I now want to add a condition to this scope as I am adding another level of sharing. Users can associate to a "box" (a gym really) through a "membership" model. So if the current_user belongs_to the same "box" as a user they follow, they should not only see the workouts marked public but also workouts where #workout.box_only == 1.
How can I affect the above to include all public workouts from followed users AND workouts from followed users where #workout.box_only == 1 and #workout.user.membership.box_id == current_user.membership.box_id. I know that syntax is incorrect but you get my point (hopefully).
UPDATE:
It also needs to be considered that :public_workouts is being called from pages that don't require a logged_in? user so in that case if the scope is trying to reference current_user it will throw an error.
UPDATE 2:
:user has_many :memberships
I believe something like the following should do it for you:
named_scope :public_workouts,
:joins => ", user, membership"
:conditions =>
"workouts.public = 1 or
membership.box_id = #{current_user.membership.box_id}",
:group => "workouts.id",
:order => "workouts.created_at DESC"
You would have to play around with this for a bit. The hard part every time I try something like this is to get the OR conditions correct. You want to get all public and those where the joined membership.box_id matches regardless of public being 1.
Edit: Admittedly this is perhaps not the most ruby way of building a query and I haven't tested it properly but something like below could also be made to work.
def self.public_workouts
query = Workout.joins(:user => { :membership })
if current_user
query.where('memberships.box_id = ? or workouts.public = 1', current_user.membership.box_id) unless current_user.membership.box_id.nil?
else
query.where('workouts.public = 1')
end
query.group('workouts.id')
query.order("workouts.created_at DESC")
return query
end
Edit2
Another alternative could be to create two separate scopes and create a class method that combines the two scopes. This class method would then be used in the view.
named_scope :box_workouts,
:joins => ", user, membership"
:conditions => "memberships.box_id = #{current_user.membership.box_id}"
:group => "workouts.id",
:order => "workouts.created_at DESC",
:select "workouts"
named_scope :public_workouts,
:conditions => :public => 1
:order => "workouts.created_at DESC"
def self.public_box_workouts
return box_workouts.merge(public_workouts).limit(3) if current_user
return public_workouts.limit(3)
end
Edit3 Not so hard, I believe something like below will work.
def self.box_and_public_workouts(user)
return public_workouts if user.nil? or user.memberships.blank?
return public_workouts + box_workouts(user.memberships.map(&:box_id))
end
named_scope :box_workouts, lambda { |box_ids| { :conditions => ['box_id IN (?)', box_ids], :order => 'created_at DESC' } }
Apologies for taking so long. I was missing confused with how the "old" way of querying the database. I went right for Rails3 :)
Anyway, I didn't want to commit anything so I tried to fork it to send a pull request but github is being rude to me tonight. Might just copy from here then.
The following scope definition contains two bugs, which causes it not to work as expected. Can you find them?
named_scope :articles_to_display,
:conditions => ["articles.publish_at < (?)", Time.now]
(the column publish_at contains time/date, when the article should be published). The bugs are fundamental ones, not just a typos.
I will either accept the first correct answer or post the solution in few days.
The first problems is that Time.now is evaluated at the class level (when the file is read by Ruby) and not evaluated when the scope is used (which is what you most likely expect). In that case you need to wrap the conditions generation in a lambda/proc.
named_scope :articles_to_display, lambda {
:conditions => ["articles.publish_at < (?)", Time.now]
}
The second issue is likely that you're want to use Time.zone.now instead of Time.now to respect the localized time of the current request rather than the system time on the server.
The following is what you want to end up with:
named_scope :articles_to_display, lambda {
:conditions => ["articles.publish_at < (?)", Time.zone.now]
}
named_scope :articles_to_display, :conditions => ["articles.publish_at < (?)", DateTime.now]
Or try proc
named_scope :articles_to_display, proc{ :conditions => ["articles.publish_at < (?)", DateTime.now]}
Here was a picture of a superman, that was deleted :D
For additional reference, the accepted answer, converted to rails 3, is:
scope :articles_to_display, lambda {
where("articles.publish_at < (?)", Time.zone.now)
}
named_scope :articles_to_display,
:conditions => ["articles.publish_at IS NOT NULL AND articles.publish_at <= (?)", Time.now]
For Example, I want to know the User have many posts. So, I can get back post using this :
#user.posts
but I don't want to get all the posts back. I would like to limite the result, for example, top ten created, or may be sorted by some column. How can I do so? Thank you.
You can always make a generic scope to handle the limit, such as putting this in an initializer:
class ActiveRecord::Base
named_scope :limit, lambda { |*limit| {
:limit => limit[0] || 10,
:offset => limit[1]
}}
end
This makes limiting queries easy:
# Default is limited to 10
#user.posts.limit
# Pass in a specific limit
#user.posts.limit(25)
# Pass in a specific limit and offset
#user.posts.limit(25, 25)
For something more robust, you might want to investigate will_paginate.
Try this:
#user.posts(:limit => 10, :order => "created_at DESC")
http://guides.rubyonrails.org/active_record_querying.html
You should take a look at the options available for the has_many association.
You could try something like this:
class User < ActiveRecord::Base
has_many :posts
has_many :top_ten_posts, { :class_name => "Post", :order => :some_rating_column, :limit => 10 }
end
This follows this prior question, which was answered. I actually discovered I could remove a join from that query, so now the working query is
start_cards = DeckCard.find :all, :joins => [:card], :conditions => ["deck_cards.deck_id = ? and cards.start_card = ?", #game.deck.id, true]
This appears to work. However, when I try to move these DeckCards into another association, I get the ActiveRecord::ReadOnlyRecord error.
Here's the code
for player in #game.players
player.tableau = Tableau.new
start_card = start_cards.pop
start_card.draw_pile = false
player.tableau.deck_cards << start_card # the error occurs on this line
end
and the relevant Models (tableau are the players cards on the table)
class Player < ActiveRecord::Base
belongs_to :game
belongs_to :user
has_one :hand
has_one :tableau
end
class Tableau < ActiveRecord::Base
belongs_to :player
has_many :deck_cards
end
class DeckCard < ActiveRecord::Base
belongs_to :card
belongs_to :deck
end
I am doing a similar action just after this code, adding DeckCards to the players hand, and that code is working fine. I wondered if I needed belongs_to :tableau in the DeckCard Model, but it works fine for the adding to player's hand. I do have a tableau_id and hand_id columns in the DeckCard table.
I looked up ReadOnlyRecord in the rails api, and it doesn't say much beyond the description.
Rails 2.3.3 and lower
From the ActiveRecord CHANGELOG(v1.12.0, October 16th, 2005):
Introduce read-only records. If you call object.readonly! then it will
mark the object as read-only and raise
ReadOnlyRecord if you call
object.save. object.readonly? reports
whether the object is read-only.
Passing :readonly => true to any
finder method will mark returned
records as read-only. The :joins
option now implies :readonly, so if
you use this option, saving the same
record will now fail. Use find_by_sql
to work around.
Using find_by_sql is not really an alternative as it returns raw row/column data, not ActiveRecords. You have two options:
Force the instance variable #readonly to false in the record (hack)
Use :include => :card instead of :join => :card
Rails 2.3.4 and above
Most of the above no longer holds true, after September 10 2012:
using Record.find_by_sql is a viable option
:readonly => true is automatically inferred only if :joins was specified without an explicit :select nor an explicit (or finder-scope-inherited) :readonly option (see the implementation of set_readonly_option! in active_record/base.rb for Rails 2.3.4, or the implementation of to_a in active_record/relation.rb and of custom_join_sql in active_record/relation/query_methods.rb for Rails 3.0.0)
however, :readonly => true is always automatically inferred in has_and_belongs_to_many if the join table has more than the two foreign keys columns and :joins was specified without an explicit :select (i.e. user-supplied :readonly values are ignored -- see finding_with_ambiguous_select? in active_record/associations/has_and_belongs_to_many_association.rb.)
in conclusion, unless dealing with a special join table and has_and_belongs_to_many, then #aaronrustad's answer applies just fine in Rails 2.3.4 and 3.0.0.
do not use :includes if you want to achieve an INNER JOIN (:includes implies a LEFT OUTER JOIN, which is less selective and less efficient than INNER JOIN.)
Or in Rails 3 you can use the readonly method (replace "..." with your conditions):
( Deck.joins(:card) & Card.where('...') ).readonly(false)
This might have changed in recent release of Rails, but the appropriate way to solve this problem is to add :readonly => false to the find options.
select('*') seems to fix this in Rails 3.2:
> Contact.select('*').joins(:slugs).where('slugs.slug' => 'the-slug').first.readonly?
=> false
Just to verify, omitting select('*') does produce a readonly record:
> Contact.joins(:slugs).where('slugs.slug' => 'the-slug').first.readonly?
=> true
Can't say I understand the rationale but at least it's a quick and clean workaround.
Instead of find_by_sql, you can specify a :select on the finder and everything's happy again...
start_cards = DeckCard.find :all,
:select => 'deck_cards.*',
:joins => [:card],
:conditions => ["deck_cards.deck_id = ? and cards.start_card = ?", #game.deck.id, true]
To deactivate it...
module DeactivateImplicitReadonly
def custom_join_sql(*args)
result = super
#implicit_readonly = false
result
end
end
ActiveRecord::Relation.send :include, DeactivateImplicitReadonly