ChatGPT解决这个技术问题 Extra ChatGPT

How to change a nullable column to not nullable in a Rails migration?

I created a date column in a previous migration and set it to be nullable. Now I want to change it to be not nullable. How do I go about doing this assuming there are null rows in that database? I'm ok with setting those columns to Time.now if they're currently null.


D
Dorian

If you do it in a migration then you could probably do it like this:

# Make sure no null value exist
MyModel.where(date_column: nil).update_all(date_column: Time.now)

# Change the column to not allow null
change_column :my_models, :date_column, :datetime, null: false

Just a note, because this made me bust my dev database. Rather use explicit hash syntax, like this: MyModel.update_all({:date_column => Time.now}, {:date_column => nil}). The query in your original form just made all my models have nil value in the field.
Do you have the use the 'up'/'down' method in this migration, or can you the simple change method in the migration?
The change method is not so suited for this case because (1) the update_all method will executed on both the migrate and a potential revert. That might not be the worst thing but because (2) the migration has no way of knowing what the column was changed from in a potential revert. So for this case I would stick with up and down.
You should never use the modal itself to make data changes in migrations. For example, you might remove the model (MyModel in this case) in a later migration. If someone would then run through all your migrations it would break. Rather execute SQL statements directly using ActiveRecord::Base.connection.execute('your sql here')
For anyone interested, my answer shows how to do this in a single step.
J
Joshua Pinter

In Rails 4, this is a better (DRYer) solution:

change_column_null :my_models, :date_column, false

To ensure no records exist with NULL values in that column, you can pass a fourth parameter, which is the default value to use for records with NULL values:

change_column_null :my_models, :date_column, false, Time.now

This causes problems when the table already has null values. See my answer
Also available in 3.2. Has a 4th parameter too for setting the default where value are null.
Plus 1 for change_column_null. However Rick Smith's comment above points out a very valid case.
Updated to add the query for updating null values. The 4th parameter (default value) is only useful when you actually want to have a default for future records as well.
Actually, according to the Rails 4.2 docs, the 4th param does NOT set a default value for future records: "The method accepts an optional fourth argument to replace existing +NULL+s with some other value. Please note the fourth argument does not set a column’s default."
R
Rick Smith

Rails 4 (other Rails 4 answers have problems):

def change
  change_column_null(:users, :admin, false, <put a default value here> )
  # change_column(:users, :admin, :string, :default => "")
end

Changing a column with NULL values in it to not allow NULL will cause problems. This is exactly the type of code that will work fine in your development setup and then crash when you try to deploy it to your LIVE production. You should first change NULL values to something valid and then disallow NULLs. The 4th value in change_column_null does exactly that. See documentation for more details.

Also, I generally prefer to set a default value for the field so I won't need to specify the field's value every time I create a new object. I included the commented out code to do that as well.


For Rails 4, this appears to be the most accurate and complete answer, including the commented-out default setting.
If you are adding a new column to a table and want to insert new values for null, but don't want to add a default value for the column, you can do this in your migration: add_column :users, :admin, :string then change_column_null(:admin, :string, false, "new_value_for_existing_records")
S
Soviut

Create a migration that has a change_column statement with a :default => value.

change_column :my_table, :my_column, :integer, :default => 0, :null => false

See: change_column

Depending on the database engine you may need to use change_column_null


This worked for me. Using MySql locally. When pushed and ran app in Heroku (Postgres) it crapped on column that was not null when I was writing it a null - rightfully so. Only "change_column_null" would work could not use "change_column ... :null => false" on MySql. Thanks.
so what was your migration after change_column_null
Postges is more strict that MySQL -- I'd expect that it would require change_column_null.
@rtfminc I strongly recommend you to use the same database engine in development and in production, as it avoids a lot of problems when it comes to edge cases.
J
James Chevalier

Rails 4:

def change
  change_column_null(:users, :admin, false )
end

Please provide a description of your answers.
j
jmarceli

In Rails 4.02+ according to the docs there is no method like update_all with 2 arguments. Instead one can use this code:

# Make sure no null value exist
MyModel.where(date_column: nil).update_all(date_column: Time.now)

# Change the column to not allow null
change_column :my_models, :date_column, :datetime, null: false

D
Dorian

You can't use add_timestamps and null:false if you have existing records, so here is the solution :

def change
  add_timestamps(:buttons, null: true)

  Button.find_each { |b| b.update(created_at: Time.zone.now, updated_at: Time.zone.now) }

  change_column_null(:buttons, :created_at, false)
  change_column_null(:buttons, :updated_at, false)
end

M
MattD

Per the Strong Migrations gem, using change_column_null in production is a bad idea because it blocks reads and writes while all records are checked.

The recommended way to handle these migrations (Postgres specific) is to separate this process into two migrations.

One to alter the table with the constraint:

class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
  def change
    safety_assured do
      execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID'
    end
  end
end

And a separate migration to validate it:

class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
  def change
    safety_assured do
      execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
    end
  end
end

The above examples are pulled (and slightly altered) from the linked documentation. Apparently for Postgres 12+ you can also add NOT NULL to the schema and then drop the constraint after the validation has been run:

class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
  def change
    safety_assured do
      execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
    end

    # in Postgres 12+, you can then safely set NOT NULL on the column
    change_column_null :users, :some_column, false
    safety_assured do
      execute 'ALTER TABLE "users" DROP CONSTRAINT "users_some_column_null"'
    end
  end
end

Naturally, this means your schema will not show that the column is NOT NULL for earlier versions of Postgres, so I'd also advise setting a model level validation to require the value to be present (though I'd suggest the same even for versions of PG that do allow this step).

Further, before running these migrations you'll want to update all existing records with a value other than null, and make sure any production code that writes to the table is not writing null for the value(s).