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

On Tue, Jan 3, 2012 at 11:14 PM, Linus P. <
[email protected]> wrote:

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


Peter V.
http://twitter.com/peter_v
http://rails.vandenabeele.com

On Tue, Jan 3, 2012 at 11:55 PM, Linus P. <
[email protected]> wrote:

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


Peter V.
http://twitter.com/peter_v
http://rails.vandenabeele.com

Thank you.

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

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

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