# Migration Style Guide

> 原文：[https://docs.gitlab.com/ee/development/migration_style_guide.html](https://docs.gitlab.com/ee/development/migration_style_guide.html)

*   [Schema Changes](#schema-changes)
*   [What Requires Downtime?](#what-requires-downtime)
*   [Downtime Tagging](#downtime-tagging)
*   [Reversibility](#reversibility)
*   [Atomicity](#atomicity)
*   [Heavy operations in a single transaction](#heavy-operations-in-a-single-transaction)
*   [Retry mechanism when acquiring database locks](#retry-mechanism-when-acquiring-database-locks)
    *   [Examples](#examples)
    *   [When to use the helper method](#when-to-use-the-helper-method)
    *   [How the helper method works](#how-the-helper-method-works)
*   [Multi-Threading](#multi-threading)
*   [Removing indexes](#removing-indexes)
*   [Adding indexes](#adding-indexes)
*   [Adding foreign-key constraints](#adding-foreign-key-constraints)
*   [`NOT NULL` constraints](#not-null-constraints)
*   [Adding Columns With Default Values](#adding-columns-with-default-values)
*   [Changing the column default](#changing-the-column-default)
*   [Updating an existing column](#updating-an-existing-column)
*   [Dropping a database table](#dropping-a-database-table)
*   [Integer column type](#integer-column-type)
*   [Strings and the Text data type](#strings-and-the-text-data-type)
*   [Timestamp column type](#timestamp-column-type)
*   [Storing JSON in database](#storing-json-in-database)
*   [Testing](#testing)
*   [Data migration](#data-migration)
    *   [Renaming reserved paths](#renaming-reserved-paths)

# Migration Style Guide[](#migration-style-guide "Permalink")

在为 GitLab 编写迁移时，必须考虑到这些迁移将由成千上万个各种规模的组织来运行，其中一些组织的数据库中包含多年的数据.

此外，对于大多数组织来说，必须使服务器脱机以进行较小或较大的升级是一大负担. 因此，重要的是要仔细编写迁移文件，可以在线进行迁移，并遵守下面的样式指南.

迁移**不准**要求 GitLab 设施采取离线，除非*绝对必要的* .

如果需要停机，则必须通过以下方式批准迁移：

1.  工程副总裁
2.  后端维护者
3.  数据库维护者

可以在[https://about.gitlab.com/company/team/中](https://about.gitlab.com/company/team/)找到持有这些头衔的人员的最新列表.

When writing your migrations, also consider that databases might have stale data or inconsistencies and guard for that. Try to make as few assumptions as possible about the state of the database.

请不要依赖特定于 GitLab 的代码，因为它可能会在将来的版本中更改. 如果需要，将 GitLab 代码复制粘贴到迁移中以使其向前兼容.

对于 GitLab.com，请考虑到在[部署 Canary](https://about.gitlab.com/handbook/engineering/infrastructure/library/canary/#configuration-and-deployment)之前运行常规迁移（在`db/migrate` ），在完成生产部署后运行部署后的迁移（ `db/post_migrate` ）.

## Schema Changes[](#schema-changes "Permalink")

Changes to the schema should be committed to `db/structure.sql`. This file is automatically generated by Rails, so you normally should not edit this file by hand. If your migration is adding a column to a table, that column will be added at the bottom. Please do not reorder columns manually for existing tables as this will cause confusing to other people using `db/structure.sql` generated by Rails.

当您的 GDK 中的本地数据库与`master`数据库中的模式不同时，可能很难将模式更改完全提交给 Git. 在这种情况下，您可以使用`scripts/regenerate-schema`脚本为要添加的迁移重新生成干净的`db/structure.sql` . 该脚本将应用在`db/migrate`或`db/post_migrate`找到的所有迁移，因此，如果您不想将任何迁移提交到架构，请重命名或删除它们. 如果您的分支不是以`master`为目标，则可以设置`TARGET`环境变量.

```
# Regenerate schema against `master`
scripts/regenerate-schema

# Regenerate schema against `12-9-stable-ee`
TARGET=12-9-stable-ee scripts/regenerate-schema 
```

## What Requires Downtime?[](#what-requires-downtime "Permalink")

文档["什么需要停机？"](what_requires_downtime.html) 指定各种数据库操作，例如

*   [dropping and renaming columns](what_requires_downtime.html#dropping-columns)
*   [changing column constraints and types](what_requires_downtime.html#changing-column-constraints)
*   [adding and dropping indexes, tables, and foreign keys](what_requires_downtime.html#adding-indexes)

以及他们是否需要停机，以及如何尽可能地进行停机.

## Downtime Tagging[](#downtime-tagging "Permalink")

每个迁移都必须指定是否需要停机，并且如果需要停机，还必须指定原因. 这是即使在迁移的 99％，不需要停机，因为这使得它更容易地发现， *确实*需要停机迁移所需.

要标记迁移，请在迁移类的主体中添加以下两个常量：

*   `DOWNTIME` ：一个布尔值，当设置为`true`指示迁移需要停机时间.
*   `DOWNTIME_REASON` ：一个字符串，其中包含需要停机的迁移原因. 当`DOWNTIME`设置为`true`时， **必须**设置此常数.

例如：

```
class MyMigration < ActiveRecord::Migration[6.0]
  DOWNTIME = true
  DOWNTIME_REASON = 'This migration requires downtime because ...'

  def change
    ...
  end
end 
```

如果迁移类中缺少`DOWNTIME`常量，则会出现错误（即 CI 将失败）.

## Reversibility[](#reversibility "Permalink")

您的迁移**必须是**可逆的. 这非常重要，因为在出​​现漏洞或错误的情况下应该可以降级.

在迁移中，添加一条注释，描述如何测试迁移的可逆性.

某些迁移无法撤消. 例如，某些数据迁移无法撤消，因为在迁移之前，我们丢失了有关数据库状态的信息. 您仍然应该创建一个带注释的`down`方法，解释为什么不能撤消`up`方法执行的更改，以便即使撤消迁移期间执行的更改也可以撤消迁移本身：

```
def down
  # no-op

  # comment explaining why changes performed by `up` cannot be reversed.
end 
```

## Atomicity[](#atomicity "Permalink")

默认情况下，迁移是单个事务. 即，在迁移开始时打开一个事务，并在处理完所有步骤后提交.

在单个事务中运行迁移可确保如果其中一个步骤失败，则将不会执行任何步骤，从而使数据库保持有效状态. 因此，要么：

*   将所有迁移都放在一个单事务迁移中.
*   如有必要，将大多数操作放入一个迁移中，并为无法在单个事务中完成的步骤创建一个单独的迁移.

例如，如果创建一个空表并需要为其建立索引，则建议使用常规的单事务迁移和默认的 rails schema 语句： [`add_index`](https://api.rubyonrails.org/v5.2/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_index) . 这是一个阻塞操作，但不会引起问题，因为该表尚未使用，因此它还没有任何记录.

## Heavy operations in a single transaction[](#heavy-operations-in-a-single-transaction "Permalink")

使用单事务迁移时，事务将在迁移期间保持数据库连接，因此您必须确保迁移中的操作不会花费太多时间：通常，在迁移中执行的查询需要在 GitLab.com 上舒适地放置`15s` .

如果您需要插入，更新或删除大量数据，请执行以下操作：

*   必须使用`disable_ddl_transaction!`禁用单个事务`disable_ddl_transaction!` .
*   应该考虑在[后台迁移中](background_migrations.html)这样做.

## Retry mechanism when acquiring database locks[](#retry-mechanism-when-acquiring-database-locks "Permalink")

更改数据库架构时，我们使用辅助方法来调用 DDL（数据定义语言）语句. 在某些情况下，这些 DDL 语句需要特定的数据库锁.

Example:

```
def change
  remove_column :users, :full_name, :string
end 
```

要执行此迁移，需要对`users`表进行排他锁定. 当表被其他进程同时访问和修改时，获取锁定可能需要一段时间. 锁定请求正在队列中等待，一旦进入队列，它也可能阻止`users`表上的其他查询.

有关 PostgresSQL 锁的更多信息： [显式锁定](https://s0www0postgresql0org.icopy.site/docs/current/explicit-locking.html)

出于稳定性考虑，GitLab.com 设置了特定的[`statement_timeout`](../user/gitlab_com/index.html#postgresql) . 调用迁移时，任何数据库查询都将有固定的执行时间. 在最坏的情况下，请求将坐在锁定队列中，在配置的语句超时时间内阻止其他查询，然后`canceling statement due to statement timeout`错误`canceling statement due to statement timeout`失败.

此问题可能导致应用程序升级过程失败，甚至导致应用程序稳定性问题，因为该表可能会在短时间内无法访问.

为了提高数据库迁移的可靠性和稳定性，GitLab 代码库提供了一种辅助方法，以使用不同的`lock_timeout`设置重试操作，并在两次尝试之间等待时间. 为了获得必要的锁定，进行了多次较小的尝试，从而使数据库可以处理其他语句.

### Examples[](#examples "Permalink")

**删除列：**

```
include Gitlab::Database::MigrationHelpers

def up
  with_lock_retries do
    remove_column :users, :full_name
  end
end

def down
  with_lock_retries do
    add_column :users, :full_name, :string
  end
end 
```

**删除外键：**

```
include Gitlab::Database::MigrationHelpers

def up
  with_lock_retries do
    remove_foreign_key :issues, :projects
  end
end

def down
  with_lock_retries do
    add_foreign_key :issues, :projects
  end
end 
```

**更改列的默认值：**

```
include Gitlab::Database::MigrationHelpers

def up
  with_lock_retries do
    change_column_default :merge_requests, :lock_version, from: nil, to: 0
  end
end

def down
  with_lock_retries do
    change_column_default :merge_requests, :lock_version, from: 0, to: nil
  end
end 
```

**用外键创建一个新表：**

我们可以简单地用`with_lock_retries`包装`create_table`方法：

```
def up
  with_lock_retries do
    create_table :issues do |t|
      t.references :project, index: true, null: false, foreign_key: { on_delete: :cascade }
      t.string :title, limit: 255
    end
  end
end

def down
  drop_table :issues
end 
```

**当我们有两个外键时创建一个新表：**

为此，我们需要进行三个迁移：

1.  创建不带外键（带有索引）的表.
2.  将外键添加到第一个表.
3.  将外键添加到第二个表.

创建表：

```
def up
  create_table :imports do |t|
    t.bigint :project_id, null: false
    t.bigint :user_id, null: false
    t.string :jid, limit: 255
  end

  add_index :imports, :project_id
  add_index :imports, :user_id
end

def down
  drop_table :imports
end 
```

在`projects`添加外键：

```
include Gitlab::Database::MigrationHelpers

def up
  with_lock_retries do
    add_foreign_key :imports, :projects, column: :project_id, on_delete: :cascade
  end
end

def down
  with_lock_retries do
    remove_foreign_key :imports, column: :project_id
  end
end 
```

向`users`添加外键：

```
include Gitlab::Database::MigrationHelpers

def up
  with_lock_retries do
    add_foreign_key :imports, :users, column: :user_id, on_delete: :cascade
  end
end

def down
  with_lock_retries do
    remove_foreign_key :imports, column: :user_id
  end
end 
```

**与`disable_ddl_transaction!`配合使用`disable_ddl_transaction!`**

通常， `with_lock_retries`帮助程序应与`disabled_ddl_transaction!` . 定制的 RuboCop 规则可确保只能将允许的方法放在锁重试块中.

```
disable_ddl_transaction!

def up
  with_lock_retries do
    add_column :users, :name, :text
  end

  add_text_limit :users, :name, 255 # Includes constraint validation (full table scan)
end 
```

RuboCop 规则通常允许使用下面列出的标准 Rails 迁移方法. 此示例将导致 Rubocop 犯罪：

```
disabled_ddl_transaction!

def up
  with_lock_retries do
    add_concurrent_index :users, :name
  end
end 
```

### When to use the helper method[](#when-to-use-the-helper-method "Permalink")

通常使用标准的 Rails 迁移帮助器方法时，可以使用`with_lock_retries`帮助器方法. 如果在同一个表上执行多个迁移助手，则调用它们不是问题.

当数据库迁移涉及高流量表之一时，建议使用`with_lock_retries`帮助程序方法：

*   `users`
*   `projects`
*   `namespaces`
*   `issues`
*   `merge_requests`
*   `ci_pipelines`
*   `ci_builds`
*   `notes`

更改示例：

*   `add_foreign_key` / `remove_foreign_key`
*   `add_column` / `remove_column`
*   `change_column_default`
*   `create_table` / `drop_table`

**注意：** `with_lock_retries`方法**不能**在`change`方法中使用，必须手动定义`up`和`down`方法以使迁移可逆.

### How the helper method works[](#how-the-helper-method-works "Permalink")

1.  重复 50 次.
2.  对于每次迭代，设置一个预配置的`lock_timeout` .
3.  尝试执行给定的块. （ `remove_column` ）.
4.  如果出现`LockWaitTimeout`错误，请为预配置的`sleep_time`睡眠，然后重试该块.
5.  如果未引发错误，则当前迭代已成功执行该块.

有关更多信息，请检查[`Gitlab::Database::WithLockRetries`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/with_lock_retries.rb)类. `with_lock_retries`帮助器方法在[`Gitlab::Database::MigrationHelpers`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/migration_helpers.rb)模块中实现.

在最坏的情况下，该方法：

*   在 40 分钟内最多执行 50 次该块.
    *   大部分时间都花费在每次迭代后的预先配置的睡眠时段中.
*   第 50 次重试之后，将像标准迁移调用一样在没有`lock_timeout`情况下执行该块.
*   如果无法获取锁，则迁移将失败，并出现`statement timeout`错误.

如果访问`users`表的事务运行时间很长（超过 40 分钟），则迁移可能会失败.

## Multi-Threading[](#multi-threading "Permalink")

有时，迁移可能需要使用多个 Ruby 线程来加快迁移速度. 为此，您的迁移需要包括模块`Gitlab::Database::MultiThreadedMigration` ：

```
class MyMigration < ActiveRecord::Migration[6.0]
  include Gitlab::Database::MigrationHelpers
  include Gitlab::Database::MultiThreadedMigration
end 
```

然后可以使用`with_multiple_threads`方法在单独的线程中执行工作. 例如：

```
class MyMigration < ActiveRecord::Migration[6.0]
  include Gitlab::Database::MigrationHelpers
  include Gitlab::Database::MultiThreadedMigration

  def up
    with_multiple_threads(4) do
      disable_statement_timeout

      # ...
    end
  end
end 
```

在这里，对`disable_statement_timeout`的调用将使用`with_multiple_threads`块本地的连接，而不是重新使用全局连接池. 这样可以确保每个线程都有自己的连接对象，并且在尝试获取一个连接对象时不会超时.

**注意：** PostgreSQL 具有允许的最大连接数. 此限制因安装而异. 因此，建议您一次迁移不要使用超过 32 个线程. 通常，4-8 个线程应该绰绰有余.

## Removing indexes[](#removing-indexes "Permalink")

如果删除索引时表不为空，请确保使用方法`remove_concurrent_index`而不是常规的`remove_index`方法. `remove_concurrent_index`方法同时删除索引，因此不需要锁定，也无需停机. 要使用此方法，必须通过调用方法`disable_ddl_transaction!`禁用单交易模式`disable_ddl_transaction!` 在您的迁移类的主体中，如下所示：

```
class MyMigration < ActiveRecord::Migration[6.0]
  include Gitlab::Database::MigrationHelpers
  disable_ddl_transaction!

  def up
    remove_concurrent_index :table_name, :column_name
  end
end 
```

请注意，在删除索引之前不必检查索引是否存在.

对于较小的表（例如空表或少于`1,000`条记录的表），建议在单事务迁移中使用`remove_index` ，并将其与不需要`disable_ddl_transaction!`其他操作结合使用`disable_ddl_transaction!` .

## Adding indexes[](#adding-indexes "Permalink")

在添加索引之前，请考虑该索引是否必要. 在某些情况下可能不需要索引，例如：

*   该表很小（少于`1,000`条记录），并且预计大小不会成倍增长.
*   任何现有索引都会过滤掉足够的行.
*   添加索引后查询时间的减少并不明显.

另外，不需要宽索引来匹配查询的所有过滤条件，我们只需要覆盖足够多的列即可使索引查找具有足够小的选择性. 请查看我们的[添加数据库索引](adding_database_indexes.html)指南以获取更多详细信息.

将索引添加到非空表时，请确保使用方法`add_concurrent_index`而不是常规的`add_index`方法. 使用 PostgreSQL 时， `add_concurrent_index`方法自动创建并发索引，从而消除了停机时间.

要使用此方法，必须通过调用方法`disable_ddl_transaction!`禁用单交易模式`disable_ddl_transaction!` 在您的迁移类的主体中，如下所示：

```
class MyMigration < ActiveRecord::Migration[6.0]
  include Gitlab::Database::MigrationHelpers

  DOWNTIME = false

  disable_ddl_transaction!

  def up
    add_concurrent_index :table, :column
  end

  def down
    remove_concurrent_index :table, :column
  end
end 
```

如果需要添加唯一索引，请记住，数据库中可能存在现有重复项. 这意味着在添加唯一索引之前，应始终*首先*添加一个删除所有重复项的迁移.

对于小型表（例如空表或少于`1,000`条记录的表），建议在单事务迁移中使用`add_index` ，并将其与不需要`disable_ddl_transaction!`其他操作结合使用`disable_ddl_transaction!` .

## Adding foreign-key constraints[](#adding-foreign-key-constraints "Permalink")

向现有列或新列添加外键约束时，还请记住在该列上添加索引.

这是所有外键所**必需**的，例如，为了支持有效的级联删除：当删除表中的许多行时，也需要删除引用的记录. 数据库必须在引用的表中查找相应的记录. 没有索引，这将导致对表进行顺序扫描，这可能需要很长时间.

这是一个示例，其中我们添加了一个带有外键约束的新列. 请注意，它包括`index: true`为其创建索引.

```
class Migration < ActiveRecord::Migration[6.0]

  def change
    add_reference :model, :other_model, index: true, foreign_key: { on_delete: :cascade }
  end
end 
```

当向非空表中的现有列添加外键约束时，我们必须使用`add_concurrent_foreign_key`和`add_concurrent_index`而不是`add_reference` .

对于空表（例如新表），建议在单事务迁移中使用`add_reference` ，并将其与不需要`disable_ddl_transaction!`其他操作结合使用`disable_ddl_transaction!` .

您可以阅读有关将[外键约束](database/add_foreign_key_to_existing_column.html)添加[到现有列的更多信息](database/add_foreign_key_to_existing_column.html) .

## `NOT NULL` constraints[](#not-null-constraints "Permalink")

[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/38358) in GitLab 13.0.

有关更多信息，请参见关于[`NOT NULL`约束](database/not_null_constraints.html)的样式指南.

## Adding Columns With Default Values[](#adding-columns-with-default-values "Permalink")

PostgreSQL 11 是自 GitLab 13.0 以来的最低版本，添加具有默认值的列变得更加容易，并且在所有情况下都应使用标准的`add_column`帮助器.

在 PostgreSQL 11 之前，添加带有默认值的列是有问题的，因为这会导致全表重写. 相应的帮助程序`add_column_with_default`已被弃用，并将在以后的版本中删除.

**注意：**如果％12.9 或更早版本需要向后端口添加具有默认值的列，则应使用`add_column_with_default`帮助器. 如果涉及[大表](https://gitlab.com/gitlab-org/gitlab/-/blob/master/rubocop/rubocop-migrations.yml#L3) ，则禁止向后移植到％12.9.

## Changing the column default[](#changing-the-column-default "Permalink")

可能有人认为，使用`change_column_default`更改默认列对于较大的表来说是一项昂贵且破坏性的操作，但实际上并非如此.

以以下迁移为例：

```
class DefaultRequestAccessGroups < ActiveRecord::Migration[5.2]
  DOWNTIME = false

  def change
    change_column_default(:namespaces, :request_access_enabled, from: false, to: true)
  end
end 
```

上面的迁移更改了我们最大的表之一的默认列值： `namespaces` . 可以将其翻译为：

```
ALTER TABLE namespaces
ALTER COLUMN request_access_enabled
DEFAULT false 
```

在这种情况下，默认值存在，我们只是更改了`request_access_enabled`列的元数据，这并不意味着重写`namespaces`表中的所有现有记录. 仅当使用默认值创建新列时，所有记录才会被重写.

**注意：** PostgresSQL 11.0 引入[了](https://www.depesz.com/2018/04/04/waiting-for-postgresql-11-fast-alter-table-add-column-with-a-non-null-default/)更快的[ALTER TABLE ADD COLUMN（具有非空默认值](https://www.depesz.com/2018/04/04/waiting-for-postgresql-11-fast-alter-table-add-column-with-a-non-null-default/) ），从而消除了在添加具有默认值的新列时重写表的需求.

由于上述原因，在单事务迁移中使用`change_column_default`是安全的，而不需要`disable_ddl_transaction!` .

## Updating an existing column[](#updating-an-existing-column "Permalink")

要将现有列更新为特定值，可以使用`update_column_in_batches` . 这会将更新分为批次，因此我们不会在单个语句中更新太多行.

这`some_column` `projects`表中的`foo`列更新为 10，其中`some_column`为`'hello'` ：

```
update_column_in_batches(:projects, :foo, 10) do |table, query|
  query.where(table[:some_column].eq('hello'))
end 
```

如果需要计算的更新，则可以将值包装在`Arel.sql` ，因此 Arel 将其视为 SQL 文字. 这也是[Rails 6](https://gitlab.com/gitlab-org/gitlab/-/issues/28497)的必需弃用.

下面的示例与上面的示例相同，但是该值设置为`bar`和`baz`列的乘积：

```
update_value = Arel.sql('bar * baz')

update_column_in_batches(:projects, :foo, update_value) do |table, query|
  query.where(table[:some_column].eq('hello'))
end 
```

像`add_column_with_default`一样，有一个 RuboCop cop 可以检测大表上的用法. 在`update_column_in_batches`的情况下，可以在大表上运行，只要它仅更新表中行的一小部分，而在未在 GitLab.com 登台环境上进行验证的情况下不要忽略它-或事先请别人为您这样做.

## Dropping a database table[](#dropping-a-database-table "Permalink")

删除数据库表并不常见，Rails 提供的`drop_table`方法通常被认为是安全的. 删除表格之前，请考虑以下因素：

如果您的表在高流量表上具有外键（如`projects` ），则`DROP TABLE`语句可能会失败，并出现**语句超时**错误. 确定哪些表是高流量可能很困难. 自我管理的实例可能会以不同的使用模式使用 GitLab 的不同功能，因此仅基于 GitLab.com 进行假设是不够的.

表**没有记录** （功能从未使用过），也**没有外键** ：

*   只需在迁移中使用`drop_table`方法即可.

```
def change
  drop_table :my_table
end 
```

Table **有记录** but **没有外键**:

*   第一版：删除与表相关的应用程序代码，例如模型，控制器和服务.
*   第二版：在迁移中使用`drop_table`方法.

```
def up
  drop_table :my_table
end

def down
  # create_table ...
end 
```

Table **有外键**:

*   第一版：删除与表相关的应用程序代码，例如模型，控制器和服务.
*   第二版：使用`with_lock_retries`帮助器方法删除外键. 在另一个迁移文件中使用`drop_table` .

**第二个版本的迁移：**

删除`projects`表上的外键：

```
# first migration file

def up
  with_lock_retries do
    remove_foreign_key :my_table, :projects
  end
end

def down
  with_lock_retries do
    add_foreign_key :my_table, :projects
  end
end 
```

放下桌子：

```
# second migration file

def up
  drop_table :my_table
end

def down
  # create_table ...
end 
```

## Integer column type[](#integer-column-type "Permalink")

默认情况下，整数列最多可容纳 4 个字节（32 位）的数字. 最大值为 2,147,483,647\. 创建一个以字节为单位保存文件大小的列时，请注意这一点. 如果以字节为单位跟踪文件大小，这会将最大文件大小限制为刚好超过 2GB.

要允许整数列最多容纳 8 个字节（64 位）的数字，请将限制明确设置为 8 个字节. 这将使该列最多`9,223,372,036,854,775,807`个值.

Rails 迁移示例：

```
add_column(:projects, :foo, :integer, default: 10, limit: 8) 
```

## Strings and the Text data type[](#strings-and-the-text-data-type "Permalink")

在 GitLab 13.0 中[引入](https://gitlab.com/gitlab-org/gitlab/-/issues/30453) .

有关更多信息，请参见[文本数据类型](database/strings_and_the_text_data_type.html)样式指南.

## Timestamp column type[](#timestamp-column-type "Permalink")

默认情况下，Rails 使用`timestamp`数据类型来存储没有时区信息的时间戳数据. 通过调用`add_timestamps`或`timestamps`方法来使用`timestamp`数据类型.

另外，Rails 将`:datetime`数据类型转换为`timestamp`一.

Example:

```
# timestamps
create_table :users do |t|
  t.timestamps
end

# add_timestamps
def up
  add_timestamps :users
end

# :datetime
def up
  add_column :users, :last_sign_in, :datetime
end 
```

代替使用这些方法，应该使用以下方法来存储带有时区的时间戳：

*   `add_timestamps_with_timezone`
*   `timestamps_with_timezone`
*   `datetime_with_timezone`

这样可以确保所有时间戳都有指定的时区. 反过来，这意味着当系统的时区更改时，现有时间戳不会突然使用其他时区. 这也使得非常清楚，首先使用了哪个时区.

## Storing JSON in database[](#storing-json-in-database "Permalink")

Rails 5 本机支持`JSONB` （二进制 JSON）列类型. 添加此列的示例迁移：

```
class AddOptionsToBuildMetadata < ActiveRecord::Migration[5.0]
  DOWNTIME = false

  def change
    add_column :ci_builds_metadata, :config_options, :jsonb
  end
end 
```

您必须使用序列化器来提供翻译层：

```
class BuildMetadata
  serialize :config_options, Serializers::JSON # rubocop:disable Cop/ActiveRecordSerialize
end 
```

使用`JSONB`列时，请使用[JsonSchemaValidator](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/validators/json_schema_validator.rb)来控制随时间推移插入的数据.

```
class BuildMetadata
  validates :config_options, json_schema: { filename: 'build_metadata_config_option' }
end 
```

## Testing[](#testing "Permalink")

请参阅" [测试 Rails 迁移](testing_guide/testing_migrations_guide.html)样式"指南.

## Data migration[](#data-migration "Permalink")

请比一般 ActiveRecord 语法更喜欢 Arel 和普通 SQL. 如果使用普通 SQL，则需要使用`quote_string` helper 手动引用所有输入.

Arel 的示例：

```
users = Arel::Table.new(:users)
users.group(users[:user_id]).having(users[:id].count.gt(5))

#update other tables with these results 
```

带有普通 SQL 和`quote_string`帮助器的示例：

```
select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag|
  tag_name = quote_string(tag["name"])
  duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]}
  origin_tag_id = duplicate_ids.first
  duplicate_ids.delete origin_tag_id

  execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})")
  execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})")
end 
```

如果需要更复杂的逻辑，则可以定义和使用迁移本地模型. 例如：

```
class MyMigration < ActiveRecord::Migration[6.0]
  class Project < ActiveRecord::Base
    self.table_name = 'projects'
  end

  def up
    # Reset the column information of all the models that update the database
    # to ensure the Active Record's knowledge of the table structure is current
    Project.reset_column_information

    # ... ...
  end
end 
```

这样做时，请确保显式设置模型的表名，这样它就不会派生自类名或名称空间.

最后，对于所有更新数据库的本地模型，请确保在迁移的`up`方法中运行`reset_column_information` .

这样做的原因是所有迁移类都在开始时加载（ `db:migrate`启动时），因此，如果另一个迁移更新了该模式，它们将与它们映射的表模式不同步. 这将导致在尝试插入基础表或对其进行更新时数据迁移失败，因为`ActiveRecord`将新列报告为`unknown attribute` .

### Renaming reserved paths[](#renaming-reserved-paths "Permalink")

引入新的项目路线时，它可能与任何现有记录冲突. 这些记录的路径应重命名，并且相关数据应在磁盘上移动.

由于我们已经必须做几次，因此现在有一些帮助程序可以帮助您.

要使用此功能，您可以在迁移中包括`Gitlab::Database::RenameReservedPathsMigration::V1` . 这将提供 3 种方法，您可以通过一种或多种需要拒绝的路径.

**`rename_root_paths`** ：这将使用给定名称重命名所有没有`parent_id`名称*空间*的路径.

**`rename_child_paths`** ：这将使用给定名称重命名所有具有`parent_id`名称*空间*的路径.

**`rename_wildcard_paths`** ：这将重命名所有*项目*的路径以及所有具有`project_id` *命名空间* .

这些行的`path`列将重命名为其先前的值，后跟一个整数. 例如： `users`将变成`users0`