Question on updating 'child' records

Hi there,

I have a Rails app with several has_many relationships, which in turn have several has_many relationships.

Let's say that I have an 'enabled' field in each table, that can be either true or false.

If I disable a Widget, not only do I want the Widget's enabled column to be set to false, I also want it's 'children's' enabled column to be set to false.

I want the ability to disable something and then re-enable it, and have its dependents follow suit. So, as a lame example:

A Dog has_many Dishes a Dog has_many Collars

A Dish has_many Kibble A Collar has_many Leashes

If I disable the Dog, I want all of its Dishes, Collars, Kibble, and Leashes disabled. Then, I want to be able to go back in and enable the Dog - if I do this, I want its Dishes, Collars, Kibble, and Leashes enabled.

I've been looking at several things, including nasty, resource heavy recursion using reflections and the collect method to grab ids, as well as trying to wrap my head around whether or not callbacks would be helpful in this situation.

Can anyone please please *please* point me in the right direction?

Thanks in advance - it's greatly appreciated!

Samantha

I would address the issue with an after_save filter in the model. Have a look at it's own self.enabled value and update the children to match.

Thank you so much, Greg. I really appreciate it. I'll post back if I need anymore assistance, as well as if that took care of it for me!

Again, I can't tell you how much I appreciate your response - I've been banging my head on this for way too long. :slight_smile:

Thanks, Samantha

I meant to ask...

