Help us learn about your current experience with the documentation. Take the survey.

字符串与文本数据类型

在添加新列来存储字符串或其他文本信息时:

  1. 我们总是使用 text 数据类型,而不是 string 数据类型。
  2. text 列应该始终设置限制,可以通过在创建表时使用 create_table#text ... limit: 100 辅助方法(见下文),或者在修改现有表时使用 add_text_limit。如果没有限制,最长的 字符串大约为 1 GB

标准的 Rails text 列类型不能定义限制,但我们扩展了 create_table 来添加 limit: 255 选项。在 create_table 之外,可以使用 add_text_limit 为已存在的列添加 check constraint

背景信息

我们总是想使用 text 而不是 string 的原因是,string 列的缺点是,如果你想更新它们的限制,必须运行 ALTER TABLE ... 命令。

在添加限制时,ALTER TABLE ... 命令需要对表进行 EXCLUSIVE LOCK,这个锁在更新列和验证所有现有记录的过程中都会被持有,对于大表来说,这个过程可能需要很长时间。

另一方面,在 PostgreSQL 中,文本 与字符串大致等效,并且还有一个额外的优势:为现有列添加限制或更新它们的限制不需要在整个验证阶段持有非常昂贵的 EXCLUSIVE LOCK。我们可以先关闭 valid 选项来更新约束,这需要 EXCLUSIVE LOCK,但仅用于更新列的声明。然后我们可以在后续步骤中使用 VALIDATE CONSTRAINT 来验证它,这只需要 SHARE UPDATE EXCLUSIVE LOCK(只与其他验证和索引创建冲突,同时允许读写)。

不要对 encrypts 属性使用文本列。改用 :jsonb

使用文本列创建新表

添加新表时,所有文本列的限制应该在创建表的同一个迁移中添加。我们在 Rails 的 #text 方法中添加 limit: 属性,这允许为此列添加限制。

例如,考虑一个创建包含两个文本列的表的迁移,db/migrate/20200401000001_create_db_guides.rb

class CreateDbGuides < Gitlab::Database::Migration[2.1]
  def change
    create_table :db_guides do |t|
      t.bigint :stars, default: 0, null: false
      t.text :title, limit: 128
      t.text :notes, limit: 1024
    end
  end
end

向现有表添加文本列

向现有表添加列需要对该表进行独占锁定。尽管这个锁只被短暂持有,但 add_column 完成执行所需的时间可能因表的访问频率而异。例如,在 GitLab.com 上获取一个非常频繁访问表的独占锁可能需要几分钟,并且需要使用 with_lock_retries

添加文本限制时,必须使用 disable_ddl_transaction! 禁用事务。这意味着如果迁移失败,添加的列不会被回滚。尝试重新运行迁移会因为列已存在而引发错误。

因此,向现有表添加文本列可以通过以下两种方式之一完成:

在单独的迁移中添加列和限制

考虑一个向 sprints 表添加新文本列 extended_title 的迁移,db/migrate/20200501000001_add_extended_title_to_sprints.rb

class AddExtendedTitleToSprints < Gitlab::Database::Migration[2.1]

  # rubocop:disable Migration/AddLimitToTextColumns
  # limit is added in 20200501000002_add_text_limit_to_sprints_extended_title
  def change
    add_column :sprints, :extended_title, :text
  end
  # rubocop:enable Migration/AddLimitToTextColumns
end

第二个迁移应该紧随第一个迁移之后,为 extended_title 添加限制,db/migrate/20200501000002_add_text_limit_to_sprints_extended_title.rb

class AddTextLimitToSprintsExtendedTitle < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  def up
    add_text_limit :sprints, :extended_title, 512
  end

  def down
    # Down is required as `add_text_limit` is not reversible
    remove_text_limit :sprints, :extended_title
  end
end

在单个迁移中添加列和限制,并检查列是否已存在

考虑一个向 sprints 表添加新文本列 extended_title 的迁移,db/migrate/20200501000001_add_extended_title_to_sprints.rb

class AddExtendedTitleToSprints < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  def up
    with_lock_retries do
      add_column :sprints, :extended_title, :text, if_not_exists: true
    end

    add_text_limit :sprints, :extended_title, 512
  end

  def down
    with_lock_retries do
      remove_column :sprints, :extended_title, if_exists: true
    end
  end
end

为现有列添加文本限制约束

为现有数据库列添加文本限制需要多个步骤,分为至少两个不同的版本发布:

  1. 版本 N.M(当前版本)

    • 添加一个部署后迁移,使用 validate: false 为文本列添加限制。

    • 添加一个部署后迁移来修复现有记录。

      根据表的大小,可能需要在下一个版本中添加清理的后台迁移。有关更多信息,请参阅 大表的文本限制约束

    • 为下一个里程碑创建一个问题来验证文本限制。

  2. 版本 N.M+1(下一个版本)

    • 使用部署后迁移验证文本限制。

示例

假设我们想在给定的版本里程碑(如 13.0)中为 issues.title_html 添加一个 1024 的限制。

Issues 是一个非常繁忙的大表,有超过 2500 万行,所以我们不希望在运行更新时锁定所有尝试访问它的其他进程。

此外,在检查我们的生产数据库后,我们知道有些 issues 的标题字符数超过了 1024 字符的限制,所以我们不能一步添加和验证约束。

即使我们没有标题超过提供限制的记录,另一个 GitLab 实例可能有这样的记录,所以我们无论如何都会遵循相同的过程。

防止新的无效记录(当前版本)

我们首先将限制作为 NOT VALID 检查约束添加到表中,这在新记录插入或当前记录更新时强制执行一致性。

在上面的示例中,标题超过 1024 个字符的现有 issues 不受影响,您仍然能够更新 issues 表中的记录。但是,当您尝试用标题超过 1024 个字符来更新 title_html 时,约束会导致数据库错误。

对现有属性添加或删除约束需要先部署任何应用程序更改,否则仍在旧版本中的应用程序服务器 可能会尝试用无效值更新属性。因此,add_text_limit 应该在部署后迁移中运行。

仍然在我们的示例中,对于 13.0 里程碑(当前),考虑以下验证已添加到模型 Issue 中:

validates :title_html, length: { maximum: 1024 }

我们还可以在同一个里程碑中通过在部署后迁移中使用 validate: false 添加文本限制来更新数据库,db/post_migrate/20200501000001_add_text_limit_migration.rb

class AddTextLimitMigration < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  def up
    # This will add the constraint WITHOUT validating it
    add_text_limit :issues, :title_html, 1024, validate: false
  end

  def down
    # Down is required as `add_text_limit` is not reversible
    remove_text_limit :issues, :title_html
  end
end

修复现有记录的数据迁移(当前版本)

这里的方法取决于数据量和清理策略。必须在 GitLab.com 上修复的记录数量是一个很好的指标,帮助我们决定是使用部署后迁移还是后台数据迁移:

  • 如果数据量少于 1,000 条记录,那么数据迁移可以在部署后迁移中执行。
  • 如果数据量超过 1,000 条记录,建议创建后台迁移。

不确定使用哪个选项时,请联系数据库团队寻求建议。

回到我们的示例,issues 表相当大且频繁访问,所以我们将为 13.0 里程碑(当前)添加一个后台迁移,db/post_migrate/20200501000002_schedule_cap_title_length_on_issues.rb

class ScheduleCapTitleLengthOnIssues < Gitlab::Database::Migration[2.1]
  # Info on how many records will be affected on GitLab.com
  # time each batch needs to run on average, etc ...
  BATCH_SIZE = 5000
  DELAY_INTERVAL = 2.minutes.to_i

  # Background migration will update issues whose title is longer than 1024 limit
  ISSUES_BACKGROUND_MIGRATION = 'CapTitleLengthOnIssues'.freeze

  disable_ddl_transaction!

  def up
    queue_batched_background_migration(
      ISSUES_BACKGROUND_MIGRATION,
      :issues,
      :id,
      batch_size: BATCH_SIZE
    )
  end

  def down
    delete_batched_background_migration(ISSUES_BACKGROUND_MIGRATION, :issues, :id, [])
  end
end

为了保持本指南简短,我们省略了后台迁移的定义,只提供了用于安排批次的部署后迁移的高级示例。您可以在 批量后台迁移指南 中找到更多信息。

验证文本限制(下一个版本)

验证文本限制会扫描整个表,并确保每条记录都是正确的。

仍然在我们的示例中,对于 13.1 里程碑(下一个),我们在最终的部署后迁移中运行 validate_text_limit 迁移辅助方法,db/post_migrate/20200601000001_validate_text_limit_migration.rb

class ValidateTextLimitMigration < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  def up
    validate_text_limit :issues, :title_html
  end

  def down
    # no-op
  end
end

增加现有列的文本限制约束

通过首先添加新的限制(使用不同的名称),然后删除之前的限制,可以安全地增加现有数据库列的文本限制:

class ChangeMaintainerNoteLimitInCiRunner < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  def up
    add_text_limit :ci_runners, :maintainer_note, 1024, constraint_name: check_constraint_name(:ci_runners, :maintainer_note, 'max_length_1K')
    remove_text_limit :ci_runners, :maintainer_note, constraint_name: check_constraint_name(:ci_runners, :maintainer_note, 'max_length')
  end

  def down
    # no-op: Danger of failing if there are records with length(maintainer_note) > 255
  end
end

大表的文本限制约束

如果您必须为真正的大表 清理文本列(例如 ci_builds 中的 artifacts),您的后台迁移会持续一段时间,并且在添加数据迁移的下一个版本中需要额外的 批量后台迁移清理

在这种罕见情况下,您需要端到端地使用 3 个版本:

  1. 版本 N.M - 添加文本限制和修复现有记录的后台迁移。
  2. 版本 N.M+1 - 清理后台迁移。
  3. 版本 N.M+2 - 验证文本限制。