为 SQLite 和其他数据库运行“批处理”迁移#
SQLite 数据库对迁移工具提出了挑战,因为它几乎不支持关系模式迁移所依赖的 ALTER 语句。其背后的原理源于 SQLite 中的哲学和架构问题,并且不太可能发生改变。
相反,迁移工具需要生成与新结构相对应的 SQLite 表的副本,将数据从现有表传输到新表,然后删除旧表。为了以一种合理可预测的方式适应这种情况,同时仍然与其他数据库兼容,Alembic 提供了批处理操作上下文。
在此上下文中,关系表被命名,然后在块中指定对该表的系列变更操作。当上下文完成时,将开始一个“移动和复制”过程;从数据库中反映现有表结构,使用给定的更改创建此表的新版本,使用“INSERT from SELECT”将数据从旧表复制到新表,最后删除旧表并将新表重命名为原始名称。
方法 Operations.batch_alter_table()
为此过程提供了网关
with op.batch_alter_table("some_table") as batch_op:
batch_op.add_column(Column('foo', Integer))
batch_op.drop_column('bar')
当在迁移脚本中调用上述指令时,在 SQLite 后端,我们会看到类似这样的 SQL
CREATE TABLE _alembic_batch_temp (
id INTEGER NOT NULL,
foo INTEGER,
PRIMARY KEY (id)
);
INSERT INTO _alembic_batch_temp (id) SELECT some_table.id FROM some_table;
DROP TABLE some_table;
ALTER TABLE _alembic_batch_temp RENAME TO some_table;
在其他后端,我们会看到通常的 ALTER
语句,就好像没有批处理指令一样 - 默认情况下,批处理上下文仅在使用 SQLite 时执行“移动和复制”过程,并且如果存在除 Operations.add_column()
之外的其他迁移指令(这是 SQLite 支持的一种列级 ALTER 语句)。可以将 Operations.batch_alter_table()
配置为在所有情况下无条件运行“移动和复制”,包括在除 SQLite 之外的数据库上;更多内容如下。
控制表反射#
在执行“移动和复制”时反射的 Table
对象使用标准 autoload=True
方法执行。可以使用 reflect_args
和 reflect_kwargs
参数影响此调用。例如,要在反射过程中覆盖 Column
,以便 Boolean
对象以 create_constraint
标志设置为 False
进行反射
with self.op.batch_alter_table(
"bar",
reflect_args=[Column('flag', Boolean(create_constraint=False))]
) as batch_op:
batch_op.alter_column(
'flag', new_column_name='bflag', existing_type=Boolean)
另一个用例,将监听器添加到 Table
,以便在反射时可以将特殊逻辑应用于列或类型,使用 column_reflect()
事件
def listen_for_reflect(inspector, table, column_info):
"correct an ENUM type"
if column_info['name'] == 'my_enum':
column_info['type'] = Enum('a', 'b', 'c')
with self.op.batch_alter_table(
"bar",
reflect_kwargs=dict(
listeners=[
('column_reflect', listen_for_reflect)
]
)
) as batch_op:
batch_op.alter_column(
'flag', new_column_name='bflag', existing_type=Boolean)
处理约束#
在“批处理”模式下使用约束(如 FOREIGN KEY、CHECK 和 UNIQUE 约束)时会遇到各种问题。本节将尝试详细说明许多此类场景。
删除未命名或已命名外键约束#
与任何其他数据库不同,SQLite 允许数据库中存在没有标识名称的约束。在所有其他后端中,如果未提供名称,目标数据库始终会生成某种名称。
这带来的挑战是,BatchOperations.drop_constraint()
方法本身无法针对未命名约束。只要在不传递名称的情况下使用 ForeignKey
或 ForeignKeyConstraint
对象,就会隐式地存在一个未命名的 FOREIGN KEY 约束。仅在 SQLite 中,当在目标数据库中创建这些约束时,它们才会完全保持未命名状态;在所有其他数据库后端中,都会分配一个自动生成的名称。
在批处理模式的范围内,这会产生一个问题,即 BatchOperations.drop_constraint()
方法需要一个约束名称才能针对正确的约束。
为了克服这个问题,Operations.batch_alter_table()
方法支持 naming_convention
参数,以便可以为所有反射的约束(包括未命名的外键)指定一个名称,如 命名约定与操作的集成、自动生成 中所述。用法如下
naming_convention = {
"fk":
"fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
}
with self.op.batch_alter_table(
"bar", naming_convention=naming_convention) as batch_op:
batch_op.drop_constraint(
"fk_bar_foo_id_foo", type_="foreignkey")
请注意,命名约定功能至少需要SQLAlchemy 0.9.4才能支持。
更改布尔型、枚举型和其他隐式 CHECK 数据类型的类型#
SQLAlchemy 类型 Boolean
和 Enum
属于称为“模式”类型的类型类别;这种类型的类型会与类型本身一起创建其他结构,最常见(但并非总是)是 CHECK 约束。
Alembic 在此自动处理删除和创建 CHECK 约束,包括在批处理模式中。在更改现有列的类型时,需要完全指定现有类型
with self.op.batch_alter_table("some_table") as batch_op:
batch_op.alter_column(
'q', type_=Integer,
existing_type=Boolean(create_constraint=True, constraint_name="ck1"))
在删除包含命名 CHECK 约束的列时,从 Alembic 1.7 开始,此命名约束也必须使用类似的形式提供,因为 Alembic 无法将此反射的 CHECK 约束链接为属于特定列
with self.op.batch_alter_table("some_table") as batch_op:
batch_op.drop_column(
'q',
existing_type=Boolean(create_constraint=True, constraint_name="ck1"))
)
在 1.7 版本中更改:BatchOperations.drop_column()
操作可以接受 existing_type
指令,其中可以指定“模式类型”,例如 Boolean
和 Enum
,以便可以删除关联的命名约束。
包括 CHECK 约束#
从 Alembic 1.7 开始,命名 CHECK 约束会自动包含在批处理模式中,因为现代 SQLAlchemy 版本能够像反射任何其他约束一样反射这些约束。
请注意,当删除或重命名在命名的 CHECK 约束中提到的列时,必须先显式删除此 CHECK 约束,因为 Alembic 无法将反映的 CHECK 约束链接到该列。假设 some_table
的列 q
在名为 ck1
的 CHECK 约束中被提及。为了删除此列,我们还必须删除检查约束
with self.op.batch_alter_table("some_table") as batch_op:
batch_op.drop_constraint("ck1", type_="check")
batch_op.drop_column('q')
1.7 版中已更改: 命名的 CHECK 约束以与任何其他类型的约束相同的方式参与批处理模式。这要求列删除或重命名现在包括显式指令,以删除引用此列的现有命名约束,否则不会自动检测到它与该特定列相关联。
未命名的 CHECK 约束仍会从表重新创建操作中自动省略。
对于未命名的 CHECK 约束,它们仍然不会自动包含在批处理中。请注意,此限制包括由 Boolean
或 Enum
数据类型生成的 CHECK 约束,在 SQLAlchemy 1.3 中会自动生成 CHECK 约束,并且无法跟踪到反映的表,假设它们是以未命名的方式生成的。
如果要将未命名的约束包含在重新创建的表中,则可以明确地声明它们
with op.batch_alter_table("some_table", table_args=[
CheckConstraint('x > 5')
]) as batch_op:
batch_op.add_column(Column('foo', Integer))
batch_op.drop_column('bar')
上述步骤仅需要针对明确声明为表定义一部分的 CHECK 约束执行。
对于由 Boolean
或 Enum
等数据类型生成的 CHECK 约束,类型对象本身必须命名才能将它们的 CHECK 约束包含在批处理中。未设置 .name
属性的布尔和枚举数据类型不会重新生成 CHECK 约束。可以通过指定 .name
参数或使用命名的 Python Enum
对象作为枚举源来设置此名称。
处理引用外键#
务必注意,批量表操作不适用于强制参照完整性的外键。这是因为目标表已删除;如果外键引用它,这将引发错误。在 SQLite 上,外键是否实际强制由 PRAGMA FOREIGN KEYS
pragma 控制;如果使用此 pragma,则在工作流模式继续进行时必须禁用它。操作完成后,批量迁移的表将具有与开始时相同的名称,因此那些引用外键将再次引用此表。
处理自引用外键是一个特殊情况。在此,Alembic 采取特殊步骤,将自引用外键重新创建为引用原始表名,而不是“temp”表,以便像其他外键约束一样,当表重命名为其原始名称时,外键再次引用正确的表。此操作仅在禁用参照完整性时才有效,这与其他表引用外键的要求一致。
当 SQLite 的 PRAGMA FOREIGN KEYS
模式开启时,它确实提供了外键约束(包括自引用)在表重命名期间将自动修改为指向其表的服务,但此模式会阻止删除目标表,而批量迁移需要删除目标表。因此,如果迁移试图重命名表而不是批量迁移表,则可能需要操作 PRAGMA FOREIGN KEYS
设置。
在离线模式下工作#
在前面的部分中,我们已经了解到“移动和复制”过程在多大程度上强调使用反射来了解要复制的表的结构。这意味着在典型情况下,“在线”模式(其中存在活动数据库连接,以便 Operations.batch_alter_table()
可以从数据库中反映表)是必需的;不能在没有额外步骤的情况下使用 --sql
标志。
为了支持离线模式,系统必须在没有表反射的情况下工作,这意味着必须将打算创建的完整表传递给 Operations.batch_alter_table()
,使用 copy_from
meta = MetaData()
some_table = Table(
'some_table', meta,
Column('id', Integer, primary_key=True),
Column('bar', String(50))
)
with op.batch_alter_table("some_table", copy_from=some_table) as batch_op:
batch_op.add_column(Column('foo', Integer))
batch_op.drop_column('bar')
上述使用模式相当繁琐,与 Alembic 首选的工作风格相去甚远;但是,如果需要执行与 SQLite 兼容的“移动和复制”迁移,并且需要它们在“离线”模式下生成平面 SQL 文件,则没有太多其他选择。
自动生成批处理模式#
批处理模式的语法本质上是 Operations.batch_alter_table()
用于进入批处理块,并且返回的 BatchOperations
上下文就像常规 Operations
上下文一样,只是省略了“表名”和“架构名”参数。
要支持为自动生成批处理模式中的迁移命令进行渲染,请配置 EnvironmentContext.configure.render_as_batch
标志在 env.py
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=True
)
自动生成现在将沿以下行生成
def upgrade():
### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('address', schema=None) as batch_op:
batch_op.add_column(sa.Column('street', sa.String(length=50), nullable=True))
在所有情况下,此模式都是安全的,因为 Operations.batch_alter_table()
指令默认情况下仅适用于 SQLite;其他后端的行为将与通常在没有批处理指令的情况下一样。
请注意,自动生成支持不包括“脱机”模式,其中使用 Operations.batch_alter_table.copy_from
参数。如果需要,则需要手动将此处的表定义输入到迁移文件中。
使用 SQLite 以外的数据库的批处理模式#
有些商店有一个奇怪的用例,在某些情况下,对于已经支持 ALTER 的数据库,“移动和复制”样式的迁移很有用。在某些情况下,ALTER 操作可能会长时间阻止对表的访问,这可能是不可接受的。“移动和复制”可以在其他后端上工作,但有一些额外的注意事项。
如果传递标志 recreate='always'
,则批处理模式指令将运行“重新创建”系统,无论后端如何
with op.batch_alter_table("some_table", recreate='always') as batch_op:
batch_op.add_column(Column('foo', Integer))
在此模式下出现的问题主要与约束有关。具有 InnoDB 的 Postgresql 和 MySQL 等数据库将在所有情况下强制执行引用完整性(例如,通过外键)。与 SQLite 不同,关闭全局引用完整性并不那么简单(也不可取)。由于新表将替换旧表,因此在批处理操作之前需要无条件删除引用目标表的现有外键约束,并在之后重新创建以引用新表。批处理模式目前不为此提供任何自动化功能。
Postgresql 数据库和可能的其他数据库的行为也如此,即当创建新表时,新表的命名约束与旧表的命名约束发生命名冲突,因为它们与旧表的命名约束匹配,并且在 Postgresql 上,这些名称需要在所有表中唯一。因此,Postgresql 方言将在创建新表之前为旧表上的所有约束发出“DROP CONSTRAINT”指令;如果操作失败,这是“安全”的,因为 Postgresql 还支持事务性 DDL。
请注意,与 SQLite 的情况一样,CHECK 约束需要使用 Operations.batch_alter_table.table_args
参数在旧表和新表之间手动移动。