如何在SQLite3中解决“无法在默认值NULL中添加NOT NULL列”?

时间:2010-07-03 07:05:45

标签: ruby-on-rails database sqlite sqlite3-ruby

尝试向现有表添加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

6 个答案:

答案 0 :(得分:152)

这是(我会考虑的)SQLite的一个小故障。无论表中是否有任何记录,都会发生此错误。

从头开始添加表时,可以指定NOT NULL,这是您使用“:null =&gt; 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在列添加期间显然非常关注,但是对于列更改它并不那么挑剔。这是我书中清晰的设计气味。

这绝对是一个黑客攻击,但它比多次迁移更短,并且它仍然适用于生产环境中更强大的SQL数据库。

答案 1 :(得分:36)

表格中已有行,您正在添加新列division_id。它需要每个现有行中的新列中的某些内容。

SQLite通常会选择NULL,但是你已经指定它不能为NULL,那么它应该是什么?它无从得知。

见:

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

请参阅我链接到的博客,了解执行此三个步骤的迁移脚本的说明。

答案 2 :(得分:6)

如果您有一个包含现有行的表,那么在添加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

Rails 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

答案 3 :(得分:1)

以下迁移在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

答案 4 :(得分:1)

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

ALTER TABLE table1 ADD COLUMN userId INTEGER NOT NULL DEFAULT 1

答案 5 :(得分:0)

不要忘记,要求使用 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。如果新列不是 NULL,则必须将默认值设置为其他值。

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