11 个版本 (6 个重大更新)
0.7.0 | 2024年1月21日 |
---|---|
0.6.1 | 2022年8月3日 |
0.6.0 | 2022年4月22日 |
0.5.1 | 2022年2月5日 |
0.1.1 | 2022年1月8日 |
#791 在 数据库接口
165KB
3.5K SLoC
Reshape
还可以查看 ReshapeDB,一个从头开始构建的新数据库,旨在使零停机时间的模式和数据迁移尽可能简单和安全。如果您想了解它,请 联系!
Reshape 是一个易于使用、零停机时间的 Postgres 模式迁移工具。它自动处理通常需要停机或手动多步更改的复杂迁移。在迁移过程中,Reshape 确保同时提供旧模式和新的模式,让您可以逐步推出应用程序。它还将执行所有更改而不会进行过度的锁定,避免由阻止其他查询引起的中断。要更全面地了解 Reshape,请查看 介绍性博客文章。
适用于 Postgres 12 及更高版本。
工作原理
Reshape通过创建封装底层表的视图来工作,您的应用程序将与这些视图交互。在迁移过程中,Reshape将自动创建一组新的视图并设置触发器,以在旧架构和新架构之间翻译插入和更新。这意味着每次部署都是一个包含三个阶段的过程。
- 开始迁移 (
reshape migration start
):设置视图和触发器,以确保新旧架构同时可用。 - 部署应用程序:您的应用程序可以逐步部署而无需停机。现有部署将继续使用旧架构,而新部署将使用新架构。
- 完成迁移 (
reshape migration complete
):删除旧架构以及任何中间数据和触发器。
如果应用程序部署失败,您应该运行reshape migration abort
,这将回滚reshape migration start
所做的任何更改,而不会丢失数据。
入门
安装
二进制文件
二进制文件可在发布部分下获取,适用于macOS和Linux。
Cargo
可以使用Cargo安装Reshape(需要Rust 1.58或更高版本)。
cargo install reshape
Docker
Reshape作为Docker镜像可在Docker Hub上获取。
docker run -v $(pwd):/usr/share/app fabianlindfors/reshape reshape migration start
创建您的第一个迁移
每次迁移应存储在migrations/
目录下的单独文件中。文件可以是JSON或TOML格式,文件名将成为迁移的名称。我们建议为每个迁移添加一个递增的数字作为前缀,因为迁移是根据文件名排序的。
让我们创建一个简单的迁移来设置一个新的表users
,该表有两个字段,id
和name
。我们将创建一个名为migrations/1_create_users_table.toml
的文件。
[[actions]]
type = "create_table"
name = "users"
primary_key = ["id"]
[[actions.columns]]
name = "id"
type = "INTEGER"
generated = "ALWAYS AS IDENTITY"
[[actions.columns]]
name = "name"
type = "TEXT"
这相当于运行CREATE TABLE users (id INTEGER GENERATED ALWAYS AS IDENTITY, name TEXT)
。
准备您的应用程序
Reshape依赖于您的应用程序使用特定的架构。在您的应用程序中建立与Postgres的连接时,您需要运行一个查询来选择最新的架构。最简单的方法是使用辅助库之一。
如果您的应用程序没有可用的辅助库的语言,您可以使用命令生成查询:reshape schema-query
。例如,您可以在运行脚本中使用环境变量将其传递给应用程序:RESHAPE_SCHEMA_QUERY=$(reshape schema-query)
。然后在您的应用程序中
# Example for Python
reshape_schema_query = os.getenv("RESHAPE_SCHEMA_QUERY")
db.execute(reshape_schema_query)
运行您的迁移
要创建新的users
表,请运行
reshape migration start --complete
我们使用--complete
标志来自动完成迁移。在生产部署期间,您应该首先运行reshape migration start
,然后在应用程序完全部署后运行reshape migration complete
。
如果没有指定其他选项,Reshape 将尝试连接到本地主机上运行的 PostgreSQL 数据库,使用 postgres
作为用户名和密码。有关如何更改连接设置的详细信息,请参阅 连接选项。
在开发期间使用
在开发过程中添加新迁移时,我们建议运行 reshape migration start
而跳过 reshape migration complete
。这样,可以通过更新迁移文件并运行 reshape migration abort
后跟 reshape migration start
来迭代新的迁移。
编写迁移
基础
每个迁移包含一个或多个操作。这些操作将按顺序执行。以下是一个包含两个操作的迁移示例,用于创建两个表,分别是 customers
和 products
。
[[actions]]
type = "create_table"
name = "customers"
primary_key = ["id"]
[[actions.columns]]
name = "id"
type = "INTEGER"
generated = "ALWAYS AS IDENTITY"
[[actions]]
type = "create_table"
name = "products"
primary_key = ["sku"]
[[actions.columns]]
name = "sku"
type = "TEXT"
每个操作都有一个 type
。支持的类型如下所述。
表
创建表
create_table
操作将创建一个新的表,包含指定的列、索引和约束。您可以选择提供 up
选项,以从现有表中回填值。
示例:创建一个具有几个列和主键的 customers
表
[[actions]]
type = "create_table"
name = "customers"
primary_key = ["id"]
[[actions.columns]]
name = "id"
type = "INTEGER"
generated = "ALWAYS AS IDENTITY"
[[actions.columns]]
name = "name"
type = "TEXT"
# Columns default to nullable
nullable = false
# default can be any valid SQL value, in this case a string literal
default = "'PLACEHOLDER'"
示例:创建具有它们之间外键的 users
和 items
表
[[actions]]
type = "create_table"
name = "users"
primary_key = ["id"]
[[actions.columns]]
name = "id"
type = "INTEGER"
generated = "ALWAYS AS IDENTITY"
[[actions]]
type = "create_table"
name = "items"
primary_key = ["id"]
[[actions.columns]]
name = "id"
type = "INTEGER"
generated = "ALWAYS AS IDENTITY"
[[actions.columns]]
name = "user_id"
type = "INTEGER"
[[actions.foreign_keys]]
columns = ["user_id"]
referenced_table = "users"
referenced_columns = ["id"]
示例:根据现有的 users
表创建 profiles
表
[[actions]]
type = "create_table"
name = "profiles"
primary_key = ["user_id"]
[[actions.columns]]
name = "user_id"
type = "INTEGER"
[[actions.columns]]
name = "user_email"
type = "TEXT"
# Backfill from `users` table and copy `users.email` to `user_email` column
# This will perform an upsert based on the primary key to avoid duplicate rows
[actions.up]
table = "users"
values = { user_id = "id", user_email = "email" }
重命名表
rename_table
操作将更改现有表的名字。
示例:将 users
表的名称更改为 customers
[[actions]]
type = "rename_table"
table = "users"
new_name = "customers"
删除表
remove_table
操作将删除现有表。
示例:删除 users
表
[[actions]]
type = "remove_table"
table = "users"
添加外键
add_foreign_key
操作将在两个现有表之间添加外键。如果现有列值不是有效的引用,则迁移将失败。
示例:从 items
表创建到 users
表的外键
[[actions]]
type = "add_foreign_key"
table = "items"
[actions.foreign_key]
columns = ["user_id"]
referenced_table = "users"
referenced_columns = ["id"]
删除外键
remove_foreign_key
操作将删除现有外键。只有在迁移完成后才会删除外键,这意味着您的新应用程序必须继续遵守外键约束。
示例:从 users
表中删除外键 items_user_id_fkey
[[actions]]
type = "remove_foreign_key"
table = "items"
foreign_key = "items_user_id_fkey"
列
添加列
add_column
操作将为现有表添加一个新列。您可以提供 up
设置作为可选。这应该是一个 SQL 表达式,它将为所有现有行运行以回填新列。 up
还可以引用其他表以执行跨表迁移(请参阅 "跨表复杂更改")。
示例:向 products
表添加一个新列 reference
[[actions]]
type = "add_column"
table = "products"
[actions.column]
name = "reference"
type = "INTEGER"
nullable = false
default = "10"
示例:用一个现有的 name
列替换两个新列,分别是 first_name
和 last_name
[[actions]]
type = "add_column"
table = "users"
# Extract the first name from the existing name column
up = "(STRING_TO_ARRAY(name, ' '))[1]"
[actions.column]
name = "first_name"
type = "TEXT"
[[actions]]
type = "add_column"
table = "users"
# Extract the last name from the existing name column
up = "(STRING_TO_ARRAY(name, ' '))[2]"
[actions.column]
name = "last_name"
type = "TEXT"
[[actions]]
type = "remove_column"
table = "users"
column = "name"
# Reconstruct name column by concatenating first and last name
down = "first_name || ' ' || last_name"
示例:从未结构化的 JSON data
列中提取嵌套值到新的 name
列
[[actions]]
type = "add_column"
table = "users"
# #>> '{}' converts the JSON string value to TEXT
up = "data['path']['to']['value'] #>> '{}'"
[actions.column]
name = "name"
type = "TEXT"
示例:将 email
列从 users
表复制到 profiles
表
# `profiles` has `user_id` column which maps to `users.id`
[[actions]]
type = "add_column"
table = "profiles"
[actions.column]
name = "email"
type = "TEXT"
nullable = false
# When `users` is updated in the old schema, we write the email value to `profiles`
[actions.up]
table = "users"
value = "email"
where = "user_id = id"
修改列
alter_column
操作允许对现有列进行多种更改,例如重命名、更改类型和更改现有值。
在进行比重命名更复杂的更改时,应提供 up
和 down
。这些应该是确定如何在新旧列版本之间进行转换的 SQL 表达式。在这些表达式中,您可以通过列名引用当前列值。
示例:将 users
表中的 last_name
列重命名为 family_name
[[actions]]
type = "alter_column"
table = "users"
column = "last_name"
[actions.changes]
name = "family_name"
示例:将 users
表中的 reference
列类型从 INTEGER
更改为 TEXT
[[actions]]
type = "alter_column"
table = "users"
column = "reference"
up = "CAST(reference AS TEXT)" # Converts from integer value to text
down = "CAST(reference AS INTEGER)" # Converts from text value to integer
[actions.changes]
type = "TEXT" # Previous type was 'INTEGER'
示例:将 index
列的所有值增加一
[[actions]]
type = "alter_column"
table = "users"
column = "index"
up = "index + 1" # Increment for new schema
down = "index - 1" # Decrement to revert for old schema
[actions.changes]
name = "index"
示例:使 name
列不可为空
[[actions]]
type = "alter_column"
table = "users"
column = "name"
# Use "N/A" for any rows that currently have a NULL name
up = "COALESCE(name, 'N/A')"
[actions.changes]
nullable = false
示例:将 created_at
列的默认值更改为当前时间
[[actions]]
type = "alter_column"
table = "users"
column = "created_at"
[actions.changes]
default = "NOW()"
删除列
remove_column
操作将从表中删除现有列。您可以提供可选的 down
设置。这应该是一个 SQL 表达式,用于在插入或更新行时确定旧架构的值。 down
还可以引用另一个表以执行跨表迁移(参见 "跨表复杂变更")。当删除的列是 NOT NULL
或没有默认值时,必须提供 down
设置。
将删除覆盖该列的所有索引。
示例:从 users
表中删除 name
列
[[actions]]
type = "remove_column"
table = "users"
column = "name"
# Use a default value of "N/A" for the old schema when inserting/updating rows
down = "'N/A'"
示例:从 users
表中删除 email
列,并使用 profiles
表中的列代替
[[actions]]
type = "remove_column"
table = "users"
column = "email"
# Our application will use the `profiles.email` column instead
# For backwards compatibility, we will write back to the removed `email` column whenever `profiles` is changed
[actions.down]
table = "profiles"
value = "profiles.email"
where = "users.id = profiles.user_id"
索引
添加索引
add_index
操作将向现有表添加新索引。
示例:在 users
表上创建一个对 name
列的唯一索引
[[actions]]
type = "create_table"
name = "users"
primary_key = "id"
[[actions.columns]]
name = "id"
type = "INTEGER"
generated = "ALWAYS AS IDENTITY"
[[actions.columns]]
name = "name"
type = "TEXT"
[[actions]]
type = "add_index"
table = "users"
[actions.index]
name = "name_idx"
columns = ["name"]
# Defaults to false
unique = true
示例:在 products
表的 data
列上添加 GIN 索引
[[actions]]
type = "add_index"
table = "products"
[actions.index]
name = "data_idx"
columns = ["data"]
# One of: btree (default), hash, gist, spgist, gin, brin
type = "gin"
删除索引
remove_index
操作将删除现有索引。实际上,索引将在迁移完成后才会被删除。
示例:删除 name_idx
索引
[[actions]]
type = "remove_index"
index = "name_idx"
枚举
创建枚举
create_enum
操作将创建一个新的具有指定值的 枚举类型。
示例:添加一个具有三个可能值的 mood
枚举类型
[[actions]]
type = "create_enum"
name = "mood"
values = ["happy", "ok", "sad"]
删除枚举
remove_enum
操作将删除现有的 枚举类型。在运行迁移之前,请确保已删除枚举的所有使用。枚举类型将在迁移完成后才会被删除。
示例:删除 mood
枚举类型
[[actions]]
type = "remove_enum"
enum = "mood"
自定义
custom
操作允许您创建一个运行自定义 SQL 的迁移。由于它不提供零停机时间保证,因此应谨慎使用。当其他操作可行时,请尽可能使用其他操作,因为它们明确设计用于零停机时间。
有三个可选设置可用,它们都接受 SQL 查询。所有查询都必须是无状态的,例如,在可用的情况下使用 IF NOT EXISTS
。
start
:当使用reshape migration start
开始迁移时运行complete
:当使用reshape migration complete
完成迁移时运行abort
:当使用reshape migration abort
取消迁移时运行
示例:启用 PostGIS 和 pg_stat_statements 扩展
[[actions]]
type = "custom"
start = """
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
"""
abort = """
DROP EXTENSION IF EXISTS postgis;
DROP EXTENSION IF EXISTS pg_stat_statements;
"""
跨表的复杂更改
在创建表、添加列和删除列时,可用的 up
和 down
选项还可以执行涉及多个表更复杂的变化。
示例:将 email
列从 users
表移动到 profiles
表
[[actions]]
type = "add_column"
table = "profiles"
[actions.column]
name = "email"
type = "TEXT"
nullable = false
# When `users` is updated in the old schema, we write the email value to `profiles`
[actions.up]
table = "users"
value = "users.email"
where = "profiles.user_id = users.id"
[[actions]]
type = "remove_column"
table = "users"
column = "email"
# When `profiles` is changed in the new schema, we write the email address back to the removed column
[actions.down]
table = "profiles"
value = "profiles.email"
where = "users.id = profiles.user_id"
示例:将用户(users
)和账户(accounts
)之间的 1:N 关系转换为 N:M,并更改相关 role
的格式。
# Add `user_account_connections` as a junction table
[[actions]]
type = "create_table"
name = "user_account_connections"
primary_key = ["account_id", "user_id"]
[[actions.columns]]
name = "account_id"
type = "INTEGER"
[[actions.columns]]
name = "user_id"
type = "INTEGER"
# `role` is currently stored directly on the `users` table but is part of the relationship
[[actions.columns]]
name = "role"
type = "TEXT"
nullable = false
# Backfill the new table from `users` and uppercase the `role`
[actions.up]
table = "users"
values = { user_id = "id", account_id = "account_id", role = "UPPER(account_role)" }
where = "user_account_connections.user_id = users.id"
[[actions]]
type = "remove_column"
table = "users"
column = "account_id"
# When `user_account_connections` is updated, we write the `account_id` back to `users`
[actions.down]
table = "user_account_connections"
value = "user_account_connections.account_id"
where = "users.id = user_account_connections.user_id"
[[actions]]
type = "remove_column"
table = "users"
column = "account_role"
# When `user_account_connections` is updated, we write the lowercase role back to `users`
[actions.down]
table = "user_account_connections"
value = "LOWER(user_account_connections.role)"
where = "users.id = user_account_connections.user_id"
命令和选项
reshape migration start
启动新的迁移,应用 migrations/
目录下尚未应用的所有迁移。命令完成后,旧架构和新架构将同时可用。当你推出使用新架构的新版本应用程序后,应运行 reshape migration complete
。
选项
参见连接选项
选项 | 默认值 | 描述 |
---|---|---|
--complete , -c |
false |
在应用迁移后自动完成迁移。 |
--dirs |
migrations/ |
搜索迁移文件的目录。可以使用 --dirs dir1 dir2 dir3 指定多个目录。 |
reshape migration complete
完成之前使用 reshape migration complete
启动的迁移。
选项
参见连接选项
reshape migration abort
取消尚未完成的任何迁移。
选项
参见连接选项
reshape schema-查询
在您的应用程序使用数据库之前生成您需要运行的 SQL 查询。此命令不需要数据库连接。它将根据 migrations/
目录(或 --dirs
指定的目录)中的最新迁移生成查询。
如果您使用 Rust 编写应用程序,我们建议使用 Rust 辅助库。
查询看起来可能像 SET search_path TO migration_1_initial_migration
。
选项
选项 | 默认值 | 描述 |
---|---|---|
--dirs |
migrations/ |
搜索迁移文件的目录。可以使用 --dirs dir1 dir2 dir3 指定多个目录。 |
连接选项
以下选项可以与所有与 Postgres 通信的命令一起使用。使用 连接 URL 或分别指定每个连接选项。
所有选项也可以通过环境变量设置,而不是通过标志。如果存在 .env
文件,则变量将自动从中加载。
选项 | 默认值 | 环境变量 | 描述 |
---|---|---|---|
--url |
DB_URL |
您的 Postgres 数据库的 URL | |
--host |
localhost |
DB_HOST |
连接到 Postgres 时使用的主机名 |
--port |
5432 |
DB_PORT |
Postgres 监听的端口 |
--database |
postgres |
DB_NAME |
数据库名称 |
--username |
postgres |
DB_USERNAME |
Postgres 用户名 |
--password |
postgres |
DB_PASSWORD |
Postgres 密码 |
许可证
Reshape 在 MIT 许可证 下发布。
依赖项
~12–22MB
~343K SLoC