thanks for pondering this one with me Andrew - I'll need to think about this
tomorrow  :) ,  couple of off-the-cuff comments:
* very neat

* just wondering if this will work for multiple magazines linked to one
article (i.e. many-to-many)

* do you think there's no way to solve this without creating a new method in
fact (like your "add_article")?

* one thing that this has made me realize is that I was also thinking
of/assuming that my validation checks would be database based (e.g. search
database to see result), but by working in the object world this helps
remove the inherit database transaction/commits only approach where I was
getting stuck seeing how to do it

regards
Greg

On Tue, Jan 13, 2009 at 7:12 PM, Andrew Timberlake <
[email protected]> wrote:

> On Tue, Jan 13, 2009 at 8:15 AM, Greg Hauptmann <
> [email protected]> wrote:
>
>> yep - the magazine should have the total_cost field (slip when I typed in
>> the example).  Also you're last suggestion is good, but I was trying to
>> construct an easy example where it highlighted the multiple-model-spanning
>> validation brick wall I'm at.  So with this in mind, and assuming I use the
>> "validate" approach (c.f. after_create hook), I still have the same question
>> really?  So an example of the issue is:
>>
>> i) assuming there is a validation routine in both Magazine and Article to
>> check business rules, that is:
>>    (a) for Magazine: Has to have an associated Article before successful
>> validation &
>>    (b) for Article: Has to have an associated Magazine before succesful
>> validation
>> ii) issue is that if I create a Magazine, the validation is then hit and
>> fails because I haven't yet created the Article (i.e. so they don't tie
>> together)
>> iii) if I create Article first to cover this then it fails because there
>> isn't a Magazine yet
>> iv) I could put the creation in a method like
>> "create_magazine_article_pair" and remove these above-mentioned validation
>> checks, however this wouldn't then protect against use of base methods (e.g.
>> create/update/delete would still be available as part of ActiveRecord)
>>
>> Is there anyway out of this?  i.e. to end up with a very solid data-access
>> layer that doesn't allow for the business rules to be broken?
>>
>> thanks
>>
>> On Tue, Jan 13, 2009 at 1:11 PM, Andrew Timberlake <
>> [email protected]> wrote:
>>
>>> On Tue, Jan 13, 2009 at 1:28 AM, Greg Hauptmann <
>>> [email protected]> wrote:
>>>
>>>> Hi,
>>>>
>>>> QUESTION: How can establishing validation that spans multiple models be
>>>> achieved in Rails?  That is in such a fashion that it is not possible for a
>>>> developer to break the validation via using any of the public model methods
>>>> (e.g. update_attribute, save, create etc).
>>>
>>>
>>> A developer can always break validation with something like
>>> save_with_validation(false)
>>>
>>>
>>>>
>>>>
>>>> EXAMPLE:
>>>> * Concept: MAGAZINE can contain multiple ARTICLES, and an ARTICLE can be
>>>> associated with multiple MAGAZINE (i.e. many to many).  Cost of Magazine =
>>>> Sum(Cost of Articles)
>>>> * Tables:
>>>>    (a) magazines (has "cost" field)
>>>>    (b) articles_magazines (to map the many-to-many)
>>>>    (c) articles (has "total_cost" field)
>>>
>>>
>>> Shouldn't the magazine have the total_cost field, and the article have a
>>> cost field?
>>>
>>>
>>>>
>>>>
>>>> BUSINESS RULE to be implemented:  Not possible for a database update
>>>> that would end up with a Magazine's "total_cost" not being equal to the
>>>> Sum(associated articles "cost"s)
>>>>
>>>> ISSUES / QUESTIONS:
>>>> (1) Assume would not try to implement rules at database constraint
>>>> level???
>>>> (2) Use of Model "before_create" - but I'm assuming here if the Article
>>>> is generated (Article.new), and then validation occurs, the code has NOT 
>>>> yet
>>>> got to the bit where it updates the Magazine?
>>>> (3) Use of "after_create" - Add a check for both Magazine and Article
>>>> perhaps here, noting the database record has been created by transaction 
>>>> NOT
>>>> finalised yet.   So would the following be the best way:
>>>>
>>>> -----example-------
>>>> class Magazine < ActiveRecord::Base
>>>>   after_save :business_rule_validation
>>>>   def business_rule_validation
>>>>     sum_of_articles = << INSERT code that calculates SUM of Articles
>>>> costs for all articles that are associated with the Magazine >>
>>>>     errors.add_to_base("business rules fail") if self.total_cost !=
>>>> sum_of_articles
>>>>   end
>>>> end
>>>
>>>
>>> The after_save callback is not for validation - see
>>> http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
>>> If you must validate use the validate method or and do your calculations
>>> and validation in there.
>>>
>>>
>>>>
>>>> class Article < ActiveRecord::Base
>>>>   << Add Same Concept as per Magazine >>
>>>> end
>>>> -----example-------
>>>>
>>>> BUT wouldn't this fail, as it assumes the Article create/update/delete
>>>> and the Magazine create/update/delete is in the SAME transaction no???
>>>> Does this mean you really have to create an overarching facade that handles
>>>> creates/updates/deletes for Article/Magazines and somehow hide the normal
>>>> per model save/update/delete???
>>>>
>>>> --
>>>> Greg
>>>> http://blog.gregnet.org/
>>>>
>>>>
>>>>
>>>>
>>>>
>>> Instead of storing the total cost and doing all this validation, I'd
>>> calculate the magazine total_cost as needed
>>>     class Magazine < ActiveRecord::Base
>>>       def total_cost
>>>         articles.to_a.sum(&:cost)
>>>       end
>>>     end
>>>
>>> --
>>> Andrew Timberlake
>>> http://ramblingsonrails.com
>>> http://www.linkedin.com/in/andrewtimberlake
>>>
>>> "I have never let my schooling interfere with my education" - Mark Twain
>>>
>>>
>>>
>>
>>
>> --
>> Greg
>> http://blog.gregnet.org/
>>
>>
>>
>>
>>
> Nice challenge, I've never had to do this (and am still not convinced of
> why you would need to - I would usually be happy with something like a
> magazine being created with no articles and then have the articles added
> later) but anyway, here's a solution:
>
> You (can't) do it with the standard association helpers (that I can work
> out) but I solved it by creating an add_article method that stores
> to-be-saved articles in an array and then checks both that array and the
> association on validation. Once the magazine is saved, it takes care of
> saving the articles which will validate because they have a magazine.
>
> class Magazine < ActiveRecord::Base
>   has_and_belongs_to_many :articles
>   after_save :save_articles
>
>   def initialize(*args)
>     super(*args)
>     @article_array = []
>   end
>
>   def total_cost
>     articles.to_a.sum(&:cost)
>   end
>
>   def add_article(article)
>     @article_array << article
>   end
>
> private
>   def validate
>     errors.add(:title, "can't have no articles") if articles.size == 0 &&
> @article_array.size == 0
>   end
>
>   def save_articles
>     @article_array.each do |article|
>       article.magazines << self
>       article.save!
>     end
>   end
> end
>
> class Article < ActiveRecord::Base
>   has_and_belongs_to_many :magazines
>
>   def validate
>     errors.add(:title, "can't have no magazines") if magazines.size == 0
>   end
> end
>
> class MagazineTest < ActiveSupport::TestCase
>   test "magazine can't be saved with no articles" do
>     assert_raise ActiveRecord::RecordInvalid do
>       Magazine.create!(:title => 'Test Magazine')
>     end
>   end
>
>   test "magazine can be save with articles" do
>     magazine = Magazine.new(:title => 'Test Magazine')
>     article = Article.new(:title => 'Test Article', :cost => 20)
>     magazine.add_article article
>
>     assert_nothing_thrown do
>       magazine.save!
>     end
>
>     assert !magazine.new_record?
>     assert !article.new_record?
>   end
> end
>
> class ArticleTest < ActiveSupport::TestCase
>   test "article can't be saved with no magazines" do
>     assert_raise ActiveRecord::RecordInvalid do
>       Article.create!(:title => 'Test Article', :cost => 20)
>     end
>
>   end
> end
>
>
> --
> Andrew Timberlake
> http://ramblingsonrails.com
> http://www.linkedin.com/in/andrewtimberlake
>
> "I have never let my schooling interfere with my education" - Mark Twain
>
> >
>


-- 
Greg
http://blog.gregnet.org/

--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups "Ruby 
on Rails: Talk" 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-talk?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to