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


#1

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


Greg
http://blog.gregnet.org/


#2

On Tue, Jan 13, 2009 at 1:28 AM, Greg H. <
removed_email_address@domain.invalid> 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)
    © articles (has “total_cost” field)

Shouldn’t the magazine have the total_cost field, and the article have a
cost field?

got to the bit where it updates 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


If you must validate use the validate method or and do your calculations
and
validation in there.

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 T.
http://ramblingsonrails.com
http://www.linkedin.com/in/andrewtimberlake

“I have never let my schooling interfere with my education” - Mark Twain


#3

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 T. <
removed_email_address@domain.invalid> wrote:

  • Tables:

(3) Use of “after_create” - Add a check for both Magazine and Article
sum_of_articles

class Magazine < ActiveRecord::Base

“I have never let my schooling interfere with my education” - Mark Twain


Greg
http://blog.gregnet.org/


#4

On Tue, Jan 13, 2009 at 8:15 AM, Greg H. <
removed_email_address@domain.invalid> wrote:

validation &
create/update/delete would still be available as part of ActiveRecord)

removed_email_address@domain.invalid> wrote:
save_with_validation(false)

(b) articles_magazines (to map the many-to-many)
would end up with a Magazine’s “total_cost” not being equal to the
finalised yet. So would the following be the best way:
end
<< Add Same Concept as per Magazine >>
Greg
articles.to_a.sum(&:cost)

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 T.
http://ramblingsonrails.com
http://www.linkedin.com/in/andrewtimberlake

“I have never let my schooling interfere with my education” - Mark Twain


#5

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

On Tue, Jan 13, 2009 at 7:12 PM, Andrew T. <
removed_email_address@domain.invalid> wrote:

i) assuming there is a validation routine in both Magazine and Article to
iv) I could put the creation in a method like
removed_email_address@domain.invalid> wrote:

Sum(Cost of Articles)

got to the bit where it updates the Magazine?
errors.add_to_base(“business rules fail”) if self.total_cost !=

per model save/update/delete???
calculate the magazine total_cost as needed

to-be-saved articles in an array and then checks both that array and the
end
def validate
end
test “magazine can’t be saved with no articles” do
assert_nothing_thrown do
assert_raise ActiveRecord::RecordInvalid do
http://www.linkedin.com/in/andrewtimberlake

“I have never let my schooling interfere with my education” - Mark Twain


Greg
http://blog.gregnet.org/


#6

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

On Tue, Jan 13, 2009 at 11:11 PM, Andrew T. <
removed_email_address@domain.invalid> wrote:

Thank you
You can do it though, you’ll just need to implement an add_magazine method
to save a model at some point where the other isn’t saved and then the
magazine.articles << Article.new(…)
abstraction of the models and you would need to couple more tightly to the
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/


#7

Greg

On Tue, Jan 13, 2009 at 2:24 PM, Greg H. <
removed_email_address@domain.invalid> wrote:

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.

regards
Greg


Andrew T.
http://ramblingsonrails.com
http://www.linkedin.com/in/andrewtimberlake

“I have never let my schooling interfere with my education” - Mark Twain


#8

On Thu, Jan 15, 2009 at 7:46 AM, Greg H. <
removed_email_address@domain.invalid> wrote:

----------- BOOK ---------------

end
@c.book = @b
@c.save!
@c.book = @b

@c = Chapter.new(:amount => 100)

deleted" do
lambda{ @c.save! }.should raise_error
@c.book = @b
$ ./script/autospec
Finished in 0.280484 seconds

10 examples, 1 failure


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.


Andrew T.
http://ramblingsonrails.com
http://www.linkedin.com/in/andrewtimberlake

“I have never let my schooling interfere with my education” - Mark Twain


#9

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:


require File.expand_path(File.dirname(FILE) + ‘/…/spec_helper’)

----------- BOOK ---------------

class Book < ActiveRecord::Base
has_many :chapters

def validate
if self.chapters.length == 0
errors.add_to_base(“Book does not have an associated Chapter”)
end
end

end

----------- CHAPTER ---------------

class Chapter < ActiveRecord::Base
belongs_to :book

def validate
if !self.book
errors.add_to_base(“Chapter does not have an associated Book”)
end
end

end

--------- RSPEC (BOOK) ------------

