Associating Children With Nonexistent Parents

November 28, 2012 — Code

Let’s say you’re building a blog where users can write articles and associate images with each of those articles (for display in a slideshow). You’d like authors to be able to write an article and upload images without ever leaving the page, so you implement an AJAXy image uploader (using something like Remotipart). This works great when an article has already been saved, but what if the author is creating a new article which doesn’t yet have an ID? Each uploaded image needs to have an article_id so it can be associated with the article. How do we associate a child with a not-yet-existent parent?

One common solution is to save the article before displaying the form. This works, but you’ll end up with extraneous articles in your database if authors abandon article creation “without saving,” and you’ll need to make sure these never get displayed on the site. Also, if you have validations on your articles which require, say, text in the title and body you’ll need to either skip validation on the initial save (in which case you end up with invalid data, just waiting to cause an error) or fill in some fake values. Neither option is great.

It would be nice if we could find out what the ID of the next article will be without actually creating it. That way we can upload images with an article_id which will eventually be associated with the article (once it’s saved). You could do something like this:


next_id = Article.last.id + 1   # (don't do this)

but if someone else tries to create an article at the same time, you’ll both be uploading images associated with the same article ID, and only one of those articles is actually going to get that ID. Also, even though most of the time relational databases assign new IDs in strict ascending order, it’s not always guaranteed to be the case. Basically, you shouldn’t count on being able to predict what your database will do.

So, what we’d like is (1) to know for certain what the next ID will be, and (2) to reserve that ID so that nobody else can use it for their article while we’re creating ours. The nextval function in PostgreSQL does exactly this. For example, to simultaneously learn and reserve the next available ID in the articles table:


SELECT nextval('articles_id_seq');

This assumes you’re using a sequence called articles_id_seq to churn out values for the articles.id column. In Rails we can do something like this in our model:


# app/models/article.rb

def self.next_id!
  query = "SELECT nextval('#{table_name}_id_seq') as id"
  connection.execute(query).first['id'].to_i
end

def initialize(*args)
  super
  self.id = self.class.next_id!
end

The next_id! class method advances the Article ID counter and returns the latest value. The constructor then assigns that value to every new Article object. You can now associate other objects with this new Article as if it were saved. The new_record? method even works as you’d expect! But wait, if we want this to work through our web interface, we need to make sure we’re sending the Article’s ID along with its other attributes, which isn’t standard Rails behavior for forms. So we’ll need to add a hidden field to our form, and controller code to manually assign the ID since it’s not a mass-assignable attribute:


# app/views/article/_form.html.erb

<input type="hidden" name="id" value="<%= @article.id %>" />

# app/controllers/articles_controller.rb

def create
  @article = Article.new(params[:article])
  @article.id = params[:article][:id]
  if @article.save
    ...
  end
end

And that should do it.

There is one other thing to think about, which is separate from the ID problem: If you’re using ActiveRecord’s :counter_cache to track the number of images associated with each article and you’re uploading images before an article is saved, the article’s images_count attribute is still going to be set to zero when it is first saved. It is not incremented on each image upload. So you’ll need to set it manually on image creation:


# app/models/article.rb

before_create :update_images_count

private
def update_images_count
  self.images_count = images.count
end

That’s all!


comments powered by Disqus