【问题标题】:Using the SQLAlchemy ORM inside an Alembic migration: how do I?在 Alembic 迁移中使用 SQLAlchemy ORM:我该怎么做?
【发布时间】:2012-11-20 12:50:02
【问题描述】:

我目前有一列包含 HTML 标记。在该标记中,有一个我想存储在新列中的时间戳(以便我可以查询它)。我的想法是在一次迁移中执行以下操作:

  1. 为数据创建一个可以为空的新列
  2. 使用 ORM 拉回我需要解析的 HTML
  3. 对于每一行
    1. 解析 HTML 以提取时间戳
    2. 更新 ORM 对象

但是当我尝试运行迁移时,它似乎陷入了无限循环。到目前为止,这是我所得到的:

def _extract_publication_date(html):
    root = html5lib.parse(html, treebuilder='lxml', namespaceHTMLElements=False)
    publication_date_string = root.xpath("//a/@data-datetime")[0]
    return parse_date(publication_date)


def _update_tip(tip):
    tip.publication_date = _extract_publication_date(tip.rendered_html)
    tip.save()


def upgrade():
    op.add_column('tip', sa.Column('publication_date', sa.DateTime(timezone=True)))
    tips = Tip.query.all()
    map(tips, _update_tip)


def downgrade():
    op.drop_column('tip', 'publication_date')

【问题讨论】:

  • 你怎么知道它陷入了无限循环?
  • 如果Tip.query 没有使用与op 相同的会话,那么将有2 个事务,其中SELECT 一个卡住等待ALTER TABLE 一个提交。无论如何,我认为将 ORM 部分移动到它自己的脚本中更干净,在alembic upgrade 之后手动运行。
  • @X-Istence 我不知道它陷入了无限循环。我确实知道该命令永远不会返回。
  • @sayap 我曾考虑过,但这意味着我无法在同一个地方跟踪我的所有数据库升级逻辑。此外,如果 ORM 逻辑有效,那么我可以在迁移中添加另一个结构步骤以创建新列 NOT NULL。如果我能弄清楚如何让 ORM 使用 seame Session 作为 op.foo 方法,我会更喜欢它。
  • 我也努力将逻辑放在同一个地方,但前提是我可以使用 SQL 做到这一点。 Alembic 不建议迁移脚本和应用程序代码之间的紧密耦合。虽然我确实看到了你的观点。您可能想尝试先进行查询并构建id: publication_date 的字典,然后使用op.execute() 对其进行更新。如果您的 ORM 会话有autocommit=True,我认为第一个事务将在查询后立即关闭,因此不会有重叠事务。

标签: python sqlalchemy alembic


【解决方案1】:

从 cmets 继续,你可以试试这样的:

import sqlalchemy as sa


tip = sa.sql.table(
    'tip',
    sa.sql.column('id', sa.Integer),
    sa.sql.column('publication_date', sa.DateTime(timezone=True)),
)


def upgrade():
    mappings = [
        (x.id, _extract_publication_date(x.rendered_html))
        for x in Tip.query
    ]

    op.add_column('tip', sa.Column('publication_date', sa.DateTime(timezone=True)))

    exp = sa.sql.case(value=tip.c.id, whens=(
        (op.inline_literal(id), op.inline_literal(publication_date))
        for id, publication_date in mappings.iteritems()
    ))

    op.execute(tip.update().values({'publication_date': exp}))


def downgrade():
    op.drop_column('tip', 'publication_date')

【讨论】:

    【解决方案2】:

    对我有用的是通过执行以下操作来获得会话:

    connection = op.get_bind()
    Session = sa.orm.sessionmaker()
    session = Session(bind=connection)
    

    【讨论】:

    • 这个某种对我有用,虽然有错误说我的模型已经绑定到一个会话。
    【解决方案3】:

    在使用@velochy 的答案进行了一些实验后,我确定了类似以下在 Alembic 中使用 SqlAlchemy 的模式。这对我很有用,可能可以作为 OP 问题的一般解决方案:

    from sqlalchemy.orm.session import Session
    from alembic import op
    
    # Copy the model definitions into the migration script if
    # you want the migration script to be robust against later
    # changes to the models. Also, if your migration includes
    # deleting an existing column that you want to access as 
    # part of the migration, then you'll want to leave that 
    # column defined in the model copies here.
    class Model1(Base): ...
    class Model2(Base): ...
    
    def upgrade():
        # Attach a sqlalchemy Session to the env connection
        session = Session(bind=op.get_bind())
    
        # Perform arbitrarily-complex ORM logic
        instance1 = Model1(foo='bar')
        instance2 = Model2(monkey='banana')
    
        # Add models to Session so they're tracked
        session.add(instance1)
        session.add(instance2)
    
        # Apply a transform to existing data
        m1s = session.query(Model1).all()
        for m1 in m1s:
            m1.foo = transform(m1.foo)
        session.commit()
    
    def downgrade():
        # Attach a sqlalchemy Session to the env connection
        session = Session(bind=op.get_bind())
    
        # Perform ORM logic in downgrade (e.g. clear tables)
        session.query(Model2).delete()
        session.query(Model1).delete()
    
        # Revert transform of existing data
        m1s = session.query(Model1).all()
        for m1 in m1s:
            m1.foo = un_transform(m1.foo)
        session.commit()
    

    这种方法似乎可以正确处理事务。在处理这个问题时,我经常会生成数据库异常,它们会按预期回滚。

    【讨论】:

    • 值得注意的是,如果您的模型经常更改,则此技术可能是不明智的。当模型发生变化时,它可能会破坏假设模型具有特定形状的旧迁移。
    • 如果模型发生变化,我不会说这种技术是不明智的,只是在迁移中定义模型而不是导入可能更安全。
    • 是的,复制模型以在旧迁移中使用是解决此问题的合法技术。然后,可以在 不破坏旧内容的情况下进行更改。然后主要的问题变成了这是否会产生足够的额外工作来增加负担。这取决于,IMO。可能没问题!
    • 我编辑了答案以建议在迁移中定义模型。请注意,在某些特殊情况下,需要在迁移中定义模型,例如从模型中删除一列但在删除之前将该列用作迁移的一部分。
    猜你喜欢
    • 1970-01-01
    • 2014-12-02
    • 1970-01-01
    • 2018-09-09
    • 2018-12-15
    • 1970-01-01
    • 2018-06-29
    • 1970-01-01
    • 2018-01-16
    相关资源
    最近更新 更多