Validation spanning multiple models(tables) - how can this be achieved in Rails?

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).

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)

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

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

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

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

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

thanks for pondering this one with me Andrew - I’ll need to think about this tomorrow :slight_smile: , 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

Greg

thanks for pondering this one with me Andrew - I’ll need to think about this tomorrow :slight_smile: , couple of off-the-cuff comments:

  • very neat

Thank you

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

It does but I only focussed on the magazine side as with the magazine/article relationship it’s more likely that you’ll create a magazine and add articles than create an article and add magazines.

You can do it though, you’ll just need to implement an add_magazine method to the article model The validations definitely work both ways, check the tests.

  • do you think there’s no way to solve this without creating a new method in fact (like your “add_article”)?

I played around with the various collection methods but each of them tries to save a model at some point where the other isn’t saved and then the validations will fail. Also most of the association methods rely on there being an id in the association model (i.e. it must be saved).

My solution doesn’t require either model to be saved and handles the saving of children when needed. My solution will also continue to work after creation if you’re updating deleting children etc. E.g. You don’t have to use the add_article method once you have at least one article saved with a link to the magazine.

You could do: magazine.add_article article magazine.save! magazine.articles << Article.new(…)

  • 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

You could deal with it more in the database but then you would lose the abstraction of the models and you would need to couple more tightly to the database.

I was thinking that one generic approach to handle cross model validations could be:

[1] VALIDATE AT OBJECT LEVEL: Validate your specific cross model rules at the Rails object level (i.e. before a “save” using the Rails validate)

  • have a “model objects array” to add each associated model object that was part of the validation

[2] ENSURE ALL MODELS THAT WERE PART OF VALIDATION ARE SAVED: In each models “after_save” then check it’s list of “model objects array” to ensure each is actually saved, then if not either save all, or if there is an issue then issue manual Rollback so that all items are rolled back.

Probem: I’m noting that re [1] and validating at the object level, if I allocate a book to a chapter, whilst the “chapter.book” works, the call “book_object.chapters” does not work??? Any way around this or is this a rails thing? i.e. my concept was in the object world the links should have been in place. Example: b = Book.new c = Chapter.new c.book = b c.book ==> works and gives b object b.chapters ==> DOES NOT WORK - gives

tks

PS. Here’s an update where I’m at if anyone whats to comment. Not quite finished however I’m wondering now if I ensure solid validation level checks in model validations (e.g. both ends of an association are working, means have to set both ends manually) that I could then rely on Rails to actually save both ends of an association even if you only save one. (e.g. seems when I save book, chapter also gets saved, and vice-versa). Here’s where I’m at:

Greg, put in a before_delete callback to see if the deletion would leave the one side open and then return false if it would.

Sorry, it’s before_destroy

just wondering what I would put in: (a) before_destroy - then proactively destroy the other associated object, BUT if those objects had a before_destory this could become circular no? (b) after_destroy - test to see if both ends were destroy, but if one end tests first prior to the other end being destroyed this would be an issue no?

I wouldn’t try do a dependant destroy, I’d return false (to stop the destroy) or raise an error so that the destroy never happens.

If you want to destroy with the dependant you are going to have to put a special case destroy on one side that will automatically destroy it’s association.

So the one will make sure it can’t be destroyed if it will break referential integrity, the other side will destroy it’s dependent along with itself.