如何防止sqlalchemy中的相关对象持久化?
How to prevent related object persistence in sqlalchemy?
环境:
- python 3.7
- SqlAlchemy 1.3.10(我也在1.2.16上测试过,结果一样)
- PostgreSQL 11
TL,DR: 我有一个 table 和一个 1-1“相关”实体化视图(没有 fk ,关系隐含在 sql 端)。在 SQLAlchemy 中,这种关系被声明为 viewonly=True
。但是如果我分配给它,会话无论如何都会尝试插入分配的 mat 视图对象(这失败了,因为它显然是一个物化视图)。
是我误解了viewonly
的目的还是关系没有设置好?
完整的可重现测试用例:
- 使用 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),
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)
session = Session()
class UniverseTests(TestCase):
def setUp(self) -> None:
session.execute("DROP MATERIALIZED VIEW IF EXISTS answer")
session.execute("DROP TABLE IF EXISTS universe")
session.execute(create_sql)
session.execute("INSERT INTO universe (id, name) VALUES (1, 'HG2G');")
session.commit()
def tearDown(self) -> None:
session.execute("DROP MATERIALIZED VIEW answer")
session.execute("DROP TABLE universe")
session.commit()
session.close()
def test__is_perfect(self):
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()
正如你所看到的,在测试过程中,我将一个伪造的Answer(id=1, computed=42)
分配给universe.answer
来测试方法.set_perfect()
。
测试在 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(我在本示例中使用的版本)及以下版本,解决方法是将关系 cascade
设置为 None
或非与持久性相关的级联选项("expunge"
、"refresh-expire"
或两者)。 False
(默认值)不起作用,因为它意味着 "save-update, merge"
.
总结一下:
answer: Answer = relationship(
'Answer',
...,
viewonly=True,
cascade=None # or "expunge" or "refresh-expire" or "expunge,refresh-expire"
)
这确实是 1.3 及以下版本的错误,已在 1.3.11 和 1.4.22 之间的某处更正。我所知道的是原始示例没有指定 cascade
,在 1.4.22.
中就像一个魅力
相关错误报告:
以及相应的更新日志:
环境:
- python 3.7
- SqlAlchemy 1.3.10(我也在1.2.16上测试过,结果一样)
- PostgreSQL 11
TL,DR: 我有一个 table 和一个 1-1“相关”实体化视图(没有 fk ,关系隐含在 sql 端)。在 SQLAlchemy 中,这种关系被声明为 viewonly=True
。但是如果我分配给它,会话无论如何都会尝试插入分配的 mat 视图对象(这失败了,因为它显然是一个物化视图)。
是我误解了viewonly
的目的还是关系没有设置好?
完整的可重现测试用例:
- 使用 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),
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)
session = Session()
class UniverseTests(TestCase):
def setUp(self) -> None:
session.execute("DROP MATERIALIZED VIEW IF EXISTS answer")
session.execute("DROP TABLE IF EXISTS universe")
session.execute(create_sql)
session.execute("INSERT INTO universe (id, name) VALUES (1, 'HG2G');")
session.commit()
def tearDown(self) -> None:
session.execute("DROP MATERIALIZED VIEW answer")
session.execute("DROP TABLE universe")
session.commit()
session.close()
def test__is_perfect(self):
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()
正如你所看到的,在测试过程中,我将一个伪造的Answer(id=1, computed=42)
分配给universe.answer
来测试方法.set_perfect()
。
测试在 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(我在本示例中使用的版本)及以下版本,解决方法是将关系 cascade
设置为 None
或非与持久性相关的级联选项("expunge"
、"refresh-expire"
或两者)。 False
(默认值)不起作用,因为它意味着 "save-update, merge"
.
总结一下:
answer: Answer = relationship(
'Answer',
...,
viewonly=True,
cascade=None # or "expunge" or "refresh-expire" or "expunge,refresh-expire"
)
这确实是 1.3 及以下版本的错误,已在 1.3.11 和 1.4.22 之间的某处更正。我所知道的是原始示例没有指定 cascade
,在 1.4.22.
相关错误报告:
以及相应的更新日志: