Rails-y way to query a model with a belongs_to association - ruby-on-rails

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.

Related

activerecord how to query for multiple conditions on association?

I am trying to make a scope for my EventsLog model which looks something along the lines of EventsLog.with_values({"value_name" => "value", "other_value_name" => "other_value"}).
The results of which would be the EventsLog records that have an associated EventsLogValue for each of the key-value pairs in the hash.
Here is what I have to work with.
Two tables whose definitions look like this:
--table for tracking events
CREATE TABLE events_log(
id INT PRIMARY KEY IDENTITY(1,1),
event_name VARCHAR(25), --name of the event
created_at DATETIME
);
--table for tracking the values corresponding to the event
CREATE TABLE events_log_values(
id INT PRIMARY KEY IDENTITY(1,1),
event_id INT,
value VARCHAR(255),
value_name VARCHAR(25),
);
From these two tables two models which look like:
class EventsLog < BaseAPIDatabase
self.table_name = "events_log"
self.primary_key = "id"
has_many :events_log_values, :foreign_key => "event_id", :primary_key => "id", :class_name => "EventsLogValue", :autosave => true
scope :since, ->(since){ where("created_at > ?", since)}
scope :named, ->(event_name){ where(:event_name => event_name) }
def values
events_log_values.inject({}) do |hsh, v|
hsh.merge({v.value_name => v.value})
end
end
end
class EventsLogValue < BaseAPIDatabase
self.table_name = "events_log_values"
self.primary_key = "id"
end
My approach so far has been to try and create a function that returns an active record relation which has applied one key-value pair at a time and then later to add a scope (or probably just a class method returning a relation) which chains them for me (something along the lines of scope :with_values, ->(values){values.inject(self){|slf, (k, v)| slf.with_value(k, v)} }).
Originally I tried to implement with_value as a fairly standard scope, scope :with_value, ->(val_name, val){ eager_load(:events_log_values).where(:events_log_values => {:value_name => val_name, :value => val}) }, which works fine by itself but when chained results in a single join with multiple conditions on the joined values.
Deciding that this would be solved by joining the values table with an alias for each condition; my new approach has been to define a has_many association in my with_value function then to eager_load that association and to add a where condition based on each new association:
def self.with_value(val_name, val)
has_many val_name.to_sym, ->(){ where(:value_name => val_name) }, :foreign_key => "event_id", :primary_key => "id", :class_name => "EventsLogValue"
res = eager_load(:events_log_values)
res.eager_load(val_name.to_sym).where("#{val_name.pluralize}_events_log" => {:value => val})
end
This actually works pretty well but has a few problems. The first one being that I have a difficult time knowing what the name in the where condition is going to be for the association. The second (and bigger problem) being that my values function now only has whatever value_names have not had an association made for them.
Here is some sql which was generated by the multiple has_manys and may help to illustrate what I am trying to do:
EventsLog.with_values("hello" => "world", "foo" => "bar").to_sql
SELECT ...
FROM [events_log]
LEFT OUTER JOIN [events_log_values] ON [events_log_values].[event_id] = [events_log].[id]
LEFT OUTER JOIN [events_log_values] [hellos_events_log] ON [hellos_events_log].[event_id] = [events_log].[id] AND [hellos_events_log].[value_name] = 'hello'
LEFT OUTER JOIN [events_log_values] [foos_events_log] ON [foos_events_log].[event_id] = [events_log].[id] AND [foos_events_log].[value_name] = 'foo'
WHERE [hellos_events_log].[value] = 'world' AND [foos_events_log].[value] = 'bar'
How can I go about getting a record which has several associated records meeting several separate conditions?
This is the answer I have been able to come up with since asking my question. It uses arel to generate sql joins with aliases for each of the values and also generates a where condition for each value.
It's not the cleanest thing but it seems to get the job done.
def self.with_values(values)
el = EventsLog.arel_table
arel_joins = el
arel_wheres = []
values.each do |k, v|
ev = EventsLogValue.arel_table.alias("#{k}_join")
arel_joins = arel_joins.join(ev).on(el[:id].eq(ev[:event_id]).and(ev[:value_name].eq(k)))
arel_wheres << ev[:value].eq(v)
end
arel_wheres.inject(EventsLog.joins(arel_joins.join_sources)){|rel, con| rel.where(con)}
end
p.s. I think I read somewhere that Model.arel_table is undocumented and should not be used? It may be prudent to use Arel::Table.new('table_name') instead.

