Конфигурация ORM

Как отобразить таблицу, не имеющую первичного ключа?

SQLAlchemy ORM для сопоставления с определенной таблицей требует, чтобы в ней был хотя бы один столбец, обозначенный как столбец первичного ключа; разумеется, вполне возможны и многостолбцовые, т.е. составные, первичные ключи. Эти столбцы **не обязательно должны быть известны базе данных как столбцы первичного ключа, хотя желательно, чтобы они были известны. Необходимо только, чтобы столбцы вели себя так, как ведет себя первичный ключ, например, как уникальный и не обнуляемый идентификатор строки.

Большинство ORM требуют, чтобы объекты имели определенный первичный ключ, поскольку объект в памяти должен соответствовать однозначно идентифицируемой строке в таблице базы данных; по крайней мере, это позволяет направлять на объект запросы UPDATE и DELETE, которые будут затрагивать только строку этого объекта и никакие другие. Однако важность первичного ключа выходит далеко за рамки этой задачи. В SQLAlchemy все ORM-сопоставленные объекты всегда однозначно связаны в рамках Session с конкретной строкой базы данных с помощью паттерна identity map, который является центральным в системе единиц работы, используемой в SQLAlchemy, а также ключевым для наиболее распространенных (и не очень) моделей использования ORM.

Примечание

Важно отметить, что мы говорим только о SQLAlchemy ORM; приложение, построенное на базе Core и работающее только с объектами Table, конструкциями select() и т.п., не нуждается в наличии первичного ключа в таблице или каким-либо образом с ней связано (хотя, опять же, в SQL все таблицы действительно должны иметь первичный ключ, если вам не нужно обновлять или удалять конкретные строки).

Практически во всех случаях таблица действительно имеет так называемый candidate key, который представляет собой столбец или серию столбцов, однозначно идентифицирующих строку. Если таблица действительно не имеет такого ключа и имеет реальные полностью дублирующиеся строки, то такая таблица не соответствует first normal form и не может быть отображена. В противном случае все столбцы, составляющие наилучший ключ-кандидат, могут быть применены непосредственно к картографу:

class SomeClass(Base):
    __table__ = some_table_with_no_pk
    __mapper_args__ = {
        "primary_key": [some_table_with_no_pk.c.uid, some_table_with_no_pk.c.bar]
    }

Еще лучше при использовании полностью объявленных метаданных таблицы использовать флаг primary_key=True для этих столбцов:

class SomeClass(Base):
    __tablename__ = "some_table_with_no_pk"

    uid = Column(Integer, primary_key=True)
    bar = Column(String, primary_key=True)

Все таблицы в реляционной базе данных должны иметь первичные ключи. Даже в таблице ассоциаций «многие-ко-многим» первичным ключом будет композит из двух столбцов ассоциаций:

CREATE TABLE my_association (
  user_id INTEGER REFERENCES user(id),
  account_id INTEGER REFERENCES account(id),
  PRIMARY KEY (user_id, account_id)
)

Как настроить столбец, который является зарезервированным словом Python или подобным ему?

Атрибутам, основанным на столбцах, можно присвоить любое имя, желаемое в связке. См. Явное именование декларативных сопоставленных столбцов.

Как получить список всех столбцов, отношений, сопоставленных атрибутов и т.д. для сопоставленного класса?

Вся эта информация доступна из объекта Mapper.

Чтобы получить Mapper для конкретного сопоставленного класса, вызовите на нем функцию inspect():

from sqlalchemy import inspect

mapper = inspect(MyClass)

Отсюда вся информация о классе может быть доступна через такие свойства, как:

  • Mapper.attrs - пространство имен всех отображаемых атрибутов. Сами атрибуты являются экземплярами MapperProperty, которые содержат дополнительные атрибуты, которые могут привести к сопоставленному SQL-выражению или столбцу, если это применимо.

  • Mapper.column_attrs - пространство имен отображаемых атрибутов, ограниченное атрибутами столбцов и SQL-выражений. Для непосредственного доступа к объектам Column может потребоваться использование Mapper.columns.

  • Mapper.relationships - пространство имен всех атрибутов RelationshipProperty.

  • Mapper.all_orm_descriptors - пространство имен всех отображаемых атрибутов, а также пользовательских атрибутов, определяемых с помощью систем hybrid_property, AssociationProxy и др.

  • Mapper.columns - Пространство имен объектов Column и других именованных SQL-выражений, связанных с отображением.

  • Mapper.mapped_table - Выборка Table или другая выборка, на которую сопоставлен данный картоприемник.

  • Mapper.local_table - Table, который является «локальным» для данного отображения; он отличается от Mapper.mapped_table в случае отображения с использованием наследования на составленный selectable.

Я получаю предупреждение или ошибку «Неявное объединение столбца X под атрибутом Y».

Это условие относится к случаю, когда в отображении присутствуют два столбца, которые в силу своего названия отображаются под одним и тем же именем атрибута, но нет никаких признаков того, что это сделано намеренно. В отображаемом классе необходимо иметь явные имена для каждого атрибута, который должен хранить независимое значение; когда два столбца имеют одинаковые имена и не разграничены, они попадают под один и тот же атрибут, в результате чего значение из одного столбца копируется в другой, исходя из того, какой столбец был назначен атрибуту первым.

Такое поведение часто бывает желательным и разрешается без предупреждения в том случае, если два столбца связаны между собой отношением внешнего ключа в связке наследования. При возникновении предупреждения или исключения проблему можно решить либо назначением столбцов атрибутам с разными именами, либо, если требуется их объединение, использованием column_property() для явного указания на это.

Приведем следующий пример:

from sqlalchemy import Integer, Column, ForeignKey
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)


class B(A):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))

Начиная с версии SQLAlchemy 0.9.5, данное условие обнаруживается и выдается предупреждение о том, что колонки id объектов A и B объединяются под одноименным атрибутом id, что является серьезной проблемой, поскольку означает, что первичный ключ объекта B всегда будет зеркально отражать первичный ключ его A.

Для решения этой проблемы можно использовать следующее отображение:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)


class B(A):
    __tablename__ = "b"

    b_id = Column("id", Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))

Предположим, что мы хотим, чтобы A.id и B.id были зеркальным отражением друг друга, несмотря на то, что B.a_id находится там, где находится A.id. Мы могли бы объединить их с помощью column_property():

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)


class B(A):
    __tablename__ = "b"

    # probably not what you want, but this is a demonstration
    id = column_property(Column(Integer, primary_key=True), A.id)
    a_id = Column(Integer, ForeignKey("a.id"))

Я использую Declarative и задаю primaryjoin/secondaryjoin с помощью and_() или or_(), и получаю сообщение об ошибке, связанной с внешними ключами.

Вы это делаете?:

class MyClass(Base):
    # ....

    foo = relationship(
        "Dest", primaryjoin=and_("MyClass.id==Dest.foo_id", "MyClass.foo==Dest.bar")
    )

Это and_() из двух строковых выражений, к которым SQLAlchemy не может применить никакого отображения. Декларативный метод позволяет задавать аргументы relationship() в виде строк, которые преобразуются в объекты выражений с помощью eval(). Но это не происходит внутри выражения and_() - это специальная операция, которую Declarative применяет только к полноте того, что передается в primaryjoin или другие аргументы в виде строки:

class MyClass(Base):
    # ....

    foo = relationship(
        "Dest", primaryjoin="and_(MyClass.id==Dest.foo_id, MyClass.foo==Dest.bar)"
    )

Или, если нужные объекты уже имеются, пропустите строки:

class MyClass(Base):
    # ....

    foo = relationship(
        Dest, primaryjoin=and_(MyClass.id == Dest.foo_id, MyClass.foo == Dest.bar)
    )

Эта же идея применима и ко всем остальным аргументам, например, foreign_keys:

# wrong !
foo = relationship(Dest, foreign_keys=["Dest.foo_id", "Dest.bar_id"])

# correct !
foo = relationship(Dest, foreign_keys="[Dest.foo_id, Dest.bar_id]")

# also correct !
foo = relationship(Dest, foreign_keys=[Dest.foo_id, Dest.bar_id])


# if you're using columns from the class that you're inside of, just use the column objects !
class MyClass(Base):
    foo_id = Column(...)
    bar_id = Column(...)
    # ...

    foo = relationship(Dest, foreign_keys=[foo_id, bar_id])
Вернуться на верх