Я столкнулся с этой проблемой некоторое время назад, когда у меня было простое представление, созданное путем объединения двух таблиц с отношением 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 г.