How can I compose/chain queries in Rails 2? - ruby-on-rails

I have one query that gets the total count of rows with one condition, and a second query that gets the total count of rows with the same condition plus another condition. Ideally, I wouldn't repeat myself in the code and could instead just chain/compose the extra condition onto the first query.
I'm thinking of something like this.
query1 = Table.find(:all, :conditions => "condition1")
query2 = query1.find(:all, :conditions => "condition2")
It'd also be nice to find out what this looks like for the Table.count use case, since that's what I'm actually trying to do at the moment.
I'm guessing that the ActiveRecord::Base has some method that will return the query object as opposed to executing it, but I haven't found that in the docs.

Although Rails 3 makes this significantly easier, you can always do it in Rails 2 with a little hack that emulates it:
# config/initializers/rails2_where_scope.rb
class ActiveRecord::Base
named_scope :where, lambda { |conditions| {
:conditions => conditions
}}
end
This way you can chain together multiple conditions in a manner that's forward-compatible with Rails 3:
query2 = Table.where(condition1).where(condition2).all
Rails 3 uses AREL to do most of the SQL computations so that's why it's much more flexible than Rails 2.

Related

Datamapper: Sorting results through association

I'm working on a Rails 3.2 app that uses Datamapper as its ORM. I'm looking for a way to sort a result set by an attribute of the associated model. Specifically I have the following models:
class Vehicle
include DataMapper::Resource
belongs_to :user
end
class User
include DataMapper::Resource
has n, :vehicles
end
Now I want to be able to query the vehicles and sort them by the name of the driver. I tried the following but neither seems to work with Datamapper:
> Vehicle.all( :order => 'users.name' )
ArgumentError: +options[:order]+ entry "users.name" does not map to a property in Vehicle
> Vehicle.all( :order => { :users => 'name' } )
ArgumentError: +options[:order]+ entry [:users, "name"] of an unsupported object Array
Right now I'm using Ruby to sort the result set post-query but obviously that's not helping performance any, also it stops me from further chaining on other scopes.
I spent some more time digging around and finally turned up an old blog which has a solution to this problem. It involves manually building the ordering query in DataMapper.
From: http://rhnh.net/2010/12/01/ordering-by-a-field-in-a-join-model-with-datamapper
def self.ordered_by_vehicle_name direction = :asc
order = DataMapper::Query::Direction.new(vehicle.name, direction)
query = all.query
query.instance_variable_set("#order", [order])
query.instance_variable_set("#links", [relationships['vehicle'].inverse])
all(query)
end
This will let you order by association and still chain on other scopes, e.g.:
User.ordered_by_vehicle_name(:desc).all( :name => 'foo' )
It's a bit hacky but it does what I wanted it to do at least ;)
Note: I'm not familiar with DataMapper and my answer might not be within the standards and recommendations of using DataMapper, but it should hopefully give you the result you're looking for.
I've been looking through various Google searches and the DataMapper documentation and I haven't found a way to "order by assocation attribute". The only solution I have thought of is "raw" SQL.
The query would look like this.
SELECT vehicles.* FROM vehicles
LEFT JOIN users ON vehicles.user_id = users.id
ORDER BY users.name
Unfortunately, from my understanding, when you directly query the database you won't get the Vehicle object, but the data from the database.
From the documentation: http://datamapper.org/docs/find.html. It's near the bottom titled "Talking directly to your data-store"
Note that this will not return Zoo objects, rather the raw data straight from the database
Vehicle.joins(:user).order('users.name').all
or in Rails 2.3,
Vehicle.all(:joins => "inner join users on vehicles.user_id = user.id", :order => 'users.name')

How do I combine ActiveRecord results from multiple has_many :through queries?

