环境:
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)
最后是一个完整的代码示例(与上面的代码相同,但可以运行):
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()
不是完全解决方案,但对于我的用例来说,一个好的解决方法是在提交之前调用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
。
所以我将其视为错误,但是我可能根本不理解此选项的目的。如果有人能启发我,我将不胜感激。