如何在Rails迁移中将可空列更改为非可空列?

231

我之前在迁移中创建了一个日期列并将其设置为可空。现在我想将其更改为不可空。假设数据库中有空行,我该怎么做?如果当前为空,我可以将这些列设置为 Time.now。

8个回答

232

如果您希望在迁移中执行此操作,则可以按照以下方式进行:

# 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

1
只是一条提示,因为这让我破坏了我的开发数据库。最好使用显式的哈希语法,像这样:MyModel.update_all({:date_column => Time.now}, {:date_column => nil})。在原始形式中的查询只会使所有模型在该字段上具有空值。 - dimitarvp
1
在这个迁移中,您是否必须使用'up'/'down'方法,还是可以在迁移中使用简单的change方法? - E.E.33
2
“change”方法并不适用于这种情况,因为(1) “update_all”方法将在迁移和潜在的还原中执行。这可能不是最糟糕的事情,但是因为(2)迁移无法知道在潜在的还原中列从哪里更改。所以对于这种情况,我会坚持使用“up”和“down”。 - DanneManne
6
在迁移中,不应直接使用模型本身来进行数据更改。例如,在后续的迁移中可能会删除该模型(如此例中的MyModel),如果有人运行了所有的迁移,这将导致错误。相反,应该直接使用ActiveRecord::Base.connection.execute('your sql here')执行SQL语句。 - Jaco Pretorius
2
对于任何感兴趣的人,我的答案展示了如何在单个步骤中完成此操作。 - Rick Smith
显示剩余6条评论

228
在Rails 4中,这是一个更好(更加DRY)的解决方案:
change_column_null :my_models, :date_column, false

为了确保该列中不存在 NULL 值的记录,您可以传递第四个参数,该参数是在记录中使用 NULL 值时要使用的默认值:
change_column_null :my_models, :date_column, false, Time.now

4
当表中已经存在空值时,这会导致问题。请参见我的回答 - Rick Smith
5
也可在3.2版本中使用。还有第四个参数可用于设置当值为NULL时的默认值。 - toxaq
1
change_column_null 加 1。然而,Rick Smith 上面的评论指出了一个非常有效的情况。 - kingsfoil
已更新以添加更新空值的查询。第四个参数(默认值)仅在您确实希望将来记录具有默认值时才有用。 - mrbrdo
8
根据Rails 4.2文档,实际上第四个参数并不会为未来的记录设置默认值:“该方法接受一个可选的第四个参数,用于将现有的+NULL+替换为其他值。请注意,第四个参数并不设置列的默认值。” - Mike Fischer
就像 @MikeFischer 所写的那样,change_column_null 不会为列设置默认值。如果您需要这样做,我建议首先使用 update_all 更新所有 null 记录,然后改用 change_column :table, :column, :column_type, default: "yourmom", null: false。这样更清晰,甚至可能更快/更少阻塞。 - Joshua Pinter

73
Rails 4(其他Rails 4答案存在问题):
def change
  change_column_null(:users, :admin, false, <put a default value here> )
  # change_column(:users, :admin, :string, :default => "")
end

将具有NULL值的列更改为不允许NULL将会导致问题。这正是在开发环境中可以正常工作但在生产环境中部署时会崩溃的代码类型。您应该首先将NULL值更改为有效值,然后再禁止NULL。 change_column_null中的第4个值正是如此。有关更多详细信息,请参见documentation
此外,我通常喜欢为字段设置默认值,这样我就不需要每次创建新对象时指定字段的值。我还包括了注释掉的代码来实现这一点。

3
针对Rails 4,这似乎是最准确、最完整的答案,包括已注释掉的默认设置。 - Mike Fischer
4
如果您正在向表中添加新列,并希望插入新值以替代空值,但不希望为该列添加默认值,则可以在迁移中执行以下操作:add_column :users, :admin, :string 然后 change_column_null(:admin, :string, false, "new_value_for_existing_records") - colsen

48
创建一个迁移,其中包含具有 :default => 值的 change_column 语句。
change_column :my_table, :my_column, :integer, :default => 0, :null => false

参考:change_column

根据数据库引擎的不同,可能需要使用 change_column_null


1
这对我有用。在本地使用MySql时,当我将应用程序推送并在Heroku(Postgres)上运行时,它会在我写入null的非空列上出现问题 - 这是正确的。只有“change_column_null”才能工作,不能在MySql上使用“change_column ...:null => false”。谢谢。 - rtfminc
1
那么在更改 change_column_null 后,你的迁移是什么呢? - js111
1
PostgreSQL比MySQL更严格--我期望它需要change_column_null - jessecurry
5
我强烈建议您在开发和生产中使用同一种数据库引擎,这样可以避免处理边缘情况时出现很多问题。 - yagooar

12
Rails 4:
def change
  change_column_null(:users, :admin, false )
end

1
请提供您答案的描述。 - Wahyu Kristianto

4

根据文档,在Rails 4.02+中没有像update_all这样带有两个参数的方法。取而代之,可以使用以下代码:

# 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

3
根据Strong Migrations gem,在生产环境中使用change_column_null是个坏主意,因为它会在检查所有记录时阻止读写操作。

处理这些迁移(Postgres特定)的推荐方法是将此过程分为两个迁移。

一个用于更改带有约束的表:

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

还需要单独进行验证的迁移:

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

上面的示例是从链接文档中提取的(并稍作修改)。显然,对于Postgres 12+,您还可以将NOT NULL添加到架构中,然后在运行验证后删除约束:
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

自然地,这意味着您的模式将不会显示该列在早期版本的Postgres中为NOT NULL,因此我建议设置模型级别验证以要求值存在(尽管我建议即使对于允许此步骤的PG版本也是如此)。
此外,在运行这些迁移之前,您需要更新所有现有记录,使其具有非空值,并确保任何编写到表中的生产代码都不会为值写入null

2

如果您有现有记录,则无法同时使用add_timestamps和null:false,因此这里是解决方案:

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

如果您选择这种方法,您可能更喜欢像 Button.where(created_at: nil).update_all(created_at: Time.zone.now) 这样的方式,因为它比循环并在每个记录上调用 update 更快。 - Ollie Bennett
如果你选择这种方式,你可能更喜欢像Button.where(created_at: nil).update_all(created_at: Time.zone.now)这样的写法,因为它比循环并在每个记录上调用update要快。 - Ollie Bennett

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接