PS. Some additional clarification: 1. Goal is to understand whether I can/should attempt to model a business_rule that cuts across multiple models in Rails 2. 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. 3. 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. 4. 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) 5. 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/ --~--~---------~--~----~------------~-------~--~----~ 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 -~----------~----~----~----~------~----~------~--~---
