Daniel Azuma
Rails/ActiveRecord 2.1 and TEXT/BLOB fields
Posted on Fri, Jun 27, 2008, at 04:52 PM
Tags: rails

We just ran into a rather annoying gotcha when upgrading our application from Rails 2.0.2 to 2.1. All of a sudden, a few the columns in a few of our ActiveRecord classes unexpectedly started defaulting to nil, and refusing to save with a MySQL error. We tracked this down to a change in how ActiveRecord 2.1 is handling default values for BLOB and TEXT columns in a MySQL database.

Background

ActiveRecord uses the database schema to automatically “construct” classes and populate default values. For the most part, this works great. You can create a table such as:

  CREATE TABLE `first_objects` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `number_field` INT NOT NULL,
    `string_field` VARCHAR(100) NOT NULL DEFAULT 'hello',
    PRIMARY KEY  (`id`)
  ) ENGINE=MyISAM DEFAULT CHARSET=utf8;

and ActiveRecord will automatically know that the class FirstObject has fields id, string_field, and number_field. Furthermore, it will populate new objects with appropriate default values where provided, leaving them nil when not. e.g.

  class FirstObject < ActiveRecord::Base; end;
  object = FirstObject.new
  object.number_field    # => nil
  object.string_field    # => "hello"

Unfortunately, the database doesn’t always cooperate with this mechanism. MySQL (as of version 5.x) does not allow default values to be specified for TEXT, BLOB, or related field types. This means you cannot specify a particular default value for a long text field. Instead, the value always defaults to NULL if the column can be NULL, or the empty string "" if the column cannot be NULL. That is, the default value is implicit. This causes a bit of a headache for ActiveRecord since there is no way for the user to provide a default value in the table definition, or even to specify that one should exist.

The old behavior

In Rails prior to 2.1, ActiveRecord dealt with this by always providing a default value for such types. e.g.

  CREATE TABLE `second_objects` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `number_field` INT NOT NULL DEFAULT '0',
    `text_field` TEXT NOT NULL,
    PRIMARY KEY  (`id`)
  ) ENGINE=MyISAM DEFAULT CHARSET=utf8;

  class SecondObject < ActiveRecord::Base; end
  object = SecondObject.new
  object.number_field   # => 0
  object.text_field      # => ""

This behavior could be considered a little confusing, but it was the behavior, and it seemed to make sense since text_field did have a default value even though it is implicit. And because there would otherwise be no other way to specify that object.text_field should default to something other than nil. Unfortunately, it also caused a headache when writing out schema.rb files since the implicit default value is not allowed in the syntax.

What changed, and why is it important?

As far as I can tell, what changed in Rails 2.1 is this patch. It removes the implicit default value. Now, the behavior for our SecondObject class is as follows:

  class SecondObject < ActiveRecord::Base; end
  object = SecondObject.new
  object.number_field   # => 0
  object.text_field     # => nil  (!)

This means the behavior has changed (and caused some whiny-nil exceptions in our code until I fixed them). But perhaps more critically, this fails:

  object2 = SecondObject.new()
  object2.save!   # Raises an exception

The text_field column must always be explicitly set before the record can be saved, because as far as ActiveRecord knows, it has no default value. Hence, the attribute defaults to nil, which is an illegal value since the column is declared NOT NULL. Furthermore, as far as I can tell, there is no (easy) way to give it a default value. Which means all MySQL columns of type TEXT, BLOB, or similar must now be explicitly set in code.

Is there a solution?

I won’t get into the debate here regarding what the Right Thing To Do should have been. For now we needed a fast Rails 2.1 upgrade path, and so we’ve put in a quick-and-dirty solution by going through our ActiveRecord classes that are affected by this issue, and modifying the initialize methods to default the values according to our desires:

  class SecondObject < ActiveRecord::Base
    def initialize(attributes=nil)
      super({:text_field => 'our default'}.merge(attributes || {}))
    end
  end

One could of course imagine patching this capability into the ActiveRecord DSL, but we didn’t think that was worth it for the few cases we were running into. Is there a better solution? Am I missing something obvious?

Comments are disabled for this article.
Recent
Tags
Random blogs