如何使用alembic对函数和触发器进行版本控制?

问题描述 投票:0回答:3

假设数据库中有一些带有函数的触发器,如下所示:

-- Insert a new entry into another table
-- every time a NEW row is inserted
CREATE FUNCTION trgfunc_write_log() RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO some_other_table (
        -- some columns
        meter_id,
        date_taken,
        temperature,
    ) values (
        NEW.meter_id,
        NEW.time_taken,
        NEW.temperature
    );

    return NEW;
END;
$$ language 'plpgsql';

-- The trigger itself: AFTER INSERT
CREATE TRIGGER trg_temperature_readings
AFTER INSERT ON temperature_readings
FOR EACH ROW
EXECUTE FUNCTION trgfunc_write_log();

通常,此触发器将位于我的 SqlAlchemy 模型旁边,并使用如下内容自动创建:

from sqlalchemy import DDL, event
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Reading(Base):
   ...

create_trigger = DDL(""" ...SQL... """)
event.listen(Reading.__table__, 'after_create', create_trigger)

通过 Alembic 迁移对此类触发器及其功能进行版本控制的最佳实践是什么?

python sql postgresql triggers alembic
3个回答
10
投票

我最近在应用程序中遇到了同样的问题,并在 Alembic Cookbook 中找到了这篇文章

它概述了创建一个对象的有点复杂的策略,该对象封装了用于在用于执行 Alembic 操作以升级和降级该架构对象的其他对象中创建视图、存储过程或触发器的名称和 SQL。在 Alembic 修订版中使用时,它看起来像这样:

from alembic import op
from my_module import ReplaceableObject


my_trigger = ReplaceableObject(
    "trigger_name",
    """...SQL..."""
)

def upgrade():
    op.create_trigger(my_trigger)

def downgrade():
    op.drop_trigger(my_trigger)

我的团队目前正在讨论与视图或存储过程相比,这个策略对于简单的触发器来说是否过于复杂。您可以更频繁地更新这些模式对象,从而使 Cookbook 抽象中概述的大部分行为比使用简单的触发器更有价值。

另一个建议的选项是这样的:

from alembic import op


create_trigger = """...SQL..."""
drop_trigger = """...SQL..."""

def upgrade():
    op.execute(create_trigger)

def downgrade():
    op.execute(drop_trigger)

这两个实现看起来几乎相同,这就是 Cookbook 抽象对于简单触发器来说不必要复杂的论点。


3
投票

使用 Alembic Utils (

pip install alembic_utils
) 非常简单。

在普通代码库中创建函数,例如

from alembic_utils.pg_function import PGFunction

trgfunc_write_log = PGFunction(
    schema="public",
    signature="trgfunc_write_log()",
    definition="""
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO some_other_table (
        -- some columns
        meter_id,
        date_taken,
        temperature,
    ) values (
        NEW.meter_id,
        NEW.time_taken,
        NEW.temperature
    );

    return NEW;
END;
$$ language 'plpgsql'
""")

trg_temperature_readings = PGTrigger(
    schema="public",
    signature="trg_temperature_readings",
    on_entity="public.temperature_readings",
    is_constraint=False,
    definition="""AFTER INSERT ON temperature_readings
        FOR EACH ROW
        EXECUTE FUNCTION trgfunc_write_log()""",
)

文档展示了如何修改你的 alembic ini 文件和 env.py 文件 - 一个问题是你必须注册实体,例如在env.py

from alembic_utils.replaceable_entity import register_entities from app.db.function import trg_temperature_readings, trgfunc_write_log register_entities([trg_temperature_readings, trgfunc_write_log])
然后 alembic 自动生成的迁移应该可以正常工作:

alembic revision --autogenerate -m 'add temperature log trigger'

.


0
投票
我无法使

alembic_utils

工作,可能是因为我的函数和视图在创建后就被DBMS规范化了,并且每次都被自动生成错误地检测为“更改”。我也无法使某些函数与 
alembic_utils
 一起使用,因为它尝试在执行之前以某种方式处理 SQL,但未能做到这一点。

因此,我为 Alembic 做了一个扩展(

Alembic Dumb DDL),它保留了每个自定义 DDL 脚本的本地状态,并将主脚本与它们进行比较(而不是数据库状态)。

通过这种方式,我的 DDL 脚本被定义在一个地方(现在允许在 git 历史记录中查看正确的差异),并且 autogenerate 命令现在支持任何类型的脚本,无论复杂性或使用的 DBMS 是什么。

此外,修订版现在看起来更漂亮,因为它们只是调用脚本的本地状态,而不是包含视图/函数/触发器的完整源代码。

def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.run_ddl_script("2024_01_08_1045_order_details_060d60b5c278.sql") op.run_ddl_script("2024_01_08_1045_customer_details_060d60b5c278.sql") # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.execute("DROP VIEW IF EXISTS customer_details;") op.run_ddl_script("2024_01_08_1016_order_details_af80846764cd.sql")
    
© www.soinside.com 2019 - 2024. All rights reserved.