如何在sqlalchemy中防止相关对象的持久化?

问题描述 投票:1回答:1

环境

  • python 3.7
  • SqlAlchemy 1.3.10(我也在1.2.16上进行了测试,结果相同)
  • PostgreSQL 11

TL,DR:我有一张表和1-1个“相关”的物化视图(没有fk,该关系在sql端是隐式的)。在SQLAlchemy中,此关系声明为viewonly=True(以防万一,也声明为backref)。但是,如果我分配给它,则会话无论如何都会尝试插入分配的mat view对象(显然,因为它是一个物化视图,所以会失败)。

我是不是误解了viewonly的目的,还是关系建立不正确?

详细信息(包含代码示例和实际的可重现测试用例):

让我们从数据模型开始:

CREATE TABLE universe (
   id SERIAL PRIMARY KEY,
   name VARCHAR NOT NULL,
   is_perfect BOOLEAN NULL DEFAULT 'f'
);

CREATE MATERIALIZED VIEW answer AS (
    SELECT
        id,
        trunc(random() * 100)::INT AS computed
    FROM universe
)

IRL,mat视图要做很多工作,并从许多表中提取数据以计算其结果。但是出于说明目的,这样做会很好。

现在的型号:

class Universe(Base):
    __tablename__ = 'universe'

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String, nullable=False)
    is_perfect = sa.Column(sa.Boolean, nullable=False, server_default='f')

    answer: Answer = relationship('Answer',
                                  backref=backref('universe', uselist=False, viewonly=True),
                                  innerjoin=False,
                                  uselist=False,
                                  viewonly=True)

    def set_perfect(self):
        """
        You'll have to excuse this method, it is just for illustration purposes.
        I wouldn't code it like this IRL.
        """
        self.is_perfect = (self.answer.computed == 42)

        session = object_session(self)
        if session:
            session.commit()


class Answer(Base):
    __tablename__ = 'answer'

    id = sa.Column(sa.Integer, sa.ForeignKey('universe.id'), primary_key=True)
    computed = sa.Column(sa.Integer, nullable=False)

Universe类(表)与Answer类(垫视图)有关系。关系和后向引用均为viewonly=True

现在测试:

class UniverseTests(TestCase):
    def test__is_perfect(self):
        session = Session()
        universe: Universe = session.query(Universe).get(1)

        universe.answer = Answer(id=1, computed=42)
        universe.set_perfect()
        assert universe.is_perfect is True

您可以看到,在测试期间,我“需要”将伪造的Answer(id=1, computed=42)分配给universe.answer

在测试过程中从mat视图中获取真实数据很痛苦,因为它需要创建一个完整的对象网络,而在大多数情况下,我只在单个对象上测试一种简单的方法(是的,对一个真实的数据库进行测试...我知道这是皱着眉头的,但是我喜欢这种方式。

所以“需要”实际上是“我不想因为我很懒,而且我已经习惯了……而且,模拟效果更好”(我知道!伪善!😱)。

set_perfect()函数期间的测试错误,因为对session.commit()的调用尝试将Answer(id=1, computed=42)对象与Universe对象上的更新字段一起提交。

此外,还有一个问题:我是否错过了一种更简单,更可行的方法来进行此操作?

错误消息的示例

sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) cannot change materialized view "answer"

[SQL: INSERT INTO answer (id, computed) VALUES (%(id)s, %(computed)s)]
[parameters: {'id': 1, 'computed': 42}]
(Background on this error at: http://sqlalche.me/e/f405)

最后是一个完整的代码示例(与上面的代码相同,但可以运行):

  • 使用SQLalchemy 1.3.10设置python 3.7 venv
  • 创建一个新的(空的)PostgreSQL数据库
  • 将此代码示例保存在文件中,并将DB_URI值更改为刚创建的值
  • 在环境中执行文件(python <path-to-script>
from __future__ import annotations

import unittest
from unittest import TestCase

import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
from sqlalchemy.orm import relationship, backref, sessionmaker, object_session

create_sql = """
CREATE TABLE universe (
   id SERIAL PRIMARY KEY,
   name VARCHAR NOT NULL,
   is_perfect BOOLEAN NULL DEFAULT 'f'
);

CREATE MATERIALIZED VIEW answer AS (
    SELECT
        id,
        trunc(random() * 100)::INT AS computed
    FROM universe
)
"""


Base: DeclarativeMeta = declarative_base()
metadata = Base.metadata


class Universe(Base):
    __tablename__ = 'universe'

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String, nullable=False)
    is_perfect = sa.Column(sa.Boolean, nullable=False, server_default='f')

    answer: Answer = relationship('Answer',
                                  backref=backref('universe', uselist=False, viewonly=True),
                                  innerjoin=False,
                                  lazy='select',
                                  uselist=False,
                                  viewonly=True)

    def set_perfect(self):
        self.is_perfect = (self.answer.computed == 42)

        session = object_session(self)
        if session:
            session.commit()


class Answer(Base):
    __tablename__ = 'answer'

    id = sa.Column(sa.Integer, sa.ForeignKey('universe.id'), primary_key=True)
    computed = sa.Column(sa.Integer, nullable=False)


DB_URI = 'postgresql://user:pass@localhost:5432/db_name'  # Fill in your own
engine = sa.create_engine(DB_URI)
Session = sessionmaker(bind=engine, expire_on_commit=True)


class UniverseTests(TestCase):
    def setUp(self) -> None:
        session = Session()
        session.execute(create_sql)
        session.execute("INSERT INTO universe (id, name) VALUES (1, 'HG2G');")
        session.commit()
        session.close()

    def tearDown(self) -> None:
        session = Session()
        session.execute("DROP MATERIALIZED VIEW answer")
        session.execute("DROP TABLE universe")
        session.commit()
        session.close()

    def test__is_perfect(self):
        session = Session()
        universe: Universe = session.query(Universe).get(1)

        universe.answer = Answer(id=1, computed=42)
        universe.set_perfect()
        assert universe.is_perfect is True


if __name__ == '__main__':
    unittest.main()
python postgresql sqlalchemy materialized-views
1个回答
0
投票

不是完全解决方案,但对于我的用例来说,一个好的解决方法是在提交之前调用session.expunge(related_object)

为了说明问题中的示例测试:

class UniverseTests(TestCase):
    def test__is_perfect(self):
        session = Session()
        universe: Universe = session.query(Universe).get(1)

        universe.answer = Answer(id=1, computed=42)
        session.expunge(universe.answer)

        universe.set_perfect()
        assert universe.is_perfect is True

这证实了我的想法:关系上的viewonly=True不会阻止sqlalchemy在将对象分配给关系时将其添加到会话中。

无论是设计还是错误,我都不知道...如果我根据对文档的理解以及(我希望是什么)常识,我希望将对象添加到会话中并保持在非脏状态,以便会话的提交不会触发相关对象的INSERT / UPDATE

所以我将其视为错误,但是我可能根本不理解此选项的目的。如果有人能启发我,我将不胜感激。

© www.soinside.com 2019 - 2024. All rights reserved.