Basically, I have an app with a tagging system and when someone searches for tag 'badger', I want it to return records tagged "badger", "Badger" and "Badgers".
With a single tag I can do this to get the records:
#notes = Tag.find_by_name(params[:tag_name]).notes.order("created_at DESC")
and it works fine. However if I get multiple tags (this is just for upper and lower case - I haven't figured out the 's' bit either yet):
Tag.find(:all, :conditions => [ "lower(name) = ?", 'badger'])
I can't use .notes.order("created_at DESC") because there are multiple results.
So, the question is.... 1) Am I going about this the right way? 2) If so, how do I get all my records back in order?
Any help much appreciated!
One implementation would be to do:
#notes = []
Tag.find(:all, :conditions => [ "lower(name) = ?", 'badger']).each do |tag|
#notes << tag.notes
end
#notes.sort_by {|note| note.created_at}
However you should be aware that this is what is known as an N + 1 query, in that it makes one query in the outer section, and then one query per result. This can be optimized by changing the first query to be:
Tag.find(:all, :conditions => [ "lower(name) = ?", 'badger'], :includes => :notes).each do |tag|
If you are using Rails 3 or above, it can be re-written slightly:
Tag.where("lower(name) = ?", "badger").includes(:notes) do |tag|
Edited
First, get an array of all possible tag names, plural, singular, lower, and upper
tag_name = params[:tag_name].to_s.downcase
possible_tag_names = [tag_name, tag_name.pluralize, tag_name.singularize].uniq
# It's probably faster to search for both lower and capitalized tags than to use the db's `lower` function
possible_tag_names += possible_tag_names.map(&:capitalize)
Are you using a tagging library? I know that some provide a method for querying multiple tags. If you aren't using one of those, you'll need to do some manual SQL joins in your query (assuming you're using a relational db like MySQL, Postgres or SQLite). I'd be happy to assist with that, but I don't know your schema.

How to merge multiple 'having' clause relation in rails 3

Facing problem for merging multiple having clause ... As in my model i had two scopes written
1.)
scope :vacant_list, lambda {|daterange|
where("vacancies.vacancy_date" => daterange , "vacancies.availability" => ["Y" ,"Q"]).joins(:vacancies).group("vacancies.property_id").having("count(vacancies.vacancy_date) >= #{daterange.count}") unless daterange.blank?
}
2.)
scope :amenity_type, lambda {|term|
where("amenities.name" => term).joins(:amenities).group("amenities.property_id").having("count(amenities.name) >= #{term.size}") unless term.blank?
}
I need to do something like this
#model = Model.vacant_list(daterange).amenity_type(term)
But i always get wrong number of arguments (2 for 1)
/home/vivek/.rvm/gems/ruby-1.9.2-p180/gems/arel-2.0.10/lib/arel/select_manager.rb:100:in `having'
/home/vivek/.rvm/gems/ruby-1.9.2-p180/gems/activerecord-3.0.6/lib/active_record/relation/query_methods.rb:180:in `build_arel'
/home/vivek/.rvm/gems/ruby-1.9.2-p180/gems/activerecord-3.0.6/lib/active_record/relation/query_methods.rb:149:in `arel'.
If i remove any one of scopes having clause then the above action works perfectly .
Is there any way to like-
#model = Model.vacant_list(daterange) and then remove the active record relation and then apply #model.amenity_type(term).
I tried lots of things but didnt find any solution for this.....
I think you're doing this wrong - it took me quite a while to dig out what the actual intent of the 'having' clauses above was. From what I can tell, it looks like the idea is that you pass in an array of dates or amenities and want to find properties that match all of them.
The underlying issue is that (AFAIK) code like this will NOT do the right thing:
# NOTE: WILL NOT WORK
scope :vacant_on, lambda { |date| where('vacancies.vacancy_date' => date, "vacancies.availability" => ["Y" ,"Q"]).joins(:vacancies) }
scope :vacant_list, lambda {|daterange|
daterange.inject(self) { |rel, date| rel.vacant_on(date) }
}
Unless this has changed in Arel (haven't poked it much) then this fails because you end up with exactly one join to the vacancies table, but multiple where clauses that specify incompatible values. The solution is to do multiple joins and alias them individually. Here's how I used to do it in Rails 2:
named_scope :vacant_on, lambda { |date|
n = self.connection.quote_table_name("vacancies_vacant_on_#{date}")
{ :joins => "INNER JOIN vacancies AS #{n} ON #{n}.property_id = properties.id",
:conditions => ["#{n}.vacancy_date = ? AND #{n}.availability IN (?)", date, ["Y","Q"]] }
}
Explicitly specifying an 'AS' here lets multiple versions of this scope coexist in one query, and you get the results you'd expect.
Here's a rough translation of this to modern Arel syntax:
scope :vacant_on, lambda { |date|
our_vacancies = Vacancy.arel_table.alias
joins(our_vacancies).on(our_vacancies[:property_id].eq(Property.arel_table[:id])).
where(our_vacancies[:vacancy_date].eq(date),
our_vacancies[:availability].in(["Y" ,"Q"]))
}
Haven't tried it, but this post:
http://www.ruby-forum.com/topic/828516
and the documentation seem to imply it would do the right thing...

rails - activerecord ... grab first result

I want to grab the most recent entry from a table. If I was just using sql, you could do
Select top 1 * from table ORDER BY EntryDate DESC
I'd like to know if there is a good active record way of doing this.
I could do something like:
table.find(:order => 'EntryDate DESC').first
But it seems like that would grab the entire result set, and then use ruby to select the first result. I'd like ActiveRecord to create sql that only brings across one result.
You need something like:
Model.first(:order => 'EntryDate DESC')
which is shorthand for
Model.find(:first, :order => 'EntryDate DESC')
Take a look at the documentation for first and find for details.
The Rails documentation seems to be pretty subjective in this instance. Note that .first is the same as find(:first, blah...)
From:http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M002263
"Find first - This will return the first record matched by the options used. These options can either be specific conditions or merely an order. If no record can be matched, nil is returned. Use Model.find(:first, *args) or its shortcut Model.first(*args)."
Digging into the ActiveRecord code, at line 1533 of base.rb (as of 9/5/2009), we find:
def find_initial(options)
options.update(:limit => 1)
find_every(options).first
end
This calls find_every which has the following definition:
def find_every(options)
include_associations = merge_includes(scope(:find, :include), options[:include])
if include_associations.any? && references_eager_loaded_tables?(options)
records = find_with_associations(options)
else
records = find_by_sql(construct_finder_sql(options))
if include_associations.any?
preload_associations(records, include_associations)
end
end
records.each { |record| record.readonly! } if options[:readonly]
records
end
Since it's doing a records.each, I'm not sure if the :limit is just limiting how many records it's returning after the query is run, but it sure looks that way (without digging any further on my own). Seems you should probably just use raw SQL if you're worried about the performance hit on this.
Could just use find_by_sql http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M002267
table.find_by_sql "Select top 1 * from table ORDER BY EntryDate DESC"

Better Performance on Associations

Right now I have a table called Campaigns that has many Hits, if I call say:
Campaign.find(30).hits
Which takes 4 seconds, or 4213 ms.
If I call this instead:
campaign = Campaign.find(30)
campaign.hits.count
Does it still load all of the hits, then count? Or does it see I am counting and avoids loading all of the hits? (Which is currently 300,000+ rows).
I am trying to figure out a smart way to load/count my hits. I am thinking about adding a method to my Campaign.rb model, like:
def self.total_hits
find :first, :select => 'COUNT(id) as hits', :conditions => ["campaign_id = ?", self.id]
end
I know that query won't load from the hits table, but that is just an example of counting it from a self made query, apposed to Ruby on Rails doing this for me.
Would this memcache query be more effecient? (I have it running, but doesn't seem to be any better/faster/slower, just the same speed.)
def self.hits
Rails.cache.fetch("Campaign_Hits_#{self.campaign_id}", :expires_in => 40) {
find(:first, :select => 'COUNT(id) as hits', :conditions => ["campaign_id = ?", self.campaign_id]).hits
}
end
Any suggestions would be great!
How about:
Campaign.find(30).hits.count
You might also consider adding the following in hit.rb (assuming a one-to-many relationship between campaigns and hits).
belongs_to :campaign, :counter_cache => true
You then need a column in the campaigns table called hits_count. This will avoid hitting hits altogether if you're only getting the count.
You can check the API for the full rundown.
My ActiveRecord might be a little rusty, so forgive me if so, but IIRC Campaign.find(30).hits is at least two separate queries. How does Campaign.find(30, :include => [ :hits ]).hits do? That should perform a single query.

Resources