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

--~--~---------~--~----~------------~-------~--~----~
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