performing batch actions on model associated objects - ruby-on-rails

In an application, a country model contains many cities, and there are two methods which performs actions on all the cities.
class Country < ActiveRecord::Base
has_many :cities
def destroy_cities
self.cities.each do |city|
city.destroy
end
end
def update_cities(new_status)
self.cities.each do |city|
city.status = new_status
end
end
end
I wonder if there are easier ways to write those methods, like a non-existant self.cities.destroy and self.cities.status = new_status
On the efficiency standpoint, it would perform those SQL queries:
DELETE FROM cities WHERE country_id=#{country_id}
UPDATE cities SET status=#{new_status} WHERE country_id=#{country_id}
Instead of running multiple queries:
SELECT * FROM cities WHERE country_id=#{country_id}
DELETE FROM cities WHERE id=#{city_ids[0]}
DELETE FROM cities WHERE id=#{city_ids[1]}
DELETE FROM cities WHERE id=#{city_ids[2]}
...

You should be able to use the delete_all and update_all activerecord methods.
If you simply want to Keep the country but delete all cities you could do;
def destroy_cities
self.cities.delete_all
end
http://apidock.com/rails/ActiveRecord/Relation/delete_all
and for updating you could do;
def update_cities(new_status)
self.cities.update_all(:status => new_status)
end
http://apidock.com/rails/ActiveRecord/Relation/update_all

Related

Rails: Finding matching attribute in a has-many / belongs-to association

Say I have a Restaurant and a Reservation model. I want to find the reservations for each restaurant according to restaurant id, something like this:
#reservations = Reservation.find( # current user )
#restaurants = []
#reservations.each do |res|
#restaurants += Restaurant.where('id like ?', res.rest_id)
end
When trying this, this constructs an array, which I've tried to convert to an Active Record object un-successfully. Am I missing something, or is there a more obvious way to do this ?
restaurant.rb
has_many :reservations
reservation
belongs_to :restaurant, class_name: 'Restaurant', foreign_key: 'rest_id'
You can find restaurant record for this reservation as
#reservation = Reservation.joins(:restaurant).where(id: reservation_id)
#load all reservations matching your condition, and eager-load all associated restaurants
#reservations = Reservation.where(<some condition>).includes(:restaurants)
#get all associated restaurants for all objects in the #reservations collection
#which have been eager loaded, so this won't hit the db again),
#flatten them to a single array,
#then call `uniq` on this array to get rid of duplicates
#restaurants = #reservations.map(&:restaurants).flatten.uniq
EDIT - added flatten, added explanation in comments

Rails 4.2: Eager-loading has_many relation with STI

Let's say I have a relation in Rails to a table that uses STI like:
class Vehicle < ActiveRecord::Base; end
class Car < Vehicle; end
class Truck < Vehicle; end
class Person < ActiveRecord::Base
has_many :cars
has_many :trucks
has_many :vehicles
end
... and I want to load a Person and all of its cars and trucks in one query. This doesn't work:
# Generates three queries
p = Person.includes([:cars, trucks]).first
... and this is close, but no luck here:
# Preloads vehicles in one query
p = Person.includes(:vehicles).first
# and this has the correct class (Car or Truck)
p.vehicles.first
# but this still runs another query
p.cars
I could do something like this in person.rb:
def cars
vehicles.find_all { |v| v.is_a? Car }
end
but then Person#cars isn't a collection proxy anymore, and I like collection proxies.
Is there an elegant solution to this?
EDIT: Adding this to Person gives me the items I want in arrays with one query; it's really pretty close to what I want:
def vehicle_hash
#vehicle_hash ||= vehicles.group_by {|v|
v.type.tableize
}
end
%w(cars trucks).each do |assoc|
define_method "#{assoc}_from_hash".to_sym do
vehicle_hash[assoc] || []
end
end
and now I can do Person.first.cars_from_hash (or find a better name for my non-synthetic use case).
When you use includes, it stores those loaded records in the association_cache, which you can look at in the console. When you do p = Person.includes(:vehicles), it stores those records as an association under the key :vehicles. It uses whatever key you pass it in the includes.
So then when you call p.cars, it notices that it doesn't have a :cars key in the association_cache and has to go look them up. It doesn't realize that Cars are mixed into the :vehicles key.
To be able to access cached cars as either through p.vehicles OR p.cars would require caching them under both of those keys.
And what it stores is not just a simple array—it's a Relation. So you can't just manually store records in the Hash.
Of the solutions you proposed, I think including each key is probably the simplest—code-wise. Person.includes(:cars, :trucks) 3 SQL statements aren't so bad if you're only doing it once per request.
If performance is an issue, I think the simplest solution would be a lot like what you suggested. I would probably write a new method find_all_cars instead of overwriting the relation method.
Although, I would probably overwrite vehicles and allow it to take a type argument:
def vehicles(sti_type=nil)
return super unless sti_type
super.find_all { |v| v.type == sti_type }
end
EDIT
You can get vehicles cached by Rails, so you probably can just rely on that. Your define_methods could also do:
%w(cars trucks).each do |assoc|
define_method "preloaded_#{assoc}" do
klass = self.class.reflect_on_all_associations.detect { |assn| assn.name.to_s == assoc }.klass
vehicles.select { |a| a.is_a? klass }
end
end
Even if you don't use includes, the first time you call it, it will cache the association—because you're selecting, not whereing. You still won't get a Relation back, of course.
It's not really that pretty, but I like that it's contained to one method that doesn't depend on any other ones.

