Rails find conditions... where attribute is not a database column - ruby-on-rails

I think it's safe to say everyone loves doing something like this in Rails:
Product.find(:all, :conditions => {:featured => true})
This will return all products where the attribute "featured" (which is a database column) is true. But let's say I have a method on Product like this:
def display_ready?
(self.photos.length > 0) && (File.exist?(self.file.path))
end
...and I want to find all products where that method returns true. I can think of several messy ways of doing it, but I think it's also safe to say we love Rails because most things are not messy.
I'd say it's a pretty common problem for me... I'd have to imagine that a good answer will help many people. Any non-messy ideas?

The only reliable way to filter these is the somewhat ugly method of retrieving all records and running them through a select:
display_ready_products = Product.all.select(&:display_ready?)
This is inefficient to the extreme especially if you have a large number of products which are probably not going to qualify.
The better way to do this is to have a counter cache for your photos, plus a flag set when your file is uploaded:
class Product < ActiveRecord::Base
has_many :photos
end
class Photo < ActiveRecord::Base
belongs_to :product, :counter_cache => true
end
You'll need to add a column to the Product table:
add_column :products, :photos_count, :default => 0
This will give you a column with the number of photos. There's a way to pre-populate these counters with the correct numbers at the start instead of zero, but there's no need to get into that here.
Add a column to record your file flag:
add_column :products, :file_exists, :boolean, :null => false, :default => false
Now trigger this when saving:
class Product < ActiveRecord::Base
before_save :assign_file_exists_flag
protected
def assign_file_exists_flag
self.file_exists = File.exist?(self.file.path)
end
end
Since these two attributes are rendered into database columns, you can now query on them directly:
Product.find(:all, :conditions => 'file_exists=1 AND photos_count>0')
You can clean that up by writing two named scopes that will encapsulate that behavior.

You need to do a two level select:
1) Select all possible rows from the database. This happens in the db.
2) Within Ruby, select the valid rows from all of the rows. Eg
possible_products = Product.find(:all, :conditions => {:featured => true})
products = possible_products.select{|p| p.display_ready?}
Added:
Or:
products = Product.find(:all, :conditions => {:featured => true}).select {|p|
p.display_ready?}
The second select is the select method of the Array object. Select is a very handy method, along with detect. (Detect comes from Enumerable and is mixed in with Array.)

Related

ActiveRecord group by on a join

Really been struggling trying to get a group by to work when I have to join to another table. I can get the group by to work when I don't join, but when I want to group by a column on the other table I start having problems.
Tables:
Book
id, category_id
Category
id, name
ActiveRecord schema:
class Category < ActiveRecord::Base
has_many :books
end
class Book < ActiveRecord::Base
belongs_to :category
end
I am trying to get a group by on a count of categories. I.E. I want to know how many books are in each category.
I have tried numerous things, here is the latest,
books = Book.joins(:category).where(:select => 'count(books.id), Category.name', :group => 'Category.name')
I am looking to get something back like
[{:name => fiction, :count => 12}, {:name => non-fiction, :count => 4}]
Any ideas?
Thanks in advance!
How about this:
Category.joins(:books).group("categories.id").count
It should return an array of key/value pairs, where the key represents the category id, and the value represents the count of books associated with that category.
If you're just after the count of books in each category, the association methods you get from the has_many association may be enough (check out the Association Basics guide). You can get the number of books that belong to a particular category using
#category.books.size
If you wanted to build the array you described, you could build it yourself with something like:
array = Categories.all.map { |cat| { name: cat.name, count: cat.books.size } }
As an extra point, if you're likely to be looking up the number of books in a category frequently, you may also want to consider using a counter cache so getting the count of books in a category doesn't require an additional trip to the database. To do that, you'd need to make the following change in your books model:
# books.rb
belongs_to :category, counter_cache: true
And create a migration to add and initialize the column to be used by the counter cache:
class AddBooksCountToCategories < ActiveRecord::Migration
def change
add_column :categories, :books_count, :integer, default: 0, null: false
Category.all.each do |cat|
Category.reset_counters(cat.id, :books)
end
end
end
EDIT: After some experimentation, the following should give you close to what you want:
counts = Category.joins(:books).count(group: 'categories.name')
That will return a hash with the category name as keys and the counts as values. You could use .map { |k, v| { name: k, count: v } } to then get it to exactly the format you specified in your question.
I would keep an eye on something like that though -- once you have a large enough number of books, the join could slow things down somewhat. Using counter_cache will always be the most performant, and for a large enough number of books eager loading with two separate queries may also give you better performance (which was the reason eager loading using includes changed from using a joins to multiple queries in Rails 2.1).

