0

I have two models that are associated via a has_many relationship. E.g.

class Newspaper < ActiveRecord::Base
  has_many :articles
end

class Article < ActiveRecord::Base
  belongs_to :newspaper

  validates :uid, presence: true,
                  uniqueness: { case_sensitive: true }
end

A newspaper is updated several times a day but we only want to construct and add articles to the association that do not already exist. The following code was my first cut of achieving this.

new_articles.each do |article|
  unless newspaper.articles.exists? uid: article.uid
    newspaper.articles.build(uid: article.uid)
  end
end

The newspaper object is either new and unsaved, or retrieved with existing relationships at this point.

My tests indicate that I am able to add two articles to the newspaper that have the same UID using the code above and this is obviously not want I want.

I appears to me that my current code will result in a validation failure upon being saved as the validation looks at uniqueness across the entire articles table and not the association.

What I'm struggling to understand is how the exists? method behaves in this scenario (and why it's not saving my bacon as I planned). I'm using FactoryGirl to build a newspaper, add an article and then simulate an update containing an article with the same uid as the article I've already added. If the code works I should get only one associated article but instead I get two. Using either build or create makes no difference, thus whether the article record is already present in the database does not appear to change the outcome.

Can anyone shed some light on how I can achieve the desired result or why the exists? method is not doing what I expect?

Thanks

Phil Ostler
  • 429
  • 3
  • 14
  • 1
    Are you positive that the articles are being saved? and that this isn't just a testing error? The validation should, as you mention, prevent articles with duplicate UIDs from being added. However it would not *remove* them from the array if the save failed (so, for example, if you post tested for `newspaper.articles.length == 1`, it would fail). The `exists?` in this case would not work for a new newspaper, as it's a scoped query, and would end up looking for an article with a null newspaper_id, like `WHERE "articles"."newspaper_id" IS NULL AND "articles.uid" = ''` – numbers1311407 Jan 22 '13 at 02:15
  • @numbers1311407 I believe your right. My test does just check the length of the array as I expected the `exists?` to filter out any duplicates and never really expected the validation to come into the equation. I've hacked together an example in irb taking care to check before and after everything has been saved and it works fine afterward the save. So this appears to be a FactoryGirl problem where the `create` method isn't creating db entries in the same way I can in the irb. Thanks! Now that's narrowed down it shouldn't be difficult to solve – Phil Ostler Jan 23 '13 at 01:01

1 Answers1

0

The association exists? actually creates a scoped query, as per the association. This is why your existing articles filter doesn't work.

unless newspaper.articles.exists? uid: article.uid

# `articles.exists?` here will produce this if the newspaper is new 
#   ... WHERE "articles"."newspaper_id" IS NULL AND "articles.uid" = '<your uid>'

# and this, if the newspaper is persisted (with an id of 1)
#   ... WHERE "articles"."newspaper_id" = 1 AND "articles.uid" = '<your uid>'

The case of the new newspaper is clearly wrong, as it would only return articles with a nil newspaper ID. But the persisted case is probably undesirable as well, as it still unnecessarily filters against newspaper ID, when you real concern here is that the UID is unique.

Rather, you probably want simply against Article, rather than scoping the exists? through the association, like:

unless Article.exists? uid: article.uid

Concerning your other problem:

this appears to be a FactoryGirl problem where the create method isn't creating db entries in the same way I can in the irb.

FactoryGirl.create should still abide by validations. It might help to see your test.

numbers1311407
  • 33,686
  • 9
  • 90
  • 92
  • 1
    I turned on logging for ActiveRecord and indeed it was looking for a NULL `newspaper_id`. I tracked down the reason that changing `build` to `create` with FactoryGirl failed to fix this to the fact that in my instructions to construct an article, I was also telling FG to create a related newspaper. This meant I was ending up with two newspaper instances which confused matters still further. All tests are now passing. Thanks for your excellent help – Phil Ostler Jan 23 '13 at 20:50