#postgresql #database-schema #applications #migration #table #automatic #table-column

bin+lib reshape

Postgres 的一个易于使用、零停机时间的模式迁移工具

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数据库接口

MIT 许可证

165KB
3.5K SLoC

Reshape

Test status badge Latest release

还可以查看 ReshapeDB,一个从头开始构建的新数据库,旨在使零停机时间的模式和数据迁移尽可能简单和安全。如果您想了解它,请 联系

Reshape 是一个易于使用、零停机时间的 Postgres 模式迁移工具。它自动处理通常需要停机或手动多步更改的复杂迁移。在迁移过程中,Reshape 确保同时提供旧模式和新的模式,让您可以逐步推出应用程序。它还将执行所有更改而不会进行过度的锁定,避免由阻止其他查询引起的中断。要更全面地了解 Reshape,请查看 介绍性博客文章

适用于 Postgres 12 及更高版本。

工作原理

Reshape通过创建封装底层表的视图来工作,您的应用程序将与这些视图交互。在迁移过程中,Reshape将自动创建一组新的视图并设置触发器,以在旧架构和新架构之间翻译插入和更新。这意味着每次部署都是一个包含三个阶段的过程。

  1. 开始迁移 (reshape migration start):设置视图和触发器,以确保新旧架构同时可用。
  2. 部署应用程序:您的应用程序可以逐步部署而无需停机。现有部署将继续使用旧架构,而新部署将使用新架构。
  3. 完成迁移 (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,该表有两个字段,idname。我们将创建一个名为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 来迭代新的迁移。

编写迁移

基础

每个迁移包含一个或多个操作。这些操作将按顺序执行。以下是一个包含两个操作的迁移示例,用于创建两个表,分别是 customersproducts

[[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'"

示例:创建具有它们之间外键的 usersitems

[[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_namelast_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 操作允许对现有列进行多种更改,例如重命名、更改类型和更改现有值。

在进行比重命名更复杂的更改时,应提供 updown。这些应该是确定如何在新旧列版本之间进行转换的 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;
"""

跨表的复杂更改

在创建表、添加列和删除列时,可用的 updown 选项还可以执行涉及多个表更复杂的变化。

示例:将 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