Hi Mike, all Understood. To help align my fictitious example to the cross-model validation question I've asked consider that: (a) the book value is fixed [e.g. perhaps think of this as a bank account transaction amount, being allocated out to different tax categories & then the user wants to adjust the tax categories] (b) the user manually adjusts the chapter value (i.e. there is no programmatic approach to calculating the distribution)
So this brings it back to my scenario I'm not sure how to solve in Rails whereby the sequence of events here would be: - change chapter 1 value - change chapter 2 value - change chapter 3 value - <only at this point should the cross model business rule be checked, i.e. Book.amount.should == Sum(chapter values)> My assumption here (correct me if I'm wrong) is that any Rails validation/after_save/observer kicks in at such of the sequence points, * whereas* what is actually required here is a cross_model business logic check at the end. Does this make sense? Is there a ways in Rails to get access to a "before_commit" type hook that would align with the point I want the business logic check to kick in? Thanks On Sun, Jan 25, 2009 at 2:52 AM, Mike Mangino <[email protected] > wrote: > > 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/ > > > > > > > > > > > > > > -- 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 -~----------~----~----~----~------~----~------~--~---