Rails-y way to query a model with a belongs_to association

I have two models:
class Wine
belongs_to :region
end
class Region
has_many :wines
end
I am attempting to use the #where method with a hash built from transforming certain elements from the params hash into a query hash, for example { :region => '2452' }
def index
...
#wines = Wine.where(hash)
...
end
But all I get is a column doesn't exist error when the query is executed:
ActiveRecord::StatementInvalid: PGError: ERROR: column wines.region does not exist
LINE 1: SELECT "wines".* FROM "wines" WHERE "wines"."region" =...
Of course, the table wines has region_id so if I queried for region_id instead I would not get an error.
The question is the following:
Is there a rails-y way to query the Wine object for specific regions using the id in the #where method? I've listed some options below based on what I know I can do.
Option 1:
I could change the way that I build the query hash so that each field has _id (like { :region_id => '1234', :varietal_id => '1515' } but not all of the associations from Wine are belongs_to and thus don't have an entry in wines for _id, making the logic more complicated with joins and what not.
Option 2:
Build a SQL where clause, again using some logic to determine whether to use the id or join against another table... again the logic would be somewhat more complicated, and delving in to SQL makes it feel less rails-y. Or I could be wrong on that front.
Option(s) 3..n:
Things I haven't thought about... your input goes here :)
You could set up a scope in the Wine model to make it more rails-y ...
class Wine < ActiveRecord::Base
belongs_to :region
attr_accessible :name, :region_id
scope :from_region, lambda { |region|
joins(:region).where(:region_id => region.id)
}
end
So then you can do something like:
region = Region.find_by_name('France')
wine = Wine.from_region(region)
Edit 1:
or if you want to be really fancy you could do a scope for multiple regions:
scope :from_regions, lambda { |regions|
joins(:region).where("region_id in (?)", regions.select(:id))
}
regions = Region.where("name in (?)", ['France','Spain']) # or however you want to select them
wines = Wine.from_regions(regions)
Edit 2:
You can also chain scopes and where clauses, if required:
regions = Region.where("name in (?)", ['France','Spain'])
wines = Wine.from_regions(regions).where(:varietal_id => '1515')
Thanks to all who replied. The answers I got would be great for single condition queries but I needed something that could deal with a varying number of conditions.
I ended up implementing my option #1, which was to build a condition hash by iterating through and concatenating _id to the values:
def query_conditions_hash(conditions)
conditions.inject({}) do |hash, (k,v)|
k = (k.to_s + "_id").to_sym
hash[k] = v.to_i
hash
end
end
So that the method would take a hash that was built from params like this:
{ region => '1235', varietal => '1551', product_attribute => '9' }
and drop an _id onto the end of each key and change the value to an integer:
{ region_id => 1235, varietal_id => 1551, product_attribute_id => 9 }
We'll see how sustainable this is, but this is what I went with for now.

Ruby on Rails: using nested named_scopes

I just got referred to stackoverflow by a friend here to help with a problem I am having. I am fairly new to ruby on rails and I am working on a collaborative project where we have a script (medal_worker.rb) that is scheduled to run at a fixed intervals to award people various medals based on various participation and success on our website. One of the new medals I am working on rewards people for "milestones". For the purpose of this problem, let's say we want to give them medals when they make 100, 1000, and 10000 comments. I would like to do this by using named_scopes from the User model (user.rb) to give me filtered lists of the users I am looking for.
My question is: How do I find the users who do not have the respective medals for the respective milestone comment level (preferably using the named_scopes from the User model)?
Here is an exerpt from my model_worker.rb file:
def award_comment_milestone(comments)
users = Users.frequent_comment_club_members(comments).not_awarded_medal(Medal.find_by_id(medal.id))
for user in users do
award_medal(medal, nil, user) if #award
end
end
Here is where I am at with the named_scopes in the user model (user.rb):
named_scope :frequent_comment_club_members, lambda { |*args|
{:include => comment_records, :conditions => ['comment_records.comment_type = ? and comment_records.comments >= ?', 'User', (args.first || 0)]}
}
named_scope :not_awarded_medal, lambda { |medal|
{:include => :awards, :conditions => ['awards.medal_id not in (select awards.medal_id from awards where awards.medal_id = ?)", medal.id] }
}
This is not working as I would like, but I don't know if the problem is in the named_scopes or how I am passing arguements or what. Thanks.
Your named_scopes look fine. Except you are starting with a single apostrophe and ending with a double apostrophe in the not_awarded_medal condition statement.
EDIT:
Take it back. Your not_awarded_medal named_scope is off.
Try something like this:
named_scope :not_awarded_medal, lambda { |medal_id|
{ :include => :awards,
:conditions => [
"? not in (select awards.id from awards where awards.user_id = users.id)", medal_id
]
}
}
untested
Now this is assuming that you have the following relationships:
User: has_many :awards
Award: belongs_to :user
If you are using has_many :through then you are going to have to change the SQL to look at the join (users_awards) table.
--
But I do see a couple of things in the award_comment_milestone function.
What is the parameter coming into award_comment_milestone? Is it an array of comments or is it a count of comments? Also where is medal defined?
If comments is an array then you need to pass comments.length into frequent_comment_club_members. If it's the count then I would rename it to comments_count so the next person can understand the logic more quickly.
Some general observations:
not_awarded_medal should just take a medal_id and not the whole object (no need to do multiple queries)
Why are you doing Medal.find_by_id(medal.id)? You already have the medal object.

How to sort Rails AR.find by number of objects in a has_many relationship

How can I write an AR find query to have the results ordered by the number of records in a has_many association?
class User < ActiveRecord::Base
has_many :photos
end
I want to do something like...
User.find(:all, :order => photos.count)
I realize my find is not valid code. Say I have the following data.
User 1, which has 3 photos
User 2, which has 5 photos
User 3, which has 2 photos
I want my find to bring me back the users in the order of...
User 2,
User 1,
User 3
based on the count of of the users photos
The easiest way to achieve this is probably to add a counter cache to that model and then sort by that column.
class Photo < ActiveRecord::Base
belongs_to :user, :counter_cache => true
end
And be sure to add a column to your users table called photos_count.
Then you will be able to...
User.find(:all, :order => 'photos_count')
If you don't want an extra column, you could always ask for an extra column in the returned result set:
User.all(:select => "#{User.table_name}.*, COUNT(#{Photo.table_name}.id) number_of_photos",
:joins => :photos,
:order => "number_of_photos")
This generates the following SQL:
SELECT users.*, COUNT(photos.id) number_of_photos
FROM `users` INNER JOIN `photos` ON photos.user_id = users.id
ORDER BY number_of_photos
If you don't want to add a counter cache column, your only option is to sort after the find. If you :include the association in your find, you won't incur any additional database work.
users = User.find(:all, :include => :photos).sort_by { |u| -u.photos.size }
Note the negative sign in the sort_by block to sort from high to low.
I would advise you not to write direct SQL, since implementations of it may vary from store to store. Fortunately, you have arel:
User.joins(:photos).group(Photo.arel_table[:user_id]).
order(Photo.arel_table[:user_id].count)
Counter cache will help, but you'll need an extra column in the db.
I'd add this as a comment on the top answer, but I can't for some reason. According to this post:
http://m.onkey.org/active-record-query-interface
The User.all(options) method will be deprecated after Rails 3.0.3, and replaced with a bunch of other (handy, chainable) active record type stuff, but it makes it very hard to figure out how to put together the same kind of query.
As a result, I've gone ahead and implemented the counter cache method. This was pretty easy and painless with the exception that you need to remember to update the column information in your migration, otherwise all existing records will have "0."
Here's what I used in my migration:
class AddUserCountToCollections < ActiveRecord::Migration
def self.up
add_column :collections, :collectionusers_count, :integer, :default => 0
Collection.reset_column_information
Collection.all.each do |c|
Collection.update_counters c.id, :collectionusers_count => c.collectionusers.count
end
end
def self.down
remove_column :collections, :collectionusers_count
end
end
In theory this should be faster, too. Hope that's helpful going forward.
Your question doesn't make sense. The :order parameter specifies a column name and an optional ordering direction i.e. asc(ending) or desc(ending).
What is the result that you're trying to achieve?

What is causing this ActiveRecord::ReadOnlyRecord error?

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

Resources