无法在 Sqlite3 中添加默认值为 NULL 的 NOT NULL 列

Posted

技术标签:

【中文标题】无法在 Sqlite3 中添加默认值为 NULL 的 NOT NULL 列【英文标题】:Cannot add a NOT NULL column with default value NULL in Sqlite3 【发布时间】:2011-03-11 09:10:41 【问题描述】:

尝试将 NOT NULL 列添加到现有表时出现以下错误。为什么会这样?。我尝试 rake db:reset 认为现有记录是问题,但即使在重置数据库后,问题仍然存在。你能帮我解决这个问题吗?

迁移文件

class AddDivisionIdToProfile < ActiveRecord::Migration
  def self.up
    add_column :profiles, :division_id, :integer, :null => false
  end

  def self.down
    remove_column :profiles, :division_id
  end
end

错误信息

SQLite3::SQLException: 无法添加默认值为 NULL 的 NOT NULL 列:ALTER TABLE "profiles" ADD "division_id" integer NOT NULL

【问题讨论】:

【参考方案1】:

这是(我认为)SQLite 的一个小故障。无论表中是否有记录,都会出现此错误。

从头开始添加表时,您可以指定 NOT NULL,这就是您使用 ":null => false" 表示法所做的事情。但是,添加列时不能这样做。 SQLite 的规范说你必须为此设置一个默认值,这是一个糟糕的选择。添加默认值不是一种选择,因为它违背了拥有 NOT NULL 外键的目的 - 即数据完整性。

这是解决此故障的一种方法,您可以在同一个迁移中完成所有操作。注意:这是针对您在数据库中还没有记录的情况。

class AddDivisionIdToProfile < ActiveRecord::Migration
  def self.up
    add_column :profiles, :division_id, :integer
    change_column :profiles, :division_id, :integer, :null => false
  end

  def self.down
    remove_column :profiles, :division_id
  end
end

我们正在添加没有 NOT NULL 约束的列,然后立即更改列以添加约束。我们可以这样做,因为虽然 SQLite 在列添加期间显然非常关心,但它对列更改并不那么挑剔。这是我书中明显的设计气味。

这绝对是一个 hack,但它比多次迁移更短,并且仍然可以在您的生产环境中使用更强大的 SQL 数据库。

【讨论】:

谢谢!这正是我所需要的。 我很好奇这是如何工作的,因为 SQLite 中没有 ALTER COLUMN。 @BrianOrtiz,使用 SQLite 执行 ALTER COLUMN 的技术很复杂。您需要将表复制到临时表,删除现有表,使用修改的列重新创建表,将临时表数据复制到新创建的表,最后删除临时表。这很可能是 ROR 所做的。而且 SQLite 没有这个内置功能也很愚蠢。 谢谢!在 Laravel 项目中,同样的想法也适用于我。【参考方案2】:

表中已有行,并且要添加新列 division_id。它需要每个现有行的新列中的某些内容。

SQLite 通常会选择 NULL,但您已经指定它不能为 NULL,那么它应该是什么呢?它没有办法知道。

见:

Adding a Non-null Column with no Default Value in a Rails Migration(2009 年,不再可用,所以这是archive.org 上的快照) Adding a NOT NULL Column to an Existing Table (2014)

该博客的建议是添加没有非空约束的列,它将在每一行中添加 NULL。然后您可以在division_id 中填写值,然后使用change_column 添加非空约束。

有关执行此三步过程的迁移脚本的说明,请参阅我链接到的博客。

【讨论】:

您关于表中已经存在行的假设听起来是正确的,并且几乎适用于任何其他 rdbms。但是,我在回答中指出,SQLite 是一个例外。即使表格为空,也会出现此错误,因此我发布了一个较短的解决方案。 这是不正确的 - sqlite 没有办法改变列。我不知道change_column是什么,但它不是sqlite。 @Benubird,change_column 是 Ruby on Rails migrations 中的一个 API 方法,这就是 OP 所要求的。 问题标题是关于 sqlite3 的。我相信 change_column 命令在内部实际上是删除表并重建它,这绝对是值得注意的事情 - 特别是如果你有一个大表,或者如果你想在一个实时数据库上运行它(因为它会不稳定跑步时) @Benubird,是的,Rails 的 SQLite 适配器中的代码确实通过创建具有新定义的新表、将数据从旧表复制到新表来模拟 ALTER TABLE 的更多操作,以及然后删除旧表。【参考方案3】:

