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