describe Book do
before(:each) do
@valid_attributes = {:amount => 100}
@b = Book.new(:amount => 100)
@c = Chapter.new(:amount => 100)
end

it “should save without error when association is in place” do
@c.book = @b
@b.chapters = [@c]
@b.save!
@c.save!
end

it “should delete without error when both ends of association are
deleted”
do
@c.book = @b
@b.chapters = [@c]
@b.save!
@c.save!

@c.destroy
@b.destroy

end

it “should fail for SAVE! if there is no association (OBJECT LEVEL)”
do
lambda{ @b.save! }.should raise_error
end

it “should fail for SAVE! if there is no association (DATABASE LEVEL)”
do
@c.book = @b
@b.chapters = [@c]
lambda{ @b.save! }.should_not raise_error #on basis that Rails will
save Chapter automatically
end

it “should fail for DELETE if one side left open” do
@c.book = @b
@b.chapters = [@c]
@b.save!
@c.save!

lambda{ @b.destroy }.should raise_error

end

end

----------RSPEC (CHAPTER) --------------------

describe Chapter do
before(:each) do
@valid_attributes = {:amount => 100}
@b = Book.new(:amount => 100)
@c = Chapter.new(:amount => 100)
end

it “should save without error when association is in place” do
@c.book = @b
@b.chapters = [@c]
@b.save!
@c.save!
end

it “should delete without error when both ends of association are
deleted”
do
@c.book = @b
@b.chapters = [@c]
@b.save!
@c.save!

@c.destroy
@b.destroy

end

it “should fail for SAVE! if there is no association (OBJECT LEVEL)”
do
lambda{ @c.save! }.should raise_error
end

it “should fail for SAVE! if there is no association (DATABASE LEVEL)”
do
@c.book = @b
@b.chapters = [@c]
lambda{ @c.save! }.should_not raise_error # on basis that Rails
will
automatically save Book
end

it “should fail for DELETE if one side left open” do
@c.book = @b
@b.chapters = [@c]
@b.save!
@c.save!

lambda{ @c.destroy }.should raise_error

end

end

$ ./script/autospec
loading autotest/rails_rspec
/opt/local/bin/ruby
/opt/local/lib/ruby/gems/1.8/gems/rspec-1.1.12/bin/spec
spec/models/all_in_one_test_spec.rb -O spec/spec.opts
F…

‘Chapter should fail for DELETE if one side left open’ FAILED
expected Exception but nothing was raised
./spec/models/all_in_one_test_spec.rb:114:

Finished in 0.280484 seconds

10 examples, 1 failure


On Thu, Jan 15, 2009 at 10:21 AM, Greg H. <
removed_email_address@domain.invalid> wrote:

each is actually saved, then if not either save all, or if there is an issue
c.book ==> works and gives b object

On Tue, Jan 13, 2009 at 2:24 PM, Greg H. <

My solution doesn’t require either model to be saved and handles the

  • one thing that this has made me realize is that I was also thinking

“I have never let my schooling interfere with my education” - Mark Twain


Greg
http://blog.gregnet.org/


#10

On Thu, Jan 15, 2009 at 8:20 AM, Andrew T. <
removed_email_address@domain.invalid> wrote:


end
end
end
@c.book = @b
end
@c.book = @b
describe Chapter do
@c.save!
@b.destroy
lambda{ @c.save! }.should_not raise_error # on basis that Rails will
end

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


Andrew T.
http://ramblingsonrails.com
http://www.linkedin.com/in/andrewtimberlake

“I have never let my schooling interfere with my education” - Mark Twain


#11

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?

On Thu, Jan 15, 2009 at 4:21 PM, Andrew T. <
removed_email_address@domain.invalid> wrote:

save both ends of an association even if you only save one. (e.g. seems when
if self.chapters.length == 0
def validate
@valid_attributes = {:amount => 100}

end
end
@c.book = @b
@c.save!
do
@c.save!
/opt/local/lib/ruby/gems/1.8/gems/rspec-1.1.12/bin/spec
10 examples, 1 failure


Andrew T.
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/


#12

On Thu, Jan 15, 2009 at 8:51 AM, Greg H. <
removed_email_address@domain.invalid> wrote:

Sorry, it’s before_destroy

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.


Andrew T.
http://ramblingsonrails.com
http://www.linkedin.com/in/andrewtimberlake

“I have never let my schooling interfere with my education” - Mark Twain