Avoid scope hitting database if association already loaded

I have 2 models like so:
class Country < ActiveRecord::Base
has_many :cities
end
class City < ActiveRecord::Base
belongs_to :country
scope :big, where("population > 1000000")
end
Then, in the code I load a country with it's cities, like so:
country = Country.include(:cities).find(id)
But when I execute:
country.cities.big
It makes a hit to the db with this query:
SELECT * FROM cities where country_id = 1 AND population > 1000000
Which works fine, but it's not necessary since the cities where all already loaded by the :include.
Is there a way to tell the scope to not hit the db if the association is already loaded?
I can do it with an association extension, but not for a regular scope. On extensions I do something like:
has_many :cities do
def big
if loaded?
detect {|city| city.population > 1000000}
else
where("population > 1000000")
end
end
end
But this would be repeating the scope in 2 places and I want to reuse the scope on the city model.
The scope logic uses methods that work with Arel under the hood, and ruby Enumerables don't know how to use them. You may be able to refactor your logic to an abstraction that can be translated to use either the Arel or Enumerable methods, but this won't always be possible:
def self.build_scope(abstracted)
where(abstracted.map(&:to_s).join(' '))
end
def self.build_enum(abstracted)
select{|city| city.send(abstracted[0]).send(*abstracted[1..2]) }
end
def self.abstract_big
[:population, ">", 10000]
end
scope :big_scope, build_scope(abstract_big)
def self.big_enum
build_enum abstract_big
end
You could then do:
country.cities.big_enum
A much better idea would be to only eagerly load according to the scope that you want (if you know it in advance):
country = Country.include(:cities).merge(City.big).find(id)

Combining scoped activerecord queries in rails

I'm trying to build a facebook style feed of items for a user. The feed will contain recent notes (on books) made by a user or people the user follows combined with other notifications such as "user x that you follow started reading a new book". You get the idea.
So far I have a scope in my Note class which returns the notes I want:
class Note < ActiveRecord::Base
scope :from_users_followed_by, lambda { |user| followed_by user }
def self.followed_by(user)
followed_ids = %(SELECT followed_id FROM relationships WHERE follower_id = :user_id)
where("user_id IN (#{followed_ids}) OR user_id = :user_id", { :user_id => user })
end
end
and a similar scope in my Readings class which returns records built when user starts reading a book:
class Reading < ActiveRecord::Base
scope :from_users_followed_by, lambda { |user| followed_by(user) }
def self.followed_by(user)
# is this not at risk of sql injection??
followed_ids = %(SELECT followed_id FROM relationships WHERE follower_id = :user_id)
# return readings where user_id IN (an array of user_ids that the user follows)
where("reader_id IN (#{followed_ids}) OR reader_id = :user_id", { :user_id => user })
end
end
Now this works fine and I can get arrays of objects from these no problem. I'm struggling to combine the two queries into a feed which is correctly ordered by creation time. The best I can do at the moment is my user class with a combined feed method:
class User < ActiveRecord::Base
def combined_feed
feed = Note.from_users_followed_by(self) | Reading.from_users_followed_by(self)
feed.sort! do |a, b|
a.created_at <=> b.created_at
end
feed.reverse
end
end
Which gets me a combined feed but strikes me as being horrendously inefficient. How can I do the equivalent at the database level in rails?
I think I would probably create an entirely separate model called FeedItem. Then, when certain events occur (such as the creation of a new note), you just create a new FeedItem record. Then you only have one table to query from and it will already be in the correct order.

Ruby on Rails: how do you write a find statement that sums up a bunch of values if an object belongs to another object?

Lets say Users have BankAccounts which have a balance value.
So, how do I generate an array that Users and the total value of all their BankAccounts' values added together?
I'm not sure if this is quite what you want, but you can do something like:
ActiveRecord::Base.connection.select_all("select user_id, sum(balance) from accounts group by user_id;")
This will give you an array of user_ids and balances from the accounts table. The advantage of doing it this way is that it comes down to only one SQL query.
You'll want to do something like this. I don't believe it's possible to use #sum via an association.
class User
def net_worth
BankAccount.sum(:balance, :conditions => ["user_id = ?", self.id])
end
end
Edit
I see a reference to a #sum in AssociationCollection, so try this:
class User
def net_worth
self.bank_accounts.sum(:balance)
end
end
(I haven't tested this code)
First you need to find the users you want so I'll just assume you want all users.
#users = User.all
Then you need to take the array and collect it with only the elements you want.
#users.collect! {|u| [u.name, u.bank_account_total_value]}
For this kinda attribute I would set it in the model assuming you have has_many :transactions as an association
Class User
has_many :transactions
def bank_account_total_value
total = 0
self.transactions.each do |t|
total += t.amount
end
end
end

Resources