Я столкнулся с этой проблемой некоторое время назад, когда у меня было простое представление, созданное путем объединения двух таблиц с отношением 1: 1 в PostgreSQL, но SQLAlchemy не понравилось мое представление.
Задний план
Основная проблема возникает из-за того, что SQLAlchemy является ORM (преобразователем объектных отношений), который достаточно хорошо подходит для большинства баз данных для наиболее распространенных случаев использования. Однако иногда это не подходит и их модели. Один из таких случаев - представления и, в частности, представления в PostgreSQL.
Почему? спросите вы. Что ж, вот в чем дело, насколько мне известно, на данный момент нет возможности изначально отразить представление в SQLAlchemy. Вместо этого для инициирования представления необходимо использовать конструкцию Table (), и в этом заключается основная проблема: см. Таблицу в SQLAlchemy, требующую первичного ключа для использования в качестве хэша, в то время как представления в PostgreSQL не могут. Ясно, что это конфликт.
Модель проблемной предметной области - пример кода прилагается
Я слишком упростил модель и представление, чтобы сосредоточить внимание на реальной проблеме, а не на несвязанном коде. В моем случае у нас есть базовая модель для Fields
, за которой следует пара связанных моделей для специализированных экземпляров этих полей. Один из них Template_Field
, показанный ниже, является простым расширением общего типа поля и никогда не доступен напрямую, а скорее через представление с именем Template_Field_View
.
Field.psql
CREATE TABLE IF NOT EXISTS quick_bits.Field (
id serial PRIMARY KEY,
order_Index INTEGER,
label TEXT,
key VARCHAR(255)
);
Template_Field.psql
Шаблоны сами по себе не представляют интереса для этого препятствия и поэтому не учитываются, однако я оставил отношение в Template_Field для иллюстративных целей.
CREATE TABLE IF NOT EXISTS quick_bits.Template_Field(
template integer REFERENCES quick_bits.Template(id),
field integer PRIMARY KEY REFERENCES quick_bits.Field(id),
);
Template_Field_View.psql
CREATE OR REPLACE VIEW quick_bits.template_field_view AS
SELECT
field.id,
template_field.template,
field.label,
field.key,
field.order_index,
FROM quick_bits.template_field as template_field
INNER JOIN quick_bits.field as field
ON field.id = template_field.field
;
Проблемы начинают всплывать
Самым наивным подходом для меня было просто попытаться сразу отразить представление в виде таблицы с SQLAlchemy в моем коде на Python.
# ...
def init_table(name):
return Table(name, meta, autoload=True, schema=config.DB_SCHEMA)
class Field(Base):
__table__ = init_table('field')
class TemplateField(Base):
__table__ = init_table('template_field')
class TemplateFieldView(Base):
__table__ = init_table('template_field_view')
Теперь давайте посмотрим, что произойдет, если мы попытаемся прочитать данные с помощью этой модели.
class TemplateFieldView(Base):
File "/Users/hultner/Development/quick_bit_test/venv/lib/python3.6/site-packages/sqlalchemy/ext/declarative/api.py", line 64, in __init__
_as_declarative(cls, classname, cls.__dict__)
File "/Users/hultner/Development/quick_bit_test/venv/lib/python3.6/site-packages/sqlalchemy/ext/declarative/base.py", line 88, in _as_declarative
_MapperConfig.setup_mapping(cls, classname, dict_)
File "/Users/hultner/Development/quick_bit_test/venv/lib/python3.6/site-packages/sqlalchemy/ext/declarative/base.py", line 103, in setup_mapping
cfg_cls(cls_, classname, dict_)
File "/Users/hultner/Development/quick_bit_test/venv/lib/python3.6/site-packages/sqlalchemy/ext/declarative/base.py", line 135, in __init__
self._early_mapping()
File "/Users/hultner/Development/quick_bit_test/venv/lib/python3.6/site-packages/sqlalchemy/ext/declarative/base.py", line 138, in _early_mapping
self.map()
File "/Users/hultner/Development/quick_bit_test/venv/lib/python3.6/site-packages/sqlalchemy/ext/declarative/base.py", line 534, in map
**self.mapper_args
File "<string>", line 2, in mapper
File "/Users/hultner/Development/quick_bit_test/venv/lib/python3.6/site-packages/sqlalchemy/orm/mapper.py", line 677, in __init__
self._configure_pks()
File "/Users/hultner/Development/quick_bit_test/venv/lib/python3.6/site-packages/sqlalchemy/orm/mapper.py", line 1277, in _configure_pks
(self, self.mapped_table.description))
sqlalchemy.exc.ArgumentError: Mapper Mapper|TemplateFieldView|template_field_view could not assemble any primary key columns for mapped table 'template_field_view'
Следующая ошибка кажется пугающей, но мы быстро видим, что проблема заключается в отображении представления без первичных ключей в качестве таблицы, для которой требуется указанный ключ.
Первая попытка обходного пути
Поскольку у нас уже есть уникальный идентификатор поля, мы можем попытаться заставить SQLAlchemy думать, что это первичный ключ.
Мой первый подход был примерно таким.
class TemplateFieldView(Base):
__table__ = init_table('template_field_view')
id = Column(Integer, primary_key=True)
Давай попробуем.
File "/Users/hultner/Development/quick_bit_test/venv/lib/python3.6/site-packages/sqlalchemy/ext/declarative/base.py", line 131, in __init__
self._setup_table()
File "/Users/hultner/Development/quick_bit_test/venv/lib/python3.6/site-packages/sqlalchemy/ext/declarative/base.py", line 403, in _setup_table
"specifying __table__" % c.key
sqlalchemy.exc.ArgumentError: Can't add additional column 'id' when specifying __table__
Некоторый прогресс, но без сигары. Теперь мы видим, что проблема больше не в первичном ключе, а в том, что мы не можем указывать новые столбцы, подобные этому, в отраженных таблицах. Это связано с тем, что внутренние механизмы SQLAlchemy уже определились со структурой таблицы и не хотят изменять это представление.
Решение
Итак, как нам обойти эту дилемму, после некоторого поиска я нашел свойство __mapper_args__
. Мы можем использовать это, чтобы изменить поведение столбцов в таблице / представлении. Подробнее читайте в SQLAlchemy Docs.
Итак, теперь у меня есть следующий код.
class TemplateFieldView(Base):
__table__ = init_table('template_field_view')
__mapper_args__ = {
'primary_key':[__table__.c.id]
}
Так что давай попробуем.
>>> get_template_field_from_view(54)
Field(id='54', template='3', order_index='1', label='Name', key='name_data', type='FIRST_NAME')>
Успех!
Вот и все. Если у кого-то есть более приятное решение, я хотел бы его услышать, а пока я надеюсь, что это будет полезно для вас!
Первоначально опубликовано на сайте hultner.github.io 23 октября 2017 г.