Update related items on save?

Hi!

I have an app with products, categories, subcategories and resellercategories.

Products…

belongs_to :resellercategory
has_one :subcategory, :through => :resellercategory
has_one :category, :through => :subcategory

So, there are a lot of resellercategories that are (manually) mapped to my fewer subcategories.

I have a before_save in my products model that sets if the product is complete:

def check_complete
self.complete = !image_small.blank? && !subcategory.blank?
nil
end

So, if the resellercategory that the product is related to is mapped to a subcategory, the product is marked as “complete”. However, at the moment it is only updated when the product is saved, I also want it to be updated when the resellercategory is mapped to a subcategory.

Is there any good way to do this? Should I create an after_save in my resellercategory that iterates through all the products and re-saves all of them to get the field updated?
Any easier/more effective way?

Regards
Linus

Hi!

I have an app with products, categories, subcategories and resellercategories.

Products…

belongs_to :resellercategory
has_one :subcategory, :through => :resellercategory
has_one :category, :through => :subcategory

So, there are a lot of resellercategories that are (manually) mapped to my fewer subcategories.

I have a before_save in my products model that sets if the product is complete:

def check_complete
self.complete = !image_small.blank? && !subcategory.blank?
nil
end

So, if the resellercategory that the product is related to is mapped to a subcategory, the product is marked as “complete”. However, at the moment it is only updated when the product is saved, I also want it to be updated when the resellercategory is mapped to a subcategory.

Is there any good way to do this? Should I create an after_save in my resellercategory that iterates through all the products and re-saves all of them to get the field updated?
Any easier/more effective way?

I am not sure I follow exactly, but …

Answer 1:

I assume this

" … when the resellercategory is mapped to a subcategory …"

creates (or modifies) a ResellerCategory object (and saves it

later to the database)?

So as you suggest:

“… Should I create an after_save in my resellercategory that …”

updates all associated products. Instead of

“… iterates through all the products and re-saves all of them to get the field updated? …”

you could use an update_all , but that seems quite low-level database

hacking (no models instantiated, no before/after filters …).

http://apidock.com/rails/ActiveRecord/Relation/update_all

#untested code !!

Product.where(:reseller_category_id => reseller_category.id).update_all(:complete => true)

This does not feel comfortable, but it would probably work …

Answer 2:

In reality, the Product.complete column is a de-facto cache for information

that is already present in the database. Unless your have real performance

problems associated with it, I would try to make a scope

class Product < ActiveRecord::Base

scope :complete …

that limits the products to those that are complete (that is, they have

a category and a small_image). Then Product.complete.where. …

will inlcude the correct SQL to only select the complete products,

without the implicit caching problem that you are facing now.

Very true:

There are only two hard things in Computer Science: cache

invalidation and naming things.

Phil Karlton

HTH,

Peter

Thank you Peter. I have some follow up questions.

Let’s say I would use a scope. How could I do that? (let’s ignore the “image_small” for now and focus on the subcategory) There is nothing in the product table that I can use to select the completed products. It is only regarded as completed if the related resellercategory is associated to a subcategory. So, in the scope I would need to join the resellercategories table and check if that is associated with a subcategory.

Feels a bit unnecessary to do that for a small thing as this. But maybe it won’t affect the performance that much.

The other option I was thinking of was just to add an after_filter in resellercategory model like this:

def update_products
self.products.each(&:save)
end

I’ll see if I can get the scope to work first.

Regards
Linus

Thank you Peter. I have some follow up questions.

Let’s say I would use a scope. How could I do that? (let’s ignore the “image_small” for now and focus on the subcategory) There is nothing in the product table that I can use to select the completed products. It is only regarded as completed if the related resellercategory is associated to a subcategory. So, in the scope I would need to join the resellercategories table and check if that is associated with a subcategory.

Well, this article

http://ablogaboutcode.com/2011/02/19/scopes-in-rails-3/

seems to indicate it can be done quite easily

class User

scope :by_age, lambda do |age|

joins(:profile).where(‘profile.age = ?’, age) unless age.nil?

end

scope :by_name, lambda{ |name| where(name: name) unless name.nil? }

scope :by_email, lambda do |email|

joins(:profile).where(‘profile.email = ?’, email) unless email.nil?

It joins user.profile to check on profile.age or profile.email …

I have not tested it, but seems not too difficult.

Feels a bit unnecessary to do that for a small thing as this. But maybe it won’t affect the performance that much.

In my view, it is relevant, since it reduces the risk of cache inconsistency, which

is a much harder problem to solve when your application grows. But I have had

discussions with colleagues on this before and some actually preferred caching

such implicit caching columns in local tables, to have simpler reporting on those

tables later on (but the risk on inconsistency was always there).

The other option I was thinking of was just to add an after_filter in resellercategory model like this:

def update_products
self.products.each(&:save)
end

Yes, this will work.

But for any implicit caching option (also the update_all), you need to know all the models

where the implicit product.complete cache is affected and have this cache update code

there (currently that would only be the ResellerCategory model, but it can grow).

The 3 options will work, but all have pros and cons:

  • Product :complete scope (no implicit caching)

  • Product update_all (sql level)

  • ResellerCategory after_save update_products (ActiveRecord level, but many queries)

HTH,

Peter

Thank you.

I got the scopes working so now I can skip the extra field :slight_smile:

Best Regards

Linus