Will something like this work across the board? (ie, I don't want to specify that if I'm updating a X, then Y and Z should be updated... or if I'm updating Z, then A and B should be updated...)

So, am I able to write an after_save method that will say, "Hey, I'm a Car that's getting disabled. I need to deactivate my radio." Or, "Hey, I'm a Driver that's being disabled. I need to deactivate my car."

Or, do I need to specify after_save methods for each and every model, so that every instance is spelled out?

Thanks again!

Samantha

Will something like this work across the board? (ie, I don't want to specify that if I'm updating a X, then Y and Z should be updated... or if I'm updating Z, then A and B should be updated...)

So, am I able to write an after_save method that will say, "Hey, I'm a Car that's getting disabled. I need to deactivate my radio." Or, "Hey, I'm a Driver that's being disabled. I need to deactivate my car."

Or, do I need to specify after_save methods for each and every model, so that every instance is spelled out?

If you keep that same "enabled" field name throughout you could use an ActiveRecord mixin to add that functionality once to all models:

module Disable   def self.append_features( base )     base.after_save do |model|       model.children.each do |c|         if model.respond_to?( :enabled ) && c.respond_to?( :enabled )           if c.enabled != model.enabled             c.enabled = model.enabled             c.save           end         end       end     end   end end

require "#{ RAILS_ROOT }/app/models/disable" class ActiveRecord::Base   include Disable end

That's pretty sweet, Greg. But this design is not DRY, of course. The "pure" way would be to just keep the state in the Dog (love the domain, btw) and have the Dog components query up to find their state via a virtual :enabled attribute.I haven't thought through the details, so I don't know for sure whether it would be possible, but I would give it a try (and maybe the OP has).

The problems with storing the same state in multiple places are just the typical ones: what happens if the process is interrupted? does it take a long time to do the multiple updates, especially inside a transaction? what happens when you give one Dog another Dog's Dish?, etc.

All that said, sometimes you do need to duplicate state, but only for adequate performance with large/complicated datasets (as far as I've seen). I don't think speed is going to be an issue for this app. :slight_smile:

///ark

Greg,

Thanks for the response! Is children a method gained by a plugin or something in Rails 2? I am using Rails 1.2.3.

So far, I've tried a few things and have really gotten my hands dirty... including:

1) Get all of my has_many and has_many_and_belongs_to associations, recurse through IDs, and end up with a massive structure consisting of a vast cornucopia of object ids. Good times, really. (And gives one enough time to get more coffee). As much as I love coffee, however, this approach won't work, considering I have as many as thousands of 'descendants' a few levels down. That gets /really/ big.

2) I have several belongs_to relationships. It isn't unheard of to have a Model like:   belongs_to :foo   belongs_to :bar

  As Mark suggested, I looked at going up the chain to see if the originator was active or not. The only thing is that I don't want to write essentially the same thing for every model. The way I was going up the chain was a little bit like this:

def whos_my_daddy   my_daddy = self.class.reflect_on_all_associations(:belongs_to)

  daddy_info = Hash.new   my_daddy.each do |dad|     daddy_info[:daddy_id] = self.send(dad.primary_key_name)     daddy_info[:daddy_class] = daddy.name   end

  daddy_info end

def check_daddy_status   you_my_daddy = self.whos_my_daddy

  unless you_my_daddy.empty?     daddy = send(you_my_daddy[:daddy_class])   end

  if daddy.active?     return true   else     return false   end end

Thing is, if I end up with two belongs_to relationships, I could face the potential of having TWO daddies, and while that doesn't bug me, things could get a bit messy.

I would think that there would be a relatively easy way to go about this.

Excuse my verbosity, and thanks again for the responses!

Samantha

I don't see how you could update that object with its parent's enabled value then. Which one do you pick?

I'm also not quite following why reflection would be necessary here, unless the parent-child relationships could change. In other words, if a Leash is owned by a Collar, why couldn't Leash#enabled? simply delegate to its Collar#enabled? ?

You mentioned writing the same thing for each model. If you did need to use reflection, you could of course derive each model from a common base class that included that functionality.

My assumption that your data set was small was wrong, I guess. Is this for like some huge kennel? :slight_smile:

///ark

I don't see how you could update that object with its parent's enabled value then. Which one do you pick?

*Exactly* That's where the dilemma comes in.

I'm also not quite following why reflection would be necessary here, unless the parent-child relationships could change. In other words, if a Leash is owned by a Collar, why couldn't Leash#enabled? simply delegate to its Collar#enabled? ?

Now, I'm not using any tree type plugin, but think of it this way:

A has B B has C C has D D has E

If I want to disable A, I want it's B, and all its descendants all the way down to E to be disabled, too. I was using acts_as_paranoid, which was handling that fine, but when I looked at restoring stuff, I just couldn't figure out how to do that cleanly. So, I moved away from acts_as_paranoid and started banging my head on other options.

I wish there was a way to easily hook into the dependent => destroy stuff.

I've been Googling every possible combination of queries that I could think of to figure out how to do this for well over a week or two.

Also, my has_many relationships aren't spread out like this:

A has_many B A has_many C through B A has_many D through C A has_many E through D

So, I can't do A.find_all_in_b, A.find_all_in_c, etc., etc., etc.

For certain purposes, it's ideal for everything down the chain to be disabled if A is disabled.

You mentioned writing the same thing for each model. If you did need to use reflection, you could of course derive each model from a common base class that included that functionality.

Hmm... Can you elaborate on that a little bit?

My assumption that your data set was small was wrong, I guess. Is this for like some huge kennel? :slight_smile:

///ark

LOL! Nice. I needed that laugh. :slight_smile: I'm not really dealing with Dogs and Leashes and Collars, just using those as example Models. :slight_smile:

Thanks Again, Samantha

> I don't see how you could update that object with its parent's enabled > value then. Which one do you pick?

*Exactly* That's where the dilemma comes in.

Hmmm. That seems like a design problem that's unsolvable in Rails (or anywhere else). I don't see how acts_as_paranoid, reflection or dependent => :destroy can help, because I don't see how this business rule can possibly be implemented.

A has B B has C C has D D has E

Got it. So my proposal is that E#enabled calls D#enabled which calls C#enabled which calls B#enabled which calls A#enabled. For example:

class Leash < ActiveRecord::Base    belongs_to :collar

   def enabled        collar.enabled    end    #... end

And on up the chain. It's obviously a potential performance hit, but (if I understand the problem), it seems clean and easy to me. It would make the cascade effect of disabling a Dog more "intentional" (self- revealing of intent) explicit, rather than setting certain columns to false when another column in another table is false. But I have a feeling I'm missing something. :slight_smile:

> You mentioned writing the same thing for each model. If you did need > to use reflection, you could of course derive each model from a common > base class that included that functionality.

Hmm... Can you elaborate on that a little bit?

If you have a method that a number of classes would like to include, you could put it in a module and include the module with each class. Or you could put the method in a class derived from ActiveRecord::Base. Then derive your models from that class, and they'll inherit the method. That's the OOP way (which is not necessarily better).

LOL! Nice. I needed that laugh. :slight_smile: I'm not really dealing with Dogs and Leashes and Collars, just using those as example Models. :slight_smile:

Excellent. :slight_smile:

///ark