返回

Alembic 级联删除与升级降级: 最佳实践指南

python

Alembic 级联删除与升级降级顺序

在数据库 Schema 迁移管理中,使用 Alembic 这类工具可以帮助开发人员高效地进行数据库结构的变更,如创建、删除、修改表、索引等。但实际应用中,级联删除和升级降级的顺序往往会带来一些挑战。这篇文章会深入探讨这两个常见问题并给出可行的解决方案。

级联删除失败:依赖关系作祟

当尝试删除一个表,而这个表又被其他表通过外键关联时,PostgreSQL 默认会拒绝执行删除操作。例如,表 terminzavrseniTerminsportistaTermin 外键引用,如果直接尝试删除 termin 表,将会报错。这是由于外键约束导致的。即使你在 SQLAlchemy 的模型中配置了 cascade="all, delete-orphan",也只是影响 ORM 行为,无法解决数据库层的约束问题。

解决方案:CASCADE约束的运用

要解决这个问题,需要在创建外键时显式地指定 ON DELETE CASCADE 约束。这将确保在删除父表记录时,相关子表记录也会被自动删除,避免了级联删除失败。

操作步骤:

  1. 在模型定义外键关系时,添加 ondelete="CASCADE" 参数。修改模型中所有使用了 ForeignKey的地方,代码片段如下(其他类似的请参考示例修改):
class ZavrseniTermin(Base):
    __tablename__ = "zavrseniTermin"

    idZavrsenogTermina = Column(Integer, primary_key=True, unique=True, autoincrement=True)
    idTermina = Column(Integer, ForeignKey('termin.idTermina', ondelete="CASCADE"))  # 注意这里的修改
    ocjena = Column(Float)

    termin = relationship("Termin", back_populates="zavrseniTermin", cascade="all, delete-orphan")

class SportistaTermin(Base):
    __tablename__ = "sportistaTermin"

    idSportistaTermin = Column(Integer, primary_key=True, unique=True, autoincrement=True)
    idTermina = Column(Integer, ForeignKey('termin.idTermina', ondelete="CASCADE"))  # 注意这里的修改
    idSportiste = Column(Integer, ForeignKey('sportista.idSportiste', ondelete="CASCADE")) #注意这里的修改
    statusTermina = Column(String)

    termin = relationship("Termin", back_populates="sportistaTermin", cascade="all, delete-orphan")
    sportista = relationship("Sportista", back_populates="sportistaTermin", cascade="all, delete-orphan")

  1. 生成新的 Alembic 版本,并执行迁移操作。由于修改了模型的ForeignKey定义,需要让 Alembic 知道,因此:

    alembic revision --autogenerate -m "添加on delete cascade"
    alembic upgrade head
    

通过查看生成的新版本文件,可以看到自动生成了一些ALTER语句,修改外键约束添加on delete cascade.

  1. 此后进行删除表操作时, alembic会按照指定删除顺序自动解决外键依赖问题,正确完成级联删除.

注意: 使用 ON DELETE CASCADE 需要谨慎,它可能会导致意外数据删除,特别是在有深度嵌套关系的情况下。建议在设计时,充分考虑数据删除的潜在影响。

升级降级顺序颠倒:迁移逻辑错乱

观察初始的升级和降级操作可以发现,它犯了一个常见的错误:降级操作实际上是在重新创建升级操作中删除的表。正确的顺序应是:upgrade 操作执行对数据库的更改(比如创建表, 添加约束),downgrade 执行与 upgrade 相反的操作(例如删除表,删除约束)。
当前状态下,upgrade 删除所有表,而 downgrade 又重新创建了它们,这样完全颠倒了迁移逻辑。这会导致如果回滚迁移,可能会造成数据丢失或重复数据的问题。

解决方案:颠倒 upgrade/downgrade 命令

要修复这个问题,核心是将 upgradedowngrade 函数中的操作对调,即 upgrade 应负责创建表, downgrade 应负责删除表。

操作步骤:

  1. 创建一个新的迁移文件:
    alembic revision --autogenerate -m "修复升级降级逻辑"
    
  2. 编辑自动生成的迁移文件。将创建表结构的操作移动到upgrade 函数中,删除表结构的操作移动到downgrade 函数中。具体做法是,拷贝上面'kreirana'版本中 downgrade 函数的内容放到新版本的 upgrade函数中,拷贝上面'kreirana'版本中 upgrade 函数的内容放到新版本的 downgrade函数中,注意去掉不需要的postgresql_ignore_search_path=False配置。以下是一个修改后的迁移脚本片段示例(其他表也需按相同方式调整):
def upgrade() -> None:
    op.create_table('radnoVrijeme',
    sa.Column('idRadnogVremena', sa.INTEGER(), server_default=sa.text('nextval(\'radnoVrijeme_idRadnogVremena_seq\'::regclass)'), autoincrement=True, nullable=False),
    sa.Column('pocetak', postgresql.TIME(), autoincrement=False, nullable=True),
    sa.Column('kraj', postgresql.TIME(), autoincrement=False, nullable=True),
    sa.PrimaryKeyConstraint('idRadnogVremena', name='radnoVrijeme_pkey')
    )
    op.create_table('banka',
    sa.Column('idRacuna', sa.INTEGER(), server_default=sa.text('nextval(\'banka_idRacuna_seq\'::regclass)'), autoincrement=True, nullable=False),
    sa.Column('balans', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=False),
    sa.Column('ziroRacun', sa.VARCHAR(), autoincrement=False, nullable=False),
    sa.Column('pin', sa.VARCHAR(), autoincrement=False, nullable=False),
    sa.PrimaryKeyConstraint('idRacuna', name='banka_pkey'),
    sa.UniqueConstraint('ziroRacun', name='banka_ziroRacun_key')
    )
  ...#其他创建表的语句

def downgrade() -> None:
    op.drop_table('posjedovanje')
    op.drop_table('turnir')
    op.drop_table('termin')
    op.drop_table('admin')
    op.drop_table('dvorana')
    op.drop_table('sportistaTermin')
    op.drop_index('ix_user_idUsera', table_name='user')
    op.drop_index('ix_user_username', table_name='user')
    op.drop_table('user')
    op.drop_table('dostupniSportovi')
    op.drop_table('slike')
    op.drop_table('sportista')
    op.drop_table('zavrseniTermin')
    op.drop_table('sport')
    op.drop_table('prijateljstva')
    op.drop_table('vlasnik')
    op.drop_table('dostupnost')
    op.drop_table('banka')
    op.drop_table('radnoVrijeme')
  1. 执行 alembic upgrade head 。此时 Alembic 会按照新文件的配置重新操作数据库。
  2. 验证 upgrade/downgrade 操作,使用alembic downgrade base 回滚数据库,然后再使用alembic upgrade head升级数据库,确认整个流程是否符合预期,数据是否正确创建和回滚。

额外安全建议:

在调整 Alembic 迁移脚本时,始终建议在测试数据库上先进行测试。 尤其当操作涉及到数据的修改或删除时,更加需要小心。可以在每次生成新的版本脚本之后,都使用 alembic 的命令,在本地测试数据库上执行 alembic upgrade headalembic downgrade base 来确保 upgrade/downgrade操作的正确性。

总而言之,解决 Alembic 级联删除和升级降级顺序问题,关键在于理解外键约束,善用 ON DELETE CASCADE 约束以及保证 upgradedowngrade 函数的操作正确。只有这样才能保证数据库 Schema 迁移过程安全且高效。