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