Среда :
- python 3.7
- SqlAlchemy 1.3.10 (я также тестировал на 1.2.16, тот же результат)
- PostgreSQL 11
TL, DR: У меня есть таблица и материализованное представление "1-1" (нет fk, отношение неявно на стороне sql). В SQLAlchemy это отношение объявляется как viewonly=True
(на всякий случай тоже backref
). Но если я назначу ему, сеанс попытается вставить назначенный объект просмотра матов в любом случае (что, очевидно, не удается, потому что это материализованное представление).
Я неправильно понимаю цель 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, просмотр матов выполняет гораздо больше работы и извлекает данные из многих таблиц для вычисления результатов. Но для целей этой иллюстрации это подойдет.
Теперь модели:
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 3.7 venv с SQLalchemy 1.3.10
- создать новый(пусто) база данных postgresql
- сохранить этот пример кода в файле и изменить значение DB_URI на значение, которое вы только что создали
- выполнить файл в env (
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()