Personally, I normally solve the problem by making one source of data  
the correct record. For instance, I would make the allocation the  
source, and then put a callback on allocation that notifies Book of  a  
change. Then, Book can recalculate itself from the allocations. This  
type of things is often very specific to the domain. If I was doing a  
percentage breakdown, for instance, allocating a portion of the profit  
to a number of people, I would have a single method the balances all  
of the allocations and includes the validation logic instead of  
allowing people to update a single allocation at a time. For instance,

class Book
   def set_allocations(allocations)
     raise InvalidAllocations unless valid_allocations?(allocations)
     ...
   end
end

That keeps the logic in a single place and makes the validation a part  
of the business rule that it is tied to.

Mike

On Jan 23, 2009, at 10:05 PM, Greg Hauptmann wrote:

> The trouble I had with cross-model validation using validates was:
>
> (a) Object Level (i.e. using Rails objects)
> * During the carrying out of a scenario (say increasing book value,  
> and then re-deciding what each of the chapter value allocation to  
> this should be) will imply during the process of making these  
> changes the business rule will break, but it's only at the end when  
> you're finished the business rule needs to be applied.  For example  
> the sequence might be:
>   - change book value
>   - change chapter 1 value
>   - change chapter 2 value
>   - change chapter 3 value
>   - <only at this point should the cross model business rule be  
> checked>
> * Hence any validation method getting called at any of the interim  
> steps I don't think will correctly apply the cross-model business rule
> * Also at Rails object level another gottcha is just because you  
> assign book a chapter (without saving) doesn't imply you can then  
> see that chapter from the book instance end (or perhaps it was the  
> other way around).
>
> (b) At Database Level - If the validation code always looks database  
> records to do the comparison then you also get business rule  
> exceptions being through during the use case scenario whereas you  
> really want to only apply it at the end.
>
> So without going to a database solution of some sort (triggers etc)  
> the only way I could think of getting this working at the Rails  
> level was to get access to an overall "just_before_commit" hook,  
> hence my question.  (Someone suggested observers, so I'll have to  
> read up on these, however if they apply at the per model level then  
> I don't think this would probably work either)
>
> Comments?
>
> PS Don't read too much into my example as it is only there for the  
> purpose of trying to highlight a simple cross-model business rule.
>
> On Sat, Jan 24, 2009 at 8:32 AM, Mike Mangino <[email protected] 
> > wrote:
>
> I think I'm a little confused about this. Can you explain again what
> you are trying to do?
>
> So the rule is that the sum of allocations for a book must equal a
> total. That sounds like a validation. My code would look like
>
> class Book
>   validate :sum_of_allocation_equals_amount
>
> def sum_of_allocation_equals_amount
>  error.add(:amount,"Does not match sum of allocations") unless
> allocations.sum_of_price == amount
> end
> end
>
> Then, you can create a book
>
> book = Book.new(:amount=>20)
>
> #and add allocations
>
> book.allocations.build(:amount=>10)
> book.allocations.build(:amount=>5)
> book.allocations.build(:amount=>5)
>
> # and then save the book, which I believe will save the allocations
>
> book.save
>
> Does that not work?
>
> Mike
>
>
> On Jan 23, 2009, at 3:48 PM, Greg Hauptmann wrote:
>
> > PS.  Some additional clarification:
> >       • Goal is to understand whether I can/should attempt to  
> model a
> > business_rule that cuts across multiple models in Rails
> >       • I think the given is that to carry out a user scenario (e.g.
> > adding more chapters to a book) would result in having to carry out
> > multiple steps at a per model level (e.g. adjust allocation of
> > amount across chapter for their authors), and that during these
> > model changes the Business Rule would have be violated, however by
> > the time you've finished the Business Rule should have been adhered
> > to.
> >       • I was really hoping to have a way to do this that didn't  
> break
> > the normal usage of Rails models, but at the same time if you did
> > jump in and try to make one isolated change on one model (e.g. add a
> > new chapter for a book without ensuring all the chapter costs were
> > adjusted to equal the book cost), the Business Rule code would kick
> > in and pull you up with an exception.
> >       • The only way I can see to handle this in the KISS (keep it  
> simple
> > stupid) fashion would be to be able to get some sort of
> > "before_commit" hook from Rails, where I ideally it would give you
> > the models that have changed in this hook, so you do cross-business
> > rule sanity check. So the check here would ultimately be database
> > focused (i.e. check against what is in the database)
> >       • I've thought about doing the check just at object level at
> > "before_save" point, however there seem to be gotchas here.
> > Hope that makes sense.  Perhaps this is just not-possible in Rails
> > currently and I should just assume I have to be very careful with
> > all my code, because there won't be that cross-model validation
> > check there to save me.  One reason to have it in place too by the
> > way is that I could leverage a front-end frame work like
> > ActiveScaffold and not have to worry about the fact it would give a
> > user the ability to change one particular row without that cross-
> > model business logic check kicking in.
> >
> >
> > On Fri, Jan 23, 2009 at 11:41 AM, Greg Hauptmann 
> > <[email protected]
> > > wrote:
> > Hi, (no luck on the user forum so I'm hoping I can ask here)
> >
> > I'm trying to get a simple cross-model business rule working.  In
> > this case the rule is (see below for models overview):
> >   * Rule = Sum(allocations amount, for a book) = Book Amount
> >
> > ISSUE: The issue is in using after_create is that either the book or
> > allocation is saved before the other.  The only way I can see to
> > make this work is to have a check just prior to COMMIT, where all
> > records are visible within the DB and your final checks can be run
> > (and rolled back if there is problem).  Hence my question:
> >
> > QUESTION: Is there an "before_commit" hook somewhere in Rails?  (or
> > how else would I satisfy my requirement)
> >
> >
> >  
> ----------------------------------------------------------------------------------
> > Macintosh-2:after_create_test greg$ spec spec/model/
> > all_in_one_test_spec.rb
> > ============= NEW TEST ======================
> > BOOK: after_save
> > F============= NEW TEST ======================
> > CHAPTER: after_save
> > .============= NEW TEST ======================
> > BOOK: after_save
> > .
> >
> > 1)
> > RuntimeError in 'Book should save if allocation amount == book  
> amount'
> > amounts do NOT match
> > ./spec/model/all_in_one_test_spec.rb:26:in `after_save_check'
> > ./spec/model/all_in_one_test_spec.rb:51:
> >
> > Finished in 0.061092 seconds
> >
> > 3 examples, 1 failure
> >  
> ----------------------------------------------------------------------------------
> >
> > Macintosh-2:after_create_test greg$ cat -n spec/model/
> > all_in_one_test_spec.rb
> >      1  require File.expand_path(File.dirname(__FILE__) + '/../
> > spec_helper')
> >      2
> >      3  # ------------ ALLOCATION -------------
> >      4  class Allocation < ActiveRecord::Base
> >      5    belongs_to :book
> >      6    belongs_to :chapter
> >      7
> >      8    after_save :after_save_check
> >      9    def after_save_check
> >     10      puts "ALLOCATION: after_save"
> >     11      b = self.book
> >     12      sum = b.allocations.collect{|i| i.amount}.inject(0){|
> > sum, n| sum + n }
> >     13      raise("amounts do NOT match") if !(b.amount == sum)
> >     14    end
> >     15  end
> >     16
> >     17  # ----------- BOOK ---------------
> >     18  class Book < ActiveRecord::Base
> >     19    has_many :allocations
> >     20    has_many :chapters, :through => :allocations
> >     21
> >     22    after_save :after_save_check
> >     23    def after_save_check
> >     24      puts "BOOK: after_save"
> >     25      sum = self.allocations.collect{|i| i.amount}.inject(0){|
> > sum, n| sum + n }
> >     26      raise "amounts do NOT match" if !(self.amount == sum)
> >     27    end
> >     28
> >     29  end
> >     30  # ----------- CHAPTER ---------------
> >     31  class Chapter < ActiveRecord::Base
> >     32    has_many :allocations
> >     33    has_many :books, :through => :allocations
> >     34
> >     35    after_save :after_save_check
> >     36    def after_save_check
> >     37      puts "CHAPTER: after_save"
> >     38    end
> >     39
> >     40  end
> >     41
> >     42  # --------- RSPEC (BOOK) ------------
> >     43  describe Book do
> >     44    before(:each) do
> >     45      puts "============= NEW TEST ======================"
> >     46      @b = Book.new(:amount => 100)
> >     47      @c = Chapter.new()
> >     48    end
> >     49
> >     50    it "should save if allocation amount == book amount" do
> >     51      @b.save!
> >     52      @c.save!
> >     53      Allocation.create!(:book_id => @b.id, :chapter_id =>
> > @c.id, :amount => 100)
> >     54    end
> >     55
> >     56    it "should raise database exception if try to save
> > allocation prior to book" do
> >     57      lambda {
> >     58        @c.save!
> >     59        Allocation.create!(:book_id => @b.id, :chapter_id =>
> > @c.id, :amount => 100)
> >     60        @b.save!
> >     61      }.should raise_error
> >     62    end
> >     63
> >     64    it "should raise error if allocation amount != book
> > amount" do
> >     65      lambda {
> >     66        @b.save!
> >     67        @c.save!
> >     68        Allocation.create!(:book_id => @b.id, :chapter_id =>
> > @c.id, :amount => 90)
> >     69      }.should raise_error
> >     70    end
> >     71
> >     72
> >     73  end
> >     74
> >  
> ----------------------------------------------------------------------------------
> > ActiveRecord::Schema.define(:version => 20090123000614) do
> >
> >   create_table "allocations", :force => true do |t|
> >     t.integer  "book_id",    :null => false
> >     t.integer  "chapter_id", :null => false
> >     t.integer  "amount",     :null => false
> >     t.datetime "created_at"
> >     t.datetime "updated_at"
> >   end
> >
> >   create_table "books", :force => true do |t|
> >     t.integer  "amount",     :null => false
> >     t.datetime "created_at"
> >     t.datetime "updated_at"
> >   end
> >
> >   create_table "chapters", :force => true do |t|
> >     t.datetime "created_at"
> >     t.datetime "updated_at"
> >   end
> >
> > end
> >  
> ----------------------------------------------------------------------------------
> >
> >
> >
> >
> >
> >
> >
> >
> >
> >
> >
> >
> > --
> > Greg
> > http://blog.gregnet.org/
> >
> >
> >
> >
> >
> > --
> > Greg
> > http://blog.gregnet.org/
> >
> >
> >
> > >
>
>
>
>
>
>
> -- 
> Greg
> http://blog.gregnet.org/
>
>
>
> >


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups "Ruby 
on Rails: Core" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to 
[email protected]
For more options, visit this group at 
http://groups.google.com/group/rubyonrails-core?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to