如果您有一个包含现有行的表,那么您需要在添加 null 约束之前更新现有行。 Guide on migrations 建议使用本地模型,如下所示:

Rails 4 及以上:

class AddDivisionIdToProfile < ActiveRecord::Migration
  class Profile < ActiveRecord::Base
  end

  def change
    add_column :profiles, :division_id, :integer

    Profile.reset_column_information
    reversible do |dir|
      dir.up  Profile.update_all division_id: Division.first.id 
    end

    change_column :profiles, :division_id, :integer, :null => false
  end

end

轨道 3

class AddDivisionIdToProfile < ActiveRecord::Migration
  class Profile < ActiveRecord::Base
  end

  def change
    add_column :profiles, :division_id, :integer

    Profile.reset_column_information
    Profile.all.each do |profile|
      profile.update_attributes!(:division_id => Division.first.id)
    end

    change_column :profiles, :division_id, :integer, :null => false
  end

end

【讨论】:

如果您希望您的迁移是可逆的 (rake db:rollback) 添加 down 方法并将 change 替换为 up【参考方案4】:

以下迁移在 Rails 6 中对我有用:

class AddDivisionToProfile < ActiveRecord::Migration[6.0]
  def change
    add_reference :profiles, :division, foreign_key: true
    change_column_null :profiles, :division_id, false
  end
end

注意第一行的:division和第二行的:division_id

API Doc for change_column_null

【讨论】:

【参考方案5】:

您可以添加具有默认值的列:

ALTER TABLE table1 ADD COLUMN userId INTEGER NOT NULL DEFAULT 1

【讨论】:

【参考方案6】:

不要忘记,要求使用 ALTER TABLE ADD COLUMN NOT NULL 的默认值也有好处,至少在将列添加到包含现有数据的表中时是这样。如https://www.sqlite.org/lang_altertable.html#altertabaddcol 中所述:

ALTER TABLE 命令通过修改架构的 SQL 文本来工作 存储在 sqlite_schema 表中。没有对表进行任何更改 重命名或添加列的内容。正因为如此,执行 此类 ALTER TABLE 命令的时间与数据量无关 在表中。它们在有 1000 万行的表上运行速度与 在有 1 行的桌子上。

文件格式本身支持https://www.sqlite.org/fileformat.html

一条记录的值可能少于 对应表。例如,这可能发生在 ALTER 之后 TABLE ... ADD COLUMN SQL 语句增加了列数 在表模式中,而不修改表中预先存在的行。 记录末尾的缺失值使用 表中定义的相应列的默认值 架构。

使用此技巧,可以通过仅更新架构来添加新列,该操作需要 387 毫秒,测试表有 670 万行。数据区中已有的记录根本不被触及,节省的时间是巨大的。添加的列的缺失值来自模式,如果没有另外说明,默认值为 NULL。如果新列不为空,则必须将默认值设置为其他值。

不知道为什么表为空时没有ALTER TABLE ADD COLUMN NOT NULL 的特殊路径。一个好的解决方法可能是从头开始创建表。

【讨论】:

以上是关于无法在 Sqlite3 中添加默认值为 NULL 的 NOT NULL 列的主要内容,如果未能解决你的问题,请参考以下文章

ALTER TABLE 添加默认值为 NULL 的列

我在数据库中新添一个时间字段 值为null 做一些操作报错 如果把值填为2010-1-1就不没错了 咋解决

sqlite3

如何给Oracle的数据添加默认值

mysql建表时怎样设置datetime类型的字段默认值为不自动更新的系统当前时间

hibernate在Oracle中插入数据,默认字段被设置为null的问题解决