Конфигурация 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-выражений. Вы можете использовать Mapper.columns, чтобы получить доступ непосредственно к объектам Column.

  • 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])
Вернуться на верх