Joining two ActiveRecord associations on common attribute

Let's say I have a User model. User has 2 has_many associations, that is User has many pencils and has many cars. Cars and Pencils table has same attribute, :date, and separate such as :speed(car) and :length(pencil). I want to join a user's pencils and cars on their common attribute, :date, so that I have an array/relation [:date, :speed, :length]. How do I achieve that, I tried joins and merge but they were no use.
I'd definitely recommend getting this into a query rather than a loop, for efficiency's sake. I think this will work:
Car.joins(:user => :pencils).where("pencils.date = cars.date")
And if you want to reduce it to the array immediately:
Car.joins(:user => :pencils).where("pencils.date = cars.date").pluck("cars.date", "cars.speed", "pencils.length")
If you need to include matches where date is nil, you might need to add:
Car.joins(:user => :pencils).where("(pencils.date = cars.date) OR (pencils.date IS NULL AND cars.date IS NULL)")
Many more efficient options exist, but here is one possible approach:
class User < ActiveRecord::Base
def get_merged_array
dates = (cars.map(&:date) & pencils.map(&:date))
results = []
dates.each do |date|
cars.where(date: date).each do |car|
pencils.where(date: date).each do |pencil|
results << [date, car.speed, pencil.length]
end
end
end
results
end
end

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 find conditions... where attribute is not a database column

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.)

How to coerce type of ActiveRecord attribute returned by :select phrase on joined table?

Having trouble with AR 2.3.5, e.g.:
users = User.all( :select => "u.id, c.user_id", :from => "users u, connections c",
:conditions => ... )
Returns, e.g.:
=> [#<User id: 1000>]
>> users.first.attributes
=> {"id"=>1000, "user_id"=>"1000"}
Note that AR returns the id of the model searched as numeric but the selected user_id of the joined model as a String, although both are int(11) in the database schema.
How could I better form this type of query to select columns of tables backing multiple models and retrieving their natural type rather than String ? Seems like AR is punting on this somewhere. How could I coerce the returned types at AR load time and not have to tack .to_i (etc.) onto every post-hoc access?
It's unfortunately not going to happen very easily. All of the data from the DB connection comes to rails as strings, the conversion of types happens in each of the dynamic attribute methods that rails creates at runtime. It knows which attributes to convert to which type by the table's column-type meta-data that it retrieves when the app starts. Each model only has column meta-data for it's own columns, that's why it's own columns end up with correct type. There is no easy way to auto-convert to the correct types.
You could on the other hand, create a simple conversion method that would take a Hash and automatically convert the attributes.
Something like this:
users = User.all(:select => "cl, comments.c2", ...)
users = convert_columns(users, 'c2' => :integer, 'other_column' => :date)
def convert_columns(records, columns = {})
records.each do |rec|
columns.each do |col, type|
rec[col] = case type
when :int then rec[col].to_i
when :date then ........
....
end
end
end
end
Why are you using :from => "users" inside a User.method ?
The following will do an inner join (which is what you are doing anyways)
users = User.all(:include => :connections, :select => "users.id, connections.user_id", :conditions => {...})
This is going to be very heavy query for the database.
Faster query would be with the outer join though.
This will also return the keys as INT not STRING
A much faster alternative was
Connection.all(:include => :user, :conditions => {...}).collect {|e| [e.user_id, e.id] }
This gives you an array of arrays with the ids. If you are going to select "id, user_id" columns only, then it may not necessarily be as AR object. An array can be faster.
I hope I am not missing some point here. Suggest me, if I am.
If you want quick solution - try to use after_find callback and preset correct attributes types there:
class User < ActiveRecord::Base
after_find :preset_types
private
def preset_types user
user.user_id = user.user_id.to_i
end
end

Resources