Greg Hauptmann
2009-Jan-23 01:41 UTC
Is there an "before_commit" hook somewhere in Rails? after_save does not help (example code attached)
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/ --~--~---------~--~----~------------~-------~--~----~ 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 rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Greg Hauptmann
2009-Jan-23 20:48 UTC
Re: Is there an "before_commit" hook somewhere in Rails? after_save does not help (example code attached)
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 < greg.hauptmann.ruby@gmail.com> 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 rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Mike Mangino
2009-Jan-23 22:32 UTC
Re: Is there an "before_commit" hook somewhere in Rails? after_save does not help (example code attached)
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 <greg.hauptmann.ruby@gmail.com > > 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 rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Greg Hauptmann
2009-Jan-24 03:05 UTC
Re: Is there an "before_commit" hook somewhere in Rails? after_save does not help (example code attached)
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 <mmangino@technologyfusion.com> 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 < > greg.hauptmann.ruby@gmail.com > > > 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 rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Mike Mangino
2009-Jan-24 16:52 UTC
Re: Is there an "before_commit" hook somewhere in Rails? after_save does not help (example code attached)
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 <mmangino@technologyfusion.com > > 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 <greg.hauptmann.ruby@gmail.com > > > 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 rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Greg Hauptmann
2009-Jan-24 23:44 UTC
Re: Is there an "before_commit" hook somewhere in Rails? after_save does not help (example code attached)
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 <mmangino@technologyfusion.com> 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 < > mmangino@technologyfusion.com > > > 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 < > greg.hauptmann.ruby@gmail.com > > > > 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 rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Matt Jones
2009-Jan-25 00:08 UTC
Re: Is there an "before_commit" hook somewhere in Rails? after_save does not help (example code attached)
There''s no explicit hook, but you can pretty much do what you''ve described using transactions. If you''re updating the chapters in a single controller action, you can use a transaction block (see http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html) to wrap all the changes. Then, either use an after_save on Book, or just call a method directly to validate the combination. You''ll want to use save! and friends within the block, and catch exceptions (ActiveRecord::RecordInvalid and ActiveRecord::RecordNotSaved) to display errors. --Matt Jones On Jan 24, 2009, at 6:44 PM, Greg Hauptmann wrote:> 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 >--~--~---------~--~----~------------~-------~--~----~ 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 rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Greg Hauptmann
2009-Jan-25 05:35 UTC
Re: Is there an "before_commit" hook somewhere in Rails? after_save does not help (example code attached)
Hi Matt/all, Actually from what I can see Rails does not hold off on trigger it''s "after_save" callbacks until just before commit in the case they are wrapped in a specific transaction (unfortunately). So there''s still no "before_commit" equivalent yet anyone has identified. Let me know if I''m wrong however here''s the test I''ve run. ------------------- test output ------------------------------------------------- Macintosh-2:after_create_test greg$ spec spec/model/with_transaction_block.rb BOOK: before_save F 1) RuntimeError in ''Book should allow creation of book-allocation-chapter if costs match'' amounts do NOT match ./spec/model/with_transaction_block.rb:27:in `after_save_check'' ./spec/model/with_transaction_block.rb:57: ./spec/model/with_transaction_block.rb:56: Finished in 0.048817 seconds 1 example, 1 failure Macintosh-2:after_create_test greg$ --------------- spec ------------------------------------------------------------------ require File.expand_path(File.dirname(__FILE__) + ''/../spec_helper'') # ------------ ALLOCATION ------------- class Allocation < ActiveRecord::Base belongs_to :book belongs_to :chapter before_save :after_save_check def after_save_check puts "ALLOCATION: before_save" b = self.book sum = b.allocations.sum(:amount) raise("amounts do NOT match") if !(b.amount == sum) end end # ----------- BOOK --------------- class Book < ActiveRecord::Base has_many :allocations has_many :chapters, :through => :allocations before_save :after_save_check def after_save_check puts "BOOK: before_save" sum = self.allocations.sum(:amount) raise "amounts do NOT match" if !(self.amount == sum) end end # ----------- CHAPTER --------------- class Chapter < ActiveRecord::Base has_many :allocations has_many :books, :through => :allocations before_save :after_save_check def after_save_check puts "CHAPTER: before_save" end end # --------- RSPEC (BOOK) ------------ describe Book do before(:each) do @b = Book.new(:amount => 100) @c = Chapter.new() end it "should allow creation of book-allocation-chapter if costs match" do Book.transaction do @b.save! # SEEMS TO TRIGGER BOOK AFTER_SAVE HERE RATHER THAN HOLDING OFF @c.save! @a1 = Allocation.create!(:book_id => @b.id, :chapter_id => @c.id, :amount => 100) end end end -------mysql log ----------------------------------------------------------------- 090125 15:31:46 1534 Connect root@localhost on after_create_test_test 1534 Query SET SQL_AUTO_IS_NULL=0 1534 Statistics 1534 Query SHOW FIELDS FROM `books` 1534 Query SHOW FIELDS FROM `chapters` 1534 Query BEGIN 1534 Query SHOW FIELDS FROM `allocations` 1534 Query SELECT sum(`allocations`.amount) AS sum_amount FROM `allocations` WHERE (`allocations`.book_id = NULL) 1534 Query ROLLBACK 1534 Quit Is my analysis correct? Cheers Greg On Sun, Jan 25, 2009 at 10:08 AM, Matt Jones <al2o3cr@gmail.com> wrote:> > There''s no explicit hook, but you can pretty much do what you''ve > described using transactions. > If you''re updating the chapters in a single controller action, you can > use a transaction block > (see > http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html > ) > to wrap all > the changes. Then, either use an after_save on Book, or just call a > method directly to validate the > combination. > > You''ll want to use save! and friends within the block, and catch > exceptions (ActiveRecord::RecordInvalid and > ActiveRecord::RecordNotSaved) to display errors. > > --Matt Jones > > > On Jan 24, 2009, at 6:44 PM, Greg Hauptmann wrote: > > > 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 > > > > > > >-- 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 rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
stephen paul suarez
2009-Jan-25 21:14 UTC
Re: Is there an "before_commit" hook somewhere in Rails? after_save does not help (example code attached)
hmmm, i have an idea for you but i think it''s a bit hacky.. i''ve put the check on validation since i think it mostly relates to validating your models..# ----------- Allocation --------------- class Allocation < ActiveRecord::Base belongs_to :book belongs_to :chapter validate :check def check #this will always be true.. this just ensures that return @check if @check @check = true self.book.valid? end end # ----------- BOOK --------------- class Book < ActiveRecord::Base has_many :allocations has_many :chapters, :through => :allocations validates_associated :allocations validate :check def check puts "BOOK: after_save" #reload associated allocations always sum = self.allocations(true).map(&:amount).sum errors.add("amount","do NOT match") unless self.amount == sum end end this is a bit tricky, since Allocation#save will try to call on Book#valid?, the validates_associated will in turn try to call Allocation#valid? once again, but the second time the code executes on Allocation#check, the validation will just return true.. hth --stephen On Sun, Jan 25, 2009 at 1:35 PM, Greg Hauptmann < greg.hauptmann.ruby@gmail.com> wrote:> Hi Matt/all, > Actually from what I can see Rails does not hold off on trigger it''s > "after_save" callbacks until just before commit in the case they are wrapped > in a specific transaction (unfortunately). So there''s still no > "before_commit" equivalent yet anyone has identified. Let me know if I''m > wrong however here''s the test I''ve run. > > > ------------------- test output > ------------------------------------------------- > Macintosh-2:after_create_test greg$ spec > spec/model/with_transaction_block.rb > BOOK: before_save > F > > 1) > RuntimeError in ''Book should allow creation of book-allocation-chapter if > costs match'' > amounts do NOT match > ./spec/model/with_transaction_block.rb:27:in `after_save_check'' > ./spec/model/with_transaction_block.rb:57: > ./spec/model/with_transaction_block.rb:56: > > Finished in 0.048817 seconds > > 1 example, 1 failure > Macintosh-2:after_create_test greg$ > > --------------- spec > ------------------------------------------------------------------ > require File.expand_path(File.dirname(__FILE__) + ''/../spec_helper'') > > # ------------ ALLOCATION ------------- > class Allocation < ActiveRecord::Base > belongs_to :book > belongs_to :chapter > > before_save :after_save_check > def after_save_check > puts "ALLOCATION: before_save" > b = self.book > sum = b.allocations.sum(:amount) > raise("amounts do NOT match") if !(b.amount == sum) > end > end > > # ----------- BOOK --------------- > class Book < ActiveRecord::Base > has_many :allocations > has_many :chapters, :through => :allocations > > before_save :after_save_check > def after_save_check > puts "BOOK: before_save" > sum = self.allocations.sum(:amount) > raise "amounts do NOT match" if !(self.amount == sum) > end > > end > # ----------- CHAPTER --------------- > class Chapter < ActiveRecord::Base > has_many :allocations > has_many :books, :through => :allocations > > before_save :after_save_check > def after_save_check > puts "CHAPTER: before_save" > end > > end > > # --------- RSPEC (BOOK) ------------ > describe Book do > before(:each) do > @b = Book.new(:amount => 100) > @c = Chapter.new() > end > > it "should allow creation of book-allocation-chapter if costs match" do > Book.transaction do > @b.save! # SEEMS TO TRIGGER BOOK AFTER_SAVE HERE RATHER THAN > HOLDING OFF > @c.save! > @a1 = Allocation.create!(:book_id => @b.id, :chapter_id => @c.id, > :amount => 100) > end > end > > end > > -------mysql log > ----------------------------------------------------------------- > 090125 15:31:46 1534 Connect root@localhost on > after_create_test_test > 1534 Query SET SQL_AUTO_IS_NULL=0 > 1534 Statistics > 1534 Query SHOW FIELDS FROM `books` > 1534 Query SHOW FIELDS FROM `chapters` > 1534 Query BEGIN > 1534 Query SHOW FIELDS FROM `allocations` > 1534 Query SELECT sum(`allocations`.amount) AS > sum_amount FROM `allocations` WHERE (`allocations`.book_id = NULL) > 1534 Query ROLLBACK > 1534 Quit > > > > Is my analysis correct? > > Cheers > Greg > > On Sun, Jan 25, 2009 at 10:08 AM, Matt Jones <al2o3cr@gmail.com> wrote: > >> >> There''s no explicit hook, but you can pretty much do what you''ve >> described using transactions. >> If you''re updating the chapters in a single controller action, you can >> use a transaction block >> (see >> http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html >> ) >> to wrap all >> the changes. Then, either use an after_save on Book, or just call a >> method directly to validate the >> combination. >> >> You''ll want to use save! and friends within the block, and catch >> exceptions (ActiveRecord::RecordInvalid and >> ActiveRecord::RecordNotSaved) to display errors. >> >> --Matt Jones >> >> >> On Jan 24, 2009, at 6:44 PM, Greg Hauptmann wrote: >> >> > 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 >> > >> >> >> >> > > > -- > 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 rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Greg Hauptmann
2009-Jan-25 21:36 UTC
Re: Is there an "before_commit" hook somewhere in Rails? after_save does not help (example code attached)
thanks stephen - I think (haven''t tested it) the problem here would still be called a line 1 (see below) of my use case. So whilst it would trigger validation for chapter in a non-looping sense, the problem is that until Chapter & the Allocation are saved to DB the validation will fail. Make sense? Hence why I was interested in an a "before_final_commit" type hook. --------------------------- 1 @b.save! # SEEMS TO TRIGGER BOOK AFTER_SAVE HERE RATHER THAN HOLDING OFF 2 @c.save! 3 @a1 = Allocation.create!(:book_id => @b.id, :chapter_id => @c.id, :amount => 100) --------------------------- On Mon, Jan 26, 2009 at 7:14 AM, stephen paul suarez <devpopol@gmail.com>wrote:> hmmm, i have an idea for you but i think it''s a bit hacky.. i''ve put the > check on validation since i think it mostly relates to validating your > models..# ----------- Allocation --------------- > class Allocation < ActiveRecord::Base > belongs_to :book > belongs_to :chapter > > validate :check > > def check > #this will always be true.. this just ensures that > return @check if @check > @check = true > self.book.valid? > end > end > > # ----------- BOOK --------------- > class Book < ActiveRecord::Base > has_many :allocations > has_many :chapters, :through => :allocations > > validates_associated :allocations > validate :check > > def check > puts "BOOK: after_save" > #reload associated allocations always > sum = self.allocations(true).map(&:amount).sum > errors.add("amount","do NOT match") unless self.amount == sum > end > end > > > this is a bit tricky, since Allocation#save will try to call on > Book#valid?, the validates_associated will in turn try to call > Allocation#valid? once again, but the second time the code executes on > Allocation#check, the validation will just return true.. hth > > --stephen > > > On Sun, Jan 25, 2009 at 1:35 PM, Greg Hauptmann < > greg.hauptmann.ruby@gmail.com> wrote: > >> Hi Matt/all, >> Actually from what I can see Rails does not hold off on trigger it''s >> "after_save" callbacks until just before commit in the case they are wrapped >> in a specific transaction (unfortunately). So there''s still no >> "before_commit" equivalent yet anyone has identified. Let me know if I''m >> wrong however here''s the test I''ve run. >> >> >> ------------------- test output >> ------------------------------------------------- >> Macintosh-2:after_create_test greg$ spec >> spec/model/with_transaction_block.rb >> BOOK: before_save >> F >> >> 1) >> RuntimeError in ''Book should allow creation of book-allocation-chapter if >> costs match'' >> amounts do NOT match >> ./spec/model/with_transaction_block.rb:27:in `after_save_check'' >> ./spec/model/with_transaction_block.rb:57: >> ./spec/model/with_transaction_block.rb:56: >> >> Finished in 0.048817 seconds >> >> 1 example, 1 failure >> Macintosh-2:after_create_test greg$ >> >> --------------- spec >> ------------------------------------------------------------------ >> require File.expand_path(File.dirname(__FILE__) + ''/../spec_helper'') >> >> # ------------ ALLOCATION ------------- >> class Allocation < ActiveRecord::Base >> belongs_to :book >> belongs_to :chapter >> >> before_save :after_save_check >> def after_save_check >> puts "ALLOCATION: before_save" >> b = self.book >> sum = b.allocations.sum(:amount) >> raise("amounts do NOT match") if !(b.amount == sum) >> end >> end >> >> # ----------- BOOK --------------- >> class Book < ActiveRecord::Base >> has_many :allocations >> has_many :chapters, :through => :allocations >> >> before_save :after_save_check >> def after_save_check >> puts "BOOK: before_save" >> sum = self.allocations.sum(:amount) >> raise "amounts do NOT match" if !(self.amount == sum) >> end >> >> end >> # ----------- CHAPTER --------------- >> class Chapter < ActiveRecord::Base >> has_many :allocations >> has_many :books, :through => :allocations >> >> before_save :after_save_check >> def after_save_check >> puts "CHAPTER: before_save" >> end >> >> end >> >> # --------- RSPEC (BOOK) ------------ >> describe Book do >> before(:each) do >> @b = Book.new(:amount => 100) >> @c = Chapter.new() >> end >> >> it "should allow creation of book-allocation-chapter if costs match" do >> Book.transaction do >> @b.save! # SEEMS TO TRIGGER BOOK AFTER_SAVE HERE RATHER THAN >> HOLDING OFF >> @c.save! >> @a1 = Allocation.create!(:book_id => @b.id, :chapter_id => @c.id, >> :amount => 100) >> end >> end >> >> end >> >> -------mysql log >> ----------------------------------------------------------------- >> 090125 15:31:46 1534 Connect root@localhost on >> after_create_test_test >> 1534 Query SET SQL_AUTO_IS_NULL=0 >> 1534 Statistics >> 1534 Query SHOW FIELDS FROM `books` >> 1534 Query SHOW FIELDS FROM `chapters` >> 1534 Query BEGIN >> 1534 Query SHOW FIELDS FROM `allocations` >> 1534 Query SELECT sum(`allocations`.amount) AS >> sum_amount FROM `allocations` WHERE (`allocations`.book_id = NULL) >> 1534 Query ROLLBACK >> 1534 Quit >> >> >> >> Is my analysis correct? >> >> Cheers >> Greg >> >> On Sun, Jan 25, 2009 at 10:08 AM, Matt Jones <al2o3cr@gmail.com> wrote: >> >>> >>> There''s no explicit hook, but you can pretty much do what you''ve >>> described using transactions. >>> If you''re updating the chapters in a single controller action, you can >>> use a transaction block >>> (see >>> http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html >>> ) >>> to wrap all >>> the changes. Then, either use an after_save on Book, or just call a >>> method directly to validate the >>> combination. >>> >>> You''ll want to use save! and friends within the block, and catch >>> exceptions (ActiveRecord::RecordInvalid and >>> ActiveRecord::RecordNotSaved) to display errors. >>> >>> --Matt Jones >>> >>> >>> On Jan 24, 2009, at 6:44 PM, Greg Hauptmann wrote: >>> >>> > 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 >>> > >>> >>> >>> >>> >> >> >> -- >> 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 rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Greg Hauptmann
2009-Jan-25 21:56 UTC
Re: Is there an "before_commit" hook somewhere in Rails? after_save does not help (example code attached)
BTW - I''ve had an indication after posting on a mysql forum site that MySql does not provide a "deferred constraints" feature, whilst apparently Oracle and Postgres do. From what I gather this would have been useful as I could have put the business logic check in the database I think, to be only run after all statements had occurred, but before the final commit. Implication is that MySql won''t provide the solution so it would have to be in Rails if at all. At this stage I''d be happy to assume I can not get a fully robust solution to cross-model validation checks in Rails. What at least would be good is to be able to get to the concept that: 1. Data Access Layer - That is model classes and base ActiveRecord methods for models: These do not provide any protection themselves. If a developer uses the normal ActiveRecord calls (e.g. save, update etc) then it''s up to them to get it right, however they would be encouraged not to use this layer directly but use the "Service Layer" 2. Service Layer - Provides methods to use the models/tables that have cross-model business rules. Basically these are the "trusted" methods that will respect the business rules (noting it''s not possible it seems to have Rails provide the robust solution). For example: - update_chapters - would update all chapters first (no model validation calls would be firing), then at the end perform the business logic check 3. Controller Layer - calls the service layer Implication here is that one may not be able to use the normal tools like ActiveScaffold which automatically gives you maintenance pages for all the models, as it would be hooking into the Data Access Layer directly and therefore wouldn''t adhere to Business Logic check.. How does this sound? Probably the best I can do? thanks On Mon, Jan 26, 2009 at 7:36 AM, Greg Hauptmann < greg.hauptmann.ruby@gmail.com> wrote:> thanks stephen - I think (haven''t tested it) the problem here would still > be called a line 1 (see below) of my use case. So whilst it would trigger > validation for chapter in a non-looping sense, the problem is that until > Chapter & the Allocation are saved to DB the validation will fail. Make > sense? Hence why I was interested in an a "before_final_commit" type hook. > --------------------------- > 1 @b.save! # SEEMS TO TRIGGER BOOK AFTER_SAVE HERE RATHER THAN HOLDING > OFF > 2 @c.save! > 3 @a1 = Allocation.create!(:book_id => @b.id, :chapter_id => @c.id, > :amount => 100) > --------------------------- > > > > > > On Mon, Jan 26, 2009 at 7:14 AM, stephen paul suarez <devpopol@gmail.com>wrote: > >> hmmm, i have an idea for you but i think it''s a bit hacky.. i''ve put the >> check on validation since i think it mostly relates to validating your >> models.. # ----------- Allocation --------------- >> class Allocation < ActiveRecord::Base >> belongs_to :book >> belongs_to :chapter >> >> validate :check >> >> def check >> #this will always be true.. this just ensures that >> return @check if @check >> @check = true >> self.book.valid? >> end >> end >> >> # ----------- BOOK --------------- >> class Book < ActiveRecord::Base >> has_many :allocations >> has_many :chapters, :through => :allocations >> >> validates_associated :allocations >> validate :check >> >> def check >> puts "BOOK: after_save" >> #reload associated allocations always >> sum = self.allocations(true).map(&:amount).sum >> errors.add("amount","do NOT match") unless self.amount == sum >> end >> end >> >> >> this is a bit tricky, since Allocation#save will try to call on >> Book#valid?, the validates_associated will in turn try to call >> Allocation#valid? once again, but the second time the code executes on >> Allocation#check, the validation will just return true.. hth >> >> --stephen >> >> >> On Sun, Jan 25, 2009 at 1:35 PM, Greg Hauptmann < >> greg.hauptmann.ruby@gmail.com> wrote: >> >>> Hi Matt/all, >>> Actually from what I can see Rails does not hold off on trigger it''s >>> "after_save" callbacks until just before commit in the case they are wrapped >>> in a specific transaction (unfortunately). So there''s still no >>> "before_commit" equivalent yet anyone has identified. Let me know if I''m >>> wrong however here''s the test I''ve run. >>> >>> >>> ------------------- test output >>> ------------------------------------------------- >>> Macintosh-2:after_create_test greg$ spec >>> spec/model/with_transaction_block.rb >>> BOOK: before_save >>> F >>> >>> 1) >>> RuntimeError in ''Book should allow creation of book-allocation-chapter if >>> costs match'' >>> amounts do NOT match >>> ./spec/model/with_transaction_block.rb:27:in `after_save_check'' >>> ./spec/model/with_transaction_block.rb:57: >>> ./spec/model/with_transaction_block.rb:56: >>> >>> Finished in 0.048817 seconds >>> >>> 1 example, 1 failure >>> Macintosh-2:after_create_test greg$ >>> >>> --------------- spec >>> ------------------------------------------------------------------ >>> require File.expand_path(File.dirname(__FILE__) + ''/../spec_helper'') >>> >>> # ------------ ALLOCATION ------------- >>> class Allocation < ActiveRecord::Base >>> belongs_to :book >>> belongs_to :chapter >>> >>> before_save :after_save_check >>> def after_save_check >>> puts "ALLOCATION: before_save" >>> b = self.book >>> sum = b.allocations.sum(:amount) >>> raise("amounts do NOT match") if !(b.amount == sum) >>> end >>> end >>> >>> # ----------- BOOK --------------- >>> class Book < ActiveRecord::Base >>> has_many :allocations >>> has_many :chapters, :through => :allocations >>> >>> before_save :after_save_check >>> def after_save_check >>> puts "BOOK: before_save" >>> sum = self.allocations.sum(:amount) >>> raise "amounts do NOT match" if !(self.amount == sum) >>> end >>> >>> end >>> # ----------- CHAPTER --------------- >>> class Chapter < ActiveRecord::Base >>> has_many :allocations >>> has_many :books, :through => :allocations >>> >>> before_save :after_save_check >>> def after_save_check >>> puts "CHAPTER: before_save" >>> end >>> >>> end >>> >>> # --------- RSPEC (BOOK) ------------ >>> describe Book do >>> before(:each) do >>> @b = Book.new(:amount => 100) >>> @c = Chapter.new() >>> end >>> >>> it "should allow creation of book-allocation-chapter if costs match" do >>> Book.transaction do >>> @b.save! # SEEMS TO TRIGGER BOOK AFTER_SAVE HERE RATHER THAN >>> HOLDING OFF >>> @c.save! >>> @a1 = Allocation.create!(:book_id => @b.id, :chapter_id => @c.id, >>> :amount => 100) >>> end >>> end >>> >>> end >>> >>> -------mysql log >>> ----------------------------------------------------------------- >>> 090125 15:31:46 1534 Connect root@localhost on >>> after_create_test_test >>> 1534 Query SET SQL_AUTO_IS_NULL=0 >>> 1534 Statistics >>> 1534 Query SHOW FIELDS FROM `books` >>> 1534 Query SHOW FIELDS FROM `chapters` >>> 1534 Query BEGIN >>> 1534 Query SHOW FIELDS FROM `allocations` >>> 1534 Query SELECT sum(`allocations`.amount) AS >>> sum_amount FROM `allocations` WHERE (`allocations`.book_id = NULL) >>> 1534 Query ROLLBACK >>> 1534 Quit >>> >>> >>> >>> Is my analysis correct? >>> >>> Cheers >>> Greg >>> >>> On Sun, Jan 25, 2009 at 10:08 AM, Matt Jones <al2o3cr@gmail.com> wrote: >>> >>>> >>>> There''s no explicit hook, but you can pretty much do what you''ve >>>> described using transactions. >>>> If you''re updating the chapters in a single controller action, you can >>>> use a transaction block >>>> (see >>>> http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html >>>> ) >>>> to wrap all >>>> the changes. Then, either use an after_save on Book, or just call a >>>> method directly to validate the >>>> combination. >>>> >>>> You''ll want to use save! and friends within the block, and catch >>>> exceptions (ActiveRecord::RecordInvalid and >>>> ActiveRecord::RecordNotSaved) to display errors. >>>> >>>> --Matt Jones >>>> >>>> >>>> On Jan 24, 2009, at 6:44 PM, Greg Hauptmann wrote: >>>> >>>> > 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 >>>> > >>>> >>>> >>>> >>>> >>> >>> >>> -- >>> 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 rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Matt Jones
2009-Jan-26 01:50 UTC
Re: Is there an "before_commit" hook somewhere in Rails? after_save does not help (example code attached)
In your previous examples, the validation was getting called twice because you *defined* it twice; once in Allocation and once in Book. The preferred idiom in Rails is to define it once, and be careful how you use the objects. Since your rule won''t allow Allocations or Books to be updated independently, it seems logical that there will be (roughly) one place in the code that does the update. AR doesn''t automatically save associated records, but it''s easy enough to do it yourself. Your models would end up looking like this: # ------------ ALLOCATION ------------- class Allocation < ActiveRecord::Base belongs_to :book belongs_to :chapter # note no validation here end # ----------- BOOK --------------- class Book < ActiveRecord::Base has_many :allocations has_many :chapters, :through => :allocations before_save :check_totals def check_totals sum = self.allocations.sum(:amount) if !(self.amount == sum) self.errors.add :amount, ''amounts do not match'' false end end end # ----------- CHAPTER --------------- class Chapter < ActiveRecord::Base has_many :allocations has_many :books, :through => :allocations end With a controller action like this: def update_stuff @book = ... # get book instance # do stuff to @book.allocations - don''t use update_attributes, as it will save the allocations if @book.save # success else # something went wrong end end The false return value from check_totals will rollback the whole implicit transaction that book.save is enclosed in if the totals don''t match up. I''m not sure what the concern about ActiveScaffold is about - I haven''t looked at it in a lot of depth, but I doubt that it supports the kind of multi-model form you''ll need to update the records the way you plan to. As it stands, it wouldn''t be possible to update either a Book or an Allocation independently. The concept of "trusted" access methods is somewhat useless; if you don''t trust the code in your controllers, you have a whole other problem. Even if ironclad validations could be set up, all it takes is a call to save(false) to get past them... --Matt On Jan 25, 2009, at 4:56 PM, Greg Hauptmann wrote:> BTW - I''ve had an indication after posting on a mysql forum site > that MySql does not provide a "deferred constraints" feature, whilst > apparently Oracle and Postgres do. From what I gather this would > have been useful as I could have put the business logic check in the > database I think, to be only run after all statements had occurred, > but before the final commit. Implication is that MySql won''t > provide the solution so it would have to be in Rails if at all. > > At this stage I''d be happy to assume I can not get a fully robust > solution to cross-model validation checks in Rails. What at least > would be good is to be able to get to the concept that: > • Data Access Layer - That is model classes and base ActiveRecord > methods for models: These do not provide any protection > themselves. If a developer uses the normal ActiveRecord calls (e.g. > save, update etc) then it''s up to them to get it right, however they > would be encouraged not to use this layer directly but use the > "Service Layer" > • Service Layer - Provides methods to use the models/tables that > have cross-model business rules. Basically these are the "trusted" > methods that will respect the business rules (noting it''s not > possible it seems to have Rails provide the robust solution). For > example: > • update_chapters - would update all chapters first (no model > validation calls would be firing), then at the end perform the > business logic check > • Controller Layer - calls the service layer > Implication here is that one may not be able to use the normal tools > like ActiveScaffold which automatically gives you maintenance pages > for all the models, as it would be hooking into the Data Access > Layer directly and therefore wouldn''t adhere to Business Logic check.. > > How does this sound? Probably the best I can do? >--~--~---------~--~----~------------~-------~--~----~ 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 rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Ryan Angilly
2009-Jan-26 13:35 UTC
Re: Is there an "before_commit" hook somewhere in Rails? after_save does not help (example code attached)
That''s why I was so confused. There''s no reason to expect @b.save! to hold off validations. That''s the whole point calling save. If you want, you can build out the objects a la @b.allocations.build(options) and then the save will also save the new allocations. So you may be able to do something like: @b = Book.new(book_options) @c = Chapter.new(chap_options) @c.save! @a = @b.allocations.build(alloc_options) @b.save # which will save the allocations too You could also simplify things by creating a workflow where the user could not change both models in one action. If you''re trying to be railsful and restful, changing the book value in one action, and then subsequently changing the chapter values in successive actions may be the way to go. On Sun, Jan 25, 2009 at 4:36 PM, Greg Hauptmann < greg.hauptmann.ruby@gmail.com> wrote:> thanks stephen - I think (haven''t tested it) the problem here would still > be called a line 1 (see below) of my use case. So whilst it would trigger > validation for chapter in a non-looping sense, the problem is that until > Chapter & the Allocation are saved to DB the validation will fail. Make > sense? Hence why I was interested in an a "before_final_commit" type hook. > --------------------------- > 1 @b.save! # SEEMS TO TRIGGER BOOK AFTER_SAVE HERE RATHER THAN HOLDING > OFF > 2 @c.save! > 3 @a1 = Allocation.create!(:book_id => @b.id, :chapter_id => @c.id, > :amount => 100) > --------------------------- > > > > > > On Mon, Jan 26, 2009 at 7:14 AM, stephen paul suarez <devpopol@gmail.com>wrote: > >> hmmm, i have an idea for you but i think it''s a bit hacky.. i''ve put the >> check on validation since i think it mostly relates to validating your >> models.. # ----------- Allocation --------------- >> class Allocation < ActiveRecord::Base >> belongs_to :book >> belongs_to :chapter >> >> validate :check >> >> def check >> #this will always be true.. this just ensures that >> return @check if @check >> @check = true >> self.book.valid? >> end >> end >> >> # ----------- BOOK --------------- >> class Book < ActiveRecord::Base >> has_many :allocations >> has_many :chapters, :through => :allocations >> >> validates_associated :allocations >> validate :check >> >> def check >> puts "BOOK: after_save" >> #reload associated allocations always >> sum = self.allocations(true).map(&:amount).sum >> errors.add("amount","do NOT match") unless self.amount == sum >> end >> end >> >> >> this is a bit tricky, since Allocation#save will try to call on >> Book#valid?, the validates_associated will in turn try to call >> Allocation#valid? once again, but the second time the code executes on >> Allocation#check, the validation will just return true.. hth >> >> --stephen >> >> >> On Sun, Jan 25, 2009 at 1:35 PM, Greg Hauptmann < >> greg.hauptmann.ruby@gmail.com> wrote: >> >>> Hi Matt/all, >>> Actually from what I can see Rails does not hold off on trigger it''s >>> "after_save" callbacks until just before commit in the case they are wrapped >>> in a specific transaction (unfortunately). So there''s still no >>> "before_commit" equivalent yet anyone has identified. Let me know if I''m >>> wrong however here''s the test I''ve run. >>> >>> >>> ------------------- test output >>> ------------------------------------------------- >>> Macintosh-2:after_create_test greg$ spec >>> spec/model/with_transaction_block.rb >>> BOOK: before_save >>> F >>> >>> 1) >>> RuntimeError in ''Book should allow creation of book-allocation-chapter if >>> costs match'' >>> amounts do NOT match >>> ./spec/model/with_transaction_block.rb:27:in `after_save_check'' >>> ./spec/model/with_transaction_block.rb:57: >>> ./spec/model/with_transaction_block.rb:56: >>> >>> Finished in 0.048817 seconds >>> >>> 1 example, 1 failure >>> Macintosh-2:after_create_test greg$ >>> >>> --------------- spec >>> ------------------------------------------------------------------ >>> require File.expand_path(File.dirname(__FILE__) + ''/../spec_helper'') >>> >>> # ------------ ALLOCATION ------------- >>> class Allocation < ActiveRecord::Base >>> belongs_to :book >>> belongs_to :chapter >>> >>> before_save :after_save_check >>> def after_save_check >>> puts "ALLOCATION: before_save" >>> b = self.book >>> sum = b.allocations.sum(:amount) >>> raise("amounts do NOT match") if !(b.amount == sum) >>> end >>> end >>> >>> # ----------- BOOK --------------- >>> class Book < ActiveRecord::Base >>> has_many :allocations >>> has_many :chapters, :through => :allocations >>> >>> before_save :after_save_check >>> def after_save_check >>> puts "BOOK: before_save" >>> sum = self.allocations.sum(:amount) >>> raise "amounts do NOT match" if !(self.amount == sum) >>> end >>> >>> end >>> # ----------- CHAPTER --------------- >>> class Chapter < ActiveRecord::Base >>> has_many :allocations >>> has_many :books, :through => :allocations >>> >>> before_save :after_save_check >>> def after_save_check >>> puts "CHAPTER: before_save" >>> end >>> >>> end >>> >>> # --------- RSPEC (BOOK) ------------ >>> describe Book do >>> before(:each) do >>> @b = Book.new(:amount => 100) >>> @c = Chapter.new() >>> end >>> >>> it "should allow creation of book-allocation-chapter if costs match" do >>> Book.transaction do >>> @b.save! # SEEMS TO TRIGGER BOOK AFTER_SAVE HERE RATHER THAN >>> HOLDING OFF >>> @c.save! >>> @a1 = Allocation.create!(:book_id => @b.id, :chapter_id => @c.id, >>> :amount => 100) >>> end >>> end >>> >>> end >>> >>> -------mysql log >>> ----------------------------------------------------------------- >>> 090125 15:31:46 1534 Connect root@localhost on >>> after_create_test_test >>> 1534 Query SET SQL_AUTO_IS_NULL=0 >>> 1534 Statistics >>> 1534 Query SHOW FIELDS FROM `books` >>> 1534 Query SHOW FIELDS FROM `chapters` >>> 1534 Query BEGIN >>> 1534 Query SHOW FIELDS FROM `allocations` >>> 1534 Query SELECT sum(`allocations`.amount) AS >>> sum_amount FROM `allocations` WHERE (`allocations`.book_id = NULL) >>> 1534 Query ROLLBACK >>> 1534 Quit >>> >>> >>> >>> Is my analysis correct? >>> >>> Cheers >>> Greg >>> >>> On Sun, Jan 25, 2009 at 10:08 AM, Matt Jones <al2o3cr@gmail.com> wrote: >>> >>>> >>>> There''s no explicit hook, but you can pretty much do what you''ve >>>> described using transactions. >>>> If you''re updating the chapters in a single controller action, you can >>>> use a transaction block >>>> (see >>>> http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html >>>> ) >>>> to wrap all >>>> the changes. Then, either use an after_save on Book, or just call a >>>> method directly to validate the >>>> combination. >>>> >>>> You''ll want to use save! and friends within the block, and catch >>>> exceptions (ActiveRecord::RecordInvalid and >>>> ActiveRecord::RecordNotSaved) to display errors. >>>> >>>> --Matt Jones >>>> >>>> >>>> On Jan 24, 2009, at 6:44 PM, Greg Hauptmann wrote: >>>> >>>> > 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 >>>> > >>>> >>>> >>>> >>>> >>> >>> >>> -- >>> 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 rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Adam
2009-Jan-26 15:50 UTC
Re: Is there an "before_commit" hook somewhere in Rails? after_save does not help (example code attached)
Greg, If I understand the problem correctly, you want to update each of the Allocations for a Book in one request, making sure that by the end of the request the value sums are still valid. Correct? Based on that assumption, I''m guessing that you''re trying to do something like this (in, I''m also guessing, your BooksController): def update # explicitly open a transaction # (maybe) update values for specified Book # save Book # for each Attribute # update values # save #end # check Attribute sum validations and rollback transaction if validation fails, commit otherwise # end transaction end The problem with this is that you''re doing a lot of unnecessary database work in the case where validation fails. And Rails doesn''t really support this approach easily. However, you have all the information you need in order to do validation before any database saves. You can build your HTML form to generate params such that the modifications to Allocations get passed to the Book object on update_attributes. The Book model then updates its Allocations, checks the sum validation, and then (in an after_save callback) saves all of its associated Allocations. If validation fails, you get a run- of-the-mill Rails validation error response without touching the database. On a related note, this validation strikes me as something that you might want to consider doing on the client side, since you have all the necessary information available there, and it could end up being much simpler there. --~--~---------~--~----~------------~-------~--~----~ 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 rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---
Greg Hauptmann
2009-Jan-27 02:44 UTC
Re: Is there an "before_commit" hook somewhere in Rails? after_save does not help (example code attached)
Matt/Ryan/Adam - thanks. I think I''d need to ponder further your suggestions and do some tests to let things sink in. In the meantime to answer some of your questions: My Goal - Clarify whether Rails could be used (via validation/before_save/after_save type hooks) to provide a solid protection for a cross-model business rule. That is to protect the developer for making a mistake and for example accidentally making changes (in his/her code) that could lead to business rule violation (i.e. assuming they didn''t change the validation/before_create/after_create checks themselves). I think in summary what I''m hearing is: (a) It''s not really possible, however (b) It is possible to achieve protection for the business rule if the developer follows an appropriate approach to making updates/changes, i.e. such that an appropriate validation would kick in if necessary. Am I correct here? Thanks On Tue, Jan 27, 2009 at 1:50 AM, Adam <amilligan@pivotallabs.com> wrote:> > Greg, > > If I understand the problem correctly, you want to update each of the > Allocations for a Book in one request, making sure that by the end of > the request the value sums are still valid. Correct? Based on that > assumption, I''m guessing that you''re trying to do something like this > (in, I''m also guessing, your BooksController): > > def update > # explicitly open a transaction > # (maybe) update values for specified Book > # save Book > # for each Attribute > # update values > # save > #end > # check Attribute sum validations and rollback transaction if > validation fails, commit otherwise > # end transaction > end > > The problem with this is that you''re doing a lot of unnecessary > database work in the case where validation fails. And Rails doesn''t > really support this approach easily. However, you have all the > information you need in order to do validation before any database > saves. You can build your HTML form to generate params such that the > modifications to Allocations get passed to the Book object on > update_attributes. The Book model then updates its Allocations, > checks the sum validation, and then (in an after_save callback) saves > all of its associated Allocations. If validation fails, you get a run- > of-the-mill Rails validation error response without touching the > database. > > On a related note, this validation strikes me as something that you > might want to consider doing on the client side, since you have all > the necessary information available there, and it could end up being > much simpler there. > > >-- 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 rubyonrails-core@googlegroups.com To unsubscribe from this group, send email to rubyonrails-core+unsubscribe@googlegroups.com For more options, visit this group at http://groups.google.com/group/rubyonrails-core?hl=en -~----------~----~----~----~------~----~------~--~---