Consequent attribute calculations with a queuing system - ruby

For all of the following assume these:
rails v3.0
ruby v1.9
resque
We have 3 models:
Product belongs_to :sku, belongs_to :category
Sku has_many :products, belongs_to :category
Category has_many :products, has_many :skus
When we update the product (let's say we disable it) we need to have some things happen to the relevant sku and category. The same is true for when a sku is updated.
The proper way of achieving this is have an after_save on each model that triggers the other models' update events.
example:
products.each(&:disable!)
# after_save triggers self.sku.products_updated
# and self.category.products_updated (self is product)
Now if we have 5000 products we are in for a treat. The same category might get updated hundreds of times and hog the database while doing so.
We also have a nice queueing system, so the more realisting way of updating products would be products.each(&:queue_disable!) which would simply toss 5000 new tasks to the working queue. The problem of 5000 category updates still exists though.
Is there a way to avoid all those updates on the db?
How can we concatenate all the category.products_updated for each category in the queue?

You can ensure a single category update for all the products by using a couple Resque plugins: Resque Unique Job and Resque Scheduler.
Delay the execution of the job to update the category slightly (however long it takes to typically call all the product updates) and ensure each job is unique by including the Unique Job module. Unique Job uses the paramaters of the job, so if you try to queue 2 jobs with category_id 123, it ignores the 2nd one since the job is already queued.
class Product
after_save :queue_category_update
def queue_category_update
Resque.enqueue_at(1.minute.from_now, Jobs::UpdateCategory, category.id) if need_to_update_category?
end
end
module Jobs
module UpdateCategory
include Resque::Plugins::UniqueJob
def self.perform(category_id)
category = Category.find_by_id(category_id)
category.update_some_stuff if category
end
end
end

Do the dependent updates in single SQL calls. #update_all will update many records at once. For example,
In an after_update callback, update all the dependent column values:
class Category
after_update :update_dependent_products
def update_dependent_products
products.update_all(disabled: disabled?) if disabled_changed?
end
end
If that's too slow, move it into a resque job:
class Category
after_update :queue_update_dependent_products
def update_dependent_products
products.update_all(disabled: disabled?) if disabled_changed?
end
def queue_update_dependent_products
Resque.enqueue(Jobs::UpdateCategoryDependencies, self.id) if disabled_changed?
end
end
class Jobs::UpdateCategoryDependencies
def self.perform(category_id)
category = Category.find_by_id(category_id)
category.update_dependent_products if category
end
end
Do similar things for the other model callbacks.

Related

Setting up a many-to-many relationship in Active Record

I have two models: meal_plans and dinners. Each meal_plan will have a week's worth of dinners, and each dinner will be on, potentially, several meal_plans.
I'm wondering what the best approach is to save 7 dinner IDs into a meal plan. Initially, I was just going to save an array of dinner IDs into a meal plan's params with a helper:
def create_week_of_meals(week)
ids = []
week.each {|dinner| ids.push(dinner.id)}
return ids
end
With strong params:
params.require(:meal_plan).permit(:user_id, :meals)
This works alright, but it leaves me with a string of meal_ids which I would have to turn back into an array and then query Dinners for each one of those dinner ids. Is there a better way of doing this? I've seen a lot of references to rails accepts_nested_attributes_for but from what I can tell that mostly deals with saving some attributes from another model into the current record, whereas I'm looking to save a reference to several models.
I am familiar with has_many_through relationships but it seems like a lot of overhead to create a separate model and seven new records for each meal plan just to attach some dinner_ids to a record.
class Dinner < ActiveRecord::Base
has_many :meal_plan_placements
has_many :meal_plans, through: :meal_plan_placements
end
class MealPlan < ActiveRecord::Base
has_many :meal_plan_placements
has_many :dinners, through: :meal_plan_placements
end
class MealPlanPlacement < ActiveRecord::Base
belongs_to :dinner
belongs_to :meal_plan
end
This should work, but I haven't actually run it locally, so you should play around with it. You can also read more about the through option.

How to get row with max sum?

I have a technicians table, a clients table and a jobs table. My technicians model looks like this:
class Technician < ActiveRecord::Base
has_many :jobs
has_many :clients, through: :job_orders
end
My jobs model looks like this:
class Job < ActiveRecord::Base
belongs_to :technician
belongs_to :client
end
And my clients model looks like this:
class Client < ActiveRecord::Base
has_many :job_orders
has_many :technicians, through: :jobs
end
I am able to pull a list of how many jobs a technician has performed by doing
techs = Technician.all
techs.each do |tech|
puts "#{tech.name} has been assigned #{tech.jobs.count} jobs"
end
Now how can I see the technician with the fewest jobs, or the client with the most job requests? I've been thinking of sorting by sum asc/desc, but I haven't been able to wrap my mind around the issue. I am not using rails, just plain old Ruby with the activerecord gem.
Client.joins(:job_orders).order('COUNT(job_orders) DESC').group(:id).limit(1) will give you the Client with the most amount of job orders, Technician.joins(:jobs).order('COUNT(jobs) ASC').group(:id).limit(1) will give you the technicians with the fewest amount of jobs
Edit: For Rails versions < 6.0

Using decrement_counter in after_update callback is subtracting 2?

I have three models. Employer, User, Job.
class Employers
has_many :jobs
has_many :users, through: :jobs
end
class User
has_many :jobs
end
class Job
belongs_to :user
belongs_to :employer
end
The Job model has a boolean column named "current". An employers user count is derived by counting all the associated jobs marked 'current'.
I opted to rolled my own cache counter, rather than use active records.
Im using a before filter in the Job model to either increment or decrement a users_count in the Employer model. The increment works as expected, but no matter how I tweak the code...the decrement drops the count by a value of 2.
Im sure I can clean these methods up a bit...there might be some redundancy.
1 Why is the decrement subtracting 2 instead of 1?
2 Can the active record cache counter handle logic like this?
class Job
before_destroy :change_employer_users_counter_cache_after_destroy
before_create :change_employer_users_counter_cache_after_create
before_update :change_employer_users_counter_cache_after_update
def change_employer_users_counter_cache_after_create
Operator.increment_counter(:users_count, self.operator_id) if self.current == true
end
def change_employer_users_counter_cache_after_update
if self.current_changed?
if self.current == true
Operator.increment_counter(:users_count, self.operator_id)
else
Operator.decrement_counter(:users_count, self.operator_id)
end
end
end
def change_employer_users_counter_cache_after_destroy
Operator.decrement_counter(:users_count, self.operator_id)
end
end
the gem "counter_culture" handled this very nicely...and cleaned up my code.

Is there a way in MongoMapper to achieve similar behavior as AR's includes method?

Is there a feature equivalent in MongoMapper to this:
class Model < ActiveRecord::Base
belongs_to :x
scope :with_x, includes(:x)
end
When running Model.with_x, this avoids N queries to X.
Is there a similar feature in MongoMapper?
When it's a belongs_to relationship, you can turn on the identity map and run two queries, once for your main documents and then one for all the associated documents. That's the best you can do since Mongo doesn't support joins.
class Comment
include MongoMapper::Document
belongs_to :user
end
class User
include MongoMapper::Document
plugin MongoMapper::Plugins::IdentityMap
end
#comments = my_post.comments # query 1
users = User.find(#comments.map(&:user_id)) # query 2
#comments.each do |comment|
comment.user.name # user pulled from identity map, no query fired
end
(Mongoid has a syntax for eager loading, but it works basically the same way.)

How can I guarantee before_destroy callback order in autosave records?

I'm implementing an audit-trail-esque system in my rails3 app; basically, I want to keep track of exactly when my users make any changes.
Currently facing an issue where I have two models - one child to the other. Pseudocode follows:
class Audit;end #this is a straightforward data-storing model, trust me!
class Parent
has_many :children, :dependent => :destroy
before_destroy :audit_destroy
def audit_destroy
Audit.create(:metadata => "some identifying data")
end
end
class Child
belongs_to :parent
before_destroy :audit_destroy
def audit_destroy
Audit.create(:metadata => "some identifying data")
end
end
On calling
Parent.destroy
I'm expecting two new Audit records to be created with created_at timestamps that are ordered relative to the parent records. Said slightly differently: the parent record's Audit is created before the child record's Audit.
This doesn't, however, seem to be guaranteed, as I haven't explicitly said anything about the order of creation of the audit records. While it seems to usually hold true, I have confirmed that sometimes the order of the creation of the Audit records is inverted.
Is there some magic in the depths of ActiveRecord surrounding the ordering of before_destroy callbacks creating new records and autosave?
Thanks,
Isaac

Resources