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

