Дополнительные техники стойкости

Встраивание выражений вставки/обновления SQL в промывку

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

class SomeClass(Base):
    __tablename__ = "some_table"

    # ...

    value = mapped_column(Integer)


someobject = session.get(SomeClass, 5)

# set 'value' attribute to a SQL expression adding one
someobject.value = SomeClass.value + 1

# issues "UPDATE some_table SET value=value+1"
session.commit()

Эта техника работает как для операторов INSERT, так и для UPDATE. После операции flush/commit атрибут value на someobject выше истекает, так что при следующем обращении вновь созданное значение будет загружено из базы данных.

Функция также имеет поддержку условных значений для работы в сочетании с колонками первичного ключа. Для бэкендов, имеющих поддержку RETURNING (включая Oracle, SQL Server, MariaDB 10.5, SQLite 3.35), SQL-выражение может быть присвоено столбцу первичного ключа. Это позволяет оценивать как SQL-выражение, так и любые триггеры на стороне сервера, которые изменяют значение первичного ключа при INSERT, могут быть успешно извлечены ORM как часть первичного ключа объекта:

class Foo(Base):
    __tablename__ = "foo"
    pk = mapped_column(Integer, primary_key=True)
    bar = mapped_column(Integer)


e = create_engine("postgresql+psycopg2://scott:tiger@localhost/test", echo=True)
Base.metadata.create_all(e)

session = Session(e)

foo = Foo(pk=sql.select(sql.func.coalesce(sql.func.max(Foo.pk) + 1, 1)))
session.add(foo)
session.commit()

На PostgreSQL вышеупомянутый Session приведет к появлению следующего INSERT:

INSERT INTO foo (foopk, bar) VALUES
((SELECT coalesce(max(foo.foopk) + %(max_1)s, %(coalesce_2)s) AS coalesce_1
FROM foo), %(bar)s) RETURNING foo.foopk

Добавлено в версии 1.3: SQL-выражения теперь можно передавать столбцу первичного ключа во время ORM flush; если база данных поддерживает RETURNING или используется pysqlite, ORM сможет получить сгенерированное сервером значение в качестве значения атрибута первичного ключа.

Использование выражений SQL с сеансами

Выражения и строки SQL могут быть выполнены через Session в транзакционном контексте. Это проще всего сделать с помощью метода Session.execute(), который возвращает CursorResult таким же образом, как Engine или Connection:

Session = sessionmaker(bind=engine)
session = Session()

# execute a string statement
result = session.execute("select * from table where id=:id", {"id": 7})

# execute a SQL expression construct
result = session.execute(select(mytable).where(mytable.c.id == 7))

Текущий Connection, удерживаемый Session, доступен с помощью метода Session.connection():

connection = session.connection()

Приведенные выше примеры относятся к Session, который привязан к одному Engine или Connection. Для выполнения операторов с помощью Session, который привязан либо к нескольким движкам, либо вообще ни к одному (т.е. полагается на привязанные метаданные), и Session.execute(), и Session.connection() принимают словарь аргументов привязки Session.execute.bind_arguments, который может включать «mapper», которому передается сопоставленный класс или экземпляр Mapper, используемый для нахождения нужного контекста для требуемого движка:

Session = sessionmaker()
session = Session()

# need to specify mapper or class when executing
result = session.execute(
    text("select * from table where id=:id"),
    {"id": 7},
    bind_arguments={"mapper": MyMappedClass},
)

result = session.execute(
    select(mytable).where(mytable.c.id == 7), bind_arguments={"mapper": MyMappedClass}
)

connection = session.connection(MyMappedClass)

Изменено в версии 1.4: аргументы mapper и clause в Session.execute() теперь передаются как часть словаря, передаваемого в качестве параметра Session.execute.bind_arguments. Предыдущие аргументы по-прежнему принимаются, однако это использование устарело.

Принудительный NULL для столбца со значением по умолчанию

ORM рассматривает любой атрибут, который никогда не был установлен для объекта, как случай «по умолчанию»; атрибут будет опущен в операторе INSERT:

class MyObject(Base):
    __tablename__ = "my_table"
    id = mapped_column(Integer, primary_key=True)
    data = mapped_column(String(50), nullable=True)


obj = MyObject(id=1)
session.add(obj)
session.commit()  # INSERT with the 'data' column omitted; the database
# itself will persist this as the NULL value

Пропуск столбца в INSERT означает, что столбец будет иметь значение NULL, если столбец не имеет значения по умолчанию, в этом случае значение по умолчанию будет сохранено. Это справедливо как с точки зрения чистого SQL с настройками по умолчанию на стороне сервера, так и с точки зрения поведения вставки SQLAlchemy с настройками по умолчанию как на стороне клиента, так и на стороне сервера:

class MyObject(Base):
    __tablename__ = "my_table"
    id = mapped_column(Integer, primary_key=True)
    data = mapped_column(String(50), nullable=True, server_default="default")


obj = MyObject(id=1)
session.add(obj)
session.commit()  # INSERT with the 'data' column omitted; the database
# itself will persist this as the value 'default'

Однако в ORM, даже если явно присвоить объекту значение Python None, это будет рассматриваться так же, как если бы значение никогда не присваивалось:

class MyObject(Base):
    __tablename__ = "my_table"
    id = mapped_column(Integer, primary_key=True)
    data = mapped_column(String(50), nullable=True, server_default="default")


obj = MyObject(id=1, data=None)
session.add(obj)
session.commit()  # INSERT with the 'data' column explicitly set to None;
# the ORM still omits it from the statement and the
# database will still persist this as the value 'default'

Приведенная выше операция сохранит в столбце data значение по умолчанию сервера "default", а не SQL NULL, даже если было передано None; это давнее поведение ORM, которое многие приложения используют как предположение.

Что же делать, если мы хотим действительно поместить NULL в этот столбец, даже если столбец имеет значение по умолчанию? Есть два подхода. Первый заключается в том, что на уровне каждого экземпляра мы присваиваем атрибут с помощью SQL-конструкции null:

from sqlalchemy import null

obj = MyObject(id=1, data=null())
session.add(obj)
session.commit()  # INSERT with the 'data' column explicitly set as null();
# the ORM uses this directly, bypassing all client-
# and server-side defaults, and the database will
# persist this as the NULL value

SQL-конструкция null всегда переводит значение SQL NULL в непосредственное присутствие в целевом операторе INSERT.

Если мы хотим иметь возможность использовать значение Python None и чтобы оно также сохранялось как NULL, несмотря на наличие значений по умолчанию в колонках, мы можем настроить это для ORM с помощью модификатора уровня Core TypeEngine.evaluates_none(), который указывает тип, в котором ORM должен рассматривать значение None как любое другое значение и передавать его, а не опускать его как «отсутствующее» значение:

class MyObject(Base):
    __tablename__ = "my_table"
    id = mapped_column(Integer, primary_key=True)
    data = mapped_column(
        String(50).evaluates_none(),  # indicate that None should always be passed
        nullable=True,
        server_default="default",
    )


obj = MyObject(id=1, data=None)
session.add(obj)
session.commit()  # INSERT with the 'data' column explicitly set to None;
# the ORM uses this directly, bypassing all client-
# and server-side defaults, and the database will
# persist this as the NULL value

Получение генерируемых сервером значений по умолчанию

Как было представлено в разделах Выражения по умолчанию DDL-Explicit, вызываемые сервером и Маркировка неявно генерируемых значений, временных меток и триггерных столбцов, ядро поддерживает понятие столбцов базы данных, для которых база данных сама генерирует значение при INSERT и в менее распространенных случаях при UPDATE. ORM поддерживает такие столбцы в части возможности получения этих вновь сгенерированных значений при flush. Такое поведение необходимо в случае столбцов с первичным ключом, которые генерируются сервером, поскольку ORM должен знать первичный ключ объекта после его сохранения.

В подавляющем большинстве случаев столбцы первичного ключа, значение которых генерируется базой данных автоматически, представляют собой простые целочисленные столбцы, которые реализуются базой данных либо как так называемые «автоинкрементные» столбцы, либо из последовательности, связанной со столбцом. Каждый диалект базы данных в рамках SQLAlchemy Core поддерживает метод получения значений этих первичных ключей, который часто является родным для Python DBAPI, и в целом этот процесс происходит автоматически. Более подробная документация по этому вопросу находится по адресу Column.autoincrement.

Для генерируемых сервером столбцов, которые не являются столбцами первичного ключа или не являются простыми автоинкрементными целочисленными столбцами, ORM требует, чтобы эти столбцы были помечены соответствующей директивой server_default, которая позволяет ORM получить это значение. Однако не все методы поддерживаются на всех бэкендах, поэтому нужно быть внимательным, чтобы использовать соответствующий метод. Необходимо ответить на два вопроса: 1. является ли этот столбец частью первичного ключа или нет, и 2. поддерживает ли база данных RETURNING или эквивалент, такой как «OUTPUT inserted»; это фразы SQL, которые возвращают сгенерированное сервером значение одновременно с вызовом оператора INSERT или UPDATE. В настоящее время RETURNING поддерживается PostgreSQL, Oracle, MariaDB 10.5, SQLite 3.35 и SQL Server.

Случай 1: не первичный ключ, поддерживается RETURNING или эквивалент

В этом случае столбцы должны быть помечены как FetchedValue или с явным Column.server_default. ORM автоматически добавит эти столбцы в предложение RETURNING при выполнении операторов INSERT, если параметр Mapper.eager_defaults установлен в True, или если оставить его по умолчанию в "auto", для диалектов, поддерживающих как RETURNING, так и insertmanyvalues:

class MyModel(Base):
    __tablename__ = "my_table"

    id = mapped_column(Integer, primary_key=True)

    # server-side SQL date function generates a new timestamp
    timestamp = mapped_column(DateTime(), server_default=func.now())

    # some other server-side function not named here, such as a trigger,
    # populates a value into this column during INSERT
    special_identifier = mapped_column(String(50), server_default=FetchedValue())

    # set eager defaults to True.  This is usually optional, as if the
    # backend supports RETURNING + insertmanyvalues, eager defaults
    # will take place regardless on INSERT
    __mapper_args__ = {"eager_defaults": True}

Выше, оператор INSERT, который не указывает явных значений для «timestamp» или «special_identifier» на стороне клиента, будет включать столбцы «timestamp» и «special_identifier» в предложение RETURNING, чтобы они были доступны немедленно. В базе данных PostgreSQL INSERT для приведенной выше таблицы будет выглядеть следующим образом:

INSERT INTO my_table DEFAULT VALUES RETURNING my_table.id, my_table.timestamp, my_table.special_identifier

Изменено в версии 2.0.0rc1: Параметр Mapper.eager_defaults теперь по умолчанию имеет новую настройку "auto", которая автоматически использует RETURNING для получения генерируемых сервером значений по умолчанию при INSERT, если резервная база данных поддерживает как RETURNING, так и insertmanyvalues.

Примечание

Значение "auto" для Mapper.eager_defaults применяется только к операторам INSERT. Операторы UPDATE не будут использовать RETURNING, даже если он доступен, если для Mapper.eager_defaults не установлено значение True. Это связано с тем, что для UPDATE не существует эквивалентной функции «insertmanyvalues», поэтому UPDATE RETURNING потребует, чтобы операторы UPDATE выдавались отдельно для каждого ряда, который UPDATEd.

Случай 2: Таблица включает генерируемые триггером значения, которые не совместимы с RETURNING

Установка "auto" Mapper.eager_defaults означает, что бэкенд, поддерживающий RETURNING, обычно использует RETURNING с операторами INSERT для получения вновь сгенерированных значений по умолчанию. Однако существуют ограничения на генерируемые сервером значения, которые генерируются с помощью триггеров, такие, что RETURNING не может быть использован:

  • SQL Server не позволяет использовать RETURNING в операторе INSERT для получения значения, сгенерированного триггером; оператор завершится неудачей.

  • SQLite имеет ограничения в сочетании использования RETURNING с триггерами, например, в предложении RETURNING не будет доступно значение INSERTed

  • Другие бэкенды могут иметь ограничения на использование RETURNING в сочетании с триггерами или другими видами генерируемых сервером значений.

Чтобы запретить использование RETURNING для таких значений, включая не только генерируемые сервером значения по умолчанию, но и гарантировать, что ORM никогда не будет использовать RETURNING с определенной таблицей, укажите Table.implicit_returning как False для сопоставленного Table. При использовании декларативного отображения это выглядит следующим образом:

class MyModel(Base):
    __tablename__ = "my_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    data: Mapped[str] = mapped_column(String(50))

    # assume a database trigger populates a value into this column
    # during INSERT
    special_identifier = mapped_column(String(50), server_default=FetchedValue())

    # disable all use of RETURNING for the table
    __table_args__ = {"implicit_returning": False}

На SQL Server с драйвером pyodbc, INSERT для вышеуказанной таблицы не будет использовать RETURNING и будет использовать функцию SQL Server scope_identity() для получения нового значения первичного ключа:

INSERT INTO my_table (data) VALUES (?); select scope_identity()

См.также

Поведение INSERT - справочная информация о методах диалекта SQL Server для получения новых значений первичного ключа

Случай 3: не первичный ключ, RETURNING или эквивалент не поддерживается или не нужен

Этот случай аналогичен случаю 1 выше, за исключением того, что мы обычно не хотим использовать Mapper.eager_defaults, поскольку его текущая реализация при отсутствии поддержки RETURNING заключается в выдаче SELECT-per-row, что не является эффективным. Поэтому параметр опущен в приведенном ниже отображении:

class MyModel(Base):
    __tablename__ = "my_table"

    id = mapped_column(Integer, primary_key=True)
    timestamp = mapped_column(DateTime(), server_default=func.now())

    # assume a database trigger populates a value into this column
    # during INSERT
    special_identifier = mapped_column(String(50), server_default=FetchedValue())

После INSERT записи с приведенным выше отображением на бэкенде, который не включает поддержку RETURNING или «insertmanyvalues», столбцы «timestamp» и «special_identifier» останутся пустыми и будут получены с помощью второго оператора SELECT при первом обращении к ним после flush, например, они будут помечены как «expired».

Если для Mapper.eager_defaults явно указано значение True, а внутренняя база данных не поддерживает RETURNING или его эквивалент, ORM выпустит оператор SELECT сразу после оператора INSERT, чтобы получить вновь созданные значения; в настоящее время ORM не имеет возможности пакетного SELECT многих вновь вставленных строк, если RETURNING недоступен. Обычно это нежелательно, поскольку добавляет в процесс flush дополнительные операторы SELECT, которые могут быть не нужны. Использование приведенного выше отображения с флагом Mapper.eager_defaults, установленным в True, в MySQL (не MariaDB) приводит к такому SQL при flush:

INSERT INTO my_table () VALUES ()

-- when eager_defaults **is** used, but RETURNING is not supported
SELECT my_table.timestamp AS my_table_timestamp, my_table.special_identifier AS my_table_special_identifier
FROM my_table WHERE my_table.id = %s

В будущем выпуске SQLAlchemy может быть предпринята попытка повысить эффективность eager defaults в отказе от RETURNING для пакетной обработки многих строк в рамках одного оператора SELECT.

Случай 4: поддерживается первичный ключ, RETURNING или эквивалент

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

Как упоминалось выше, для целочисленных «автоинкрементных» столбцов, а также столбцов, помеченных Identity и специальных конструкций, таких как PostgreSQL SERIAL, эти типы обрабатываются Core автоматически; базы данных включают функции для получения «последнего вставленного идентификатора», где RETURNING не поддерживается, а где RETURNING поддерживается, SQLAlchemy будет использовать его.

Например, при использовании Oracle со столбцом, помеченным как Identity, RETURNING используется автоматически для получения нового значения первичного ключа:

class MyOracleModel(Base):
    __tablename__ = "my_table"

    id: Mapped[int] = mapped_column(Identity(), primary_key=True)
    data: Mapped[str] = mapped_column(String(50))

INSERT для модели, как указано выше, в Oracle выглядит следующим образом:

INSERT INTO my_table (data) VALUES (:data) RETURNING my_table.id INTO :ret_0

SQLAlchemy создает INSERT для поля «data», но включает только «id» в предложение RETURNING, так что на стороне сервера произойдет генерация для «id» и новое значение будет возвращено немедленно.

Для нецелых значений, генерируемых функциями или триггерами на стороне сервера, а также для целых значений, которые поступают из конструкций вне самой таблицы, включая явные последовательности и триггеры, в метаданных таблицы должна быть отмечена генерация по умолчанию сервера. Снова используя Oracle в качестве примера, мы можем проиллюстрировать аналогичную таблицу, как описано выше, именуя явную последовательность с помощью конструкции Sequence:

class MyOracleModel(Base):
    __tablename__ = "my_table"

    id: Mapped[int] = mapped_column(Sequence("my_oracle_seq"), primary_key=True)
    data: Mapped[str] = mapped_column(String(50))

INSERT для этой версии модели в Oracle будет выглядеть следующим образом:

INSERT INTO my_table (id, data) VALUES (my_oracle_seq.nextval, :data) RETURNING my_table.id INTO :ret_0

Там, где указано выше, SQLAlchemy отображает my_sequence.nextval для столбца первичного ключа, чтобы он использовался для генерации нового первичного ключа, а также использует RETURNING для немедленного получения нового значения.

Если источник данных не представлен простой функцией SQL или Sequence, например, при использовании триггеров или специфических для базы данных типов данных, которые генерируют новые значения, наличие значения, генерирующего значение по умолчанию, может быть указано с помощью FetchedValue в определении столбца. Ниже приведена модель, использующая столбец SQL Server TIMESTAMP в качестве первичного ключа; на SQL Server этот тип данных генерирует новые значения автоматически, поэтому это указывается в метаданных таблицы путем указания FetchedValue для параметра Column.server_default:

class MySQLServerModel(Base):
    __tablename__ = "my_table"

    timestamp: Mapped[datetime.datetime] = mapped_column(
        TIMESTAMP(), server_default=FetchedValue(), primary_key=True
    )
    data: Mapped[str] = mapped_column(String(50))

INSERT для вышеуказанной таблицы на SQL Server выглядит следующим образом:

INSERT INTO my_table (data) OUTPUT inserted.timestamp VALUES (?)

Случай 5: первичный ключ, RETURNING или эквивалент не поддерживается

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

Примечание

В этом разделе мы проиллюстрируем несколько рецептов, связанных со значениями datetime для MySQL, поскольку типы данных datetime в этом бэкенде имеют дополнительные идиосинкразические требования, которые полезно проиллюстрировать. Следует помнить, что MySQL требует явного «предварительно выполненного» генератора по умолчанию для любого автоматически создаваемого типа данных, используемого в качестве первичного ключа, кроме обычного одностолбцового автоинкрементного целочисленного значения.

MySQL с первичным ключом DateTime

На примере столбца DateTime для MySQL мы добавляем явное значение по умолчанию с поддержкой предварительного выполнения, используя SQL-функцию «NOW()»:

class MyModel(Base):
    __tablename__ = "my_table"

    timestamp = mapped_column(DateTime(), default=func.now(), primary_key=True)

Где выше, мы выбираем функцию «NOW()», чтобы передать в столбец значение времени. SQL, сгенерированный выше, выглядит следующим образом:

SELECT now() AS anon_1
INSERT INTO my_table (timestamp) VALUES (%s)
('2018-08-09 13:08:46',)

MySQL с первичным ключом TIMESTAMP

При использовании типа данных TIMESTAMP в MySQL, MySQL обычно автоматически ассоциирует значение по умолчанию на стороне сервера с этим типом данных. Однако, когда мы используем его в качестве первичного ключа, ядро не сможет получить вновь созданное значение, если мы сами не выполним функцию. Поскольку TIMESTAMP в MySQL на самом деле хранит двоичное значение, нам нужно добавить дополнительный «CAST» к нашему использованию «NOW()», чтобы мы получили двоичное значение, которое может быть сохранено в столбце:

from sqlalchemy import cast, Binary


class MyModel(Base):
    __tablename__ = "my_table"

    timestamp = mapped_column(
        TIMESTAMP(), default=cast(func.now(), Binary), primary_key=True
    )

Выше, помимо выбора функции «NOW()», мы дополнительно используем тип данных Binary в сочетании с cast(), чтобы возвращаемое значение было двоичным. SQL, созданный на основе вышеприведенного в рамках INSERT, выглядит следующим образом:

SELECT CAST(now() AS BINARY) AS anon_1
INSERT INTO my_table (timestamp) VALUES (%s)
(b'2018-08-09 13:08:46',)

Примечания о нетерпеливой выборке вызываемых клиентом выражений SQL, используемых для INSERT или UPDATE

Предыдущие примеры показывают использование Column.server_default для создания таблиц, которые включают функции генерации по умолчанию в DDL.

SQLAlchemy также поддерживает не-DDL значения по умолчанию на стороне сервера, как документировано в Выражения SQL, вызываемые клиентом; эти «вызываемые клиентом SQL выражения» устанавливаются с помощью параметров Column.default и Column.onupdate.

В настоящее время эти SQL-выражения подвержены тем же ограничениям в ORM, что и настоящие умолчания на стороне сервера; они не будут с нетерпением подбираться с RETURNING, когда Mapper.eager_defaults установлен в "auto" или True, если директива FetchedValue не связана с Column, несмотря на то, что эти выражения не являются умолчаниями сервера DDL и активно обрабатываются самой SQLAlchemy. Это ограничение может быть устранено в будущих выпусках SQLAlchemy.

Конструкция FetchedValue может быть применена к Column.server_default или Column.server_onupdate одновременно с использованием SQL-выражения Column.default и Column.onupdate, как в примере ниже, где конструкция func.now() используется в качестве вызываемого клиентом SQL-выражения для Column.default и Column.onupdate. Для того чтобы поведение Mapper.eager_defaults включало извлечение этих значений с использованием RETURNING, когда они доступны, Column.server_default и Column.server_onupdate используются вместе с FetchedValue для обеспечения того, что извлечение происходит:

class MyModel(Base):
    __tablename__ = "my_table"

    id = mapped_column(Integer, primary_key=True)

    created = mapped_column(
        DateTime(), default=func.now(), server_default=FetchedValue()
    )
    updated = mapped_column(
        DateTime(),
        onupdate=func.now(),
        server_default=FetchedValue(),
        server_onupdate=FetchedValue(),
    )

    __mapper_args__ = {"eager_defaults": True}

При отображении, подобном приведенному выше, SQL, созданный ORM для INSERT и UPDATE, будет включать created и updated в предложение RETURNING:

INSERT INTO my_table (created) VALUES (now()) RETURNING my_table.id, my_table.created, my_table.updated

UPDATE my_table SET updated=now() WHERE my_table.id = %(my_table_id)s RETURNING my_table.updated

Использование INSERT, UPDATE и ON CONFLICT (т.е. upsert) для возврата объектов ORM

SQLAlchemy 2.0 включает расширенные возможности для создания нескольких разновидностей операторов INSERT, UPDATE и upsert с поддержкой ORM. Документацию см. в документе по адресу Операции INSERT, UPDATE и DELETE с поддержкой ORM. Об апсерте смотрите ORM «upsert» Statements.

Использование PostgreSQL ON CONFLICT с RETURNING для возврата upserted ORM объектов

Этот раздел переместился в ORM «upsert» Statements.

Стратегии разделения (например, несколько бэкэндов баз данных на сессию)

Простая вертикальная перегородка

Вертикальное разделение размещает различные классы, иерархии классов или сопоставленные таблицы в нескольких базах данных, конфигурируя Session с аргументом Session.binds. Этот аргумент получает словарь, содержащий любую комбинацию ORM-сопоставленных классов, произвольных классов в сопоставленной иерархии (таких как декларативные базовые классы или миксины), Table объектов и Mapper объектов в качестве ключей, которые затем ссылаются обычно на Engine или менее типично Connection объекты в качестве целей. К словарю обращаются всякий раз, когда Session нужно выдать SQL от имени определенного вида сопоставленного класса, чтобы найти соответствующий источник подключения к базе данных:

engine1 = create_engine("postgresql+psycopg2://db1")
engine2 = create_engine("postgresql+psycopg2://db2")

Session = sessionmaker()

# bind User operations to engine 1, Account operations to engine 2
Session.configure(binds={User: engine1, Account: engine2})

session = Session()

Выше, SQL-операции против любого класса будут использовать Engine, связанный с этим классом. Функциональность является всеобъемлющей для операций чтения и записи; операция Query, направленная на объекты, сопоставленные с engine1 (определяется путем просмотра первого объекта в списке запрашиваемых элементов), будет использовать engine1 для выполнения запроса. Операция flush будет использовать обои движки на основе каждого класса, поскольку она очищает объекты типа User и Account.

В более распространенном случае обычно существуют базовые классы или классы-миксины, которые можно использовать для различения операций, предназначенных для разных соединений с базой данных. Аргумент Session.binds может содержать любой произвольный класс Python в качестве ключа, который будет использоваться, если окажется, что он находится в __mro__ (порядок разрешения методов Python) для определенного сопоставленного класса. Предположим, что две декларативные базы представляют два разных подключения к базе данных:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Session


class BaseA(DeclarativeBase):
    pass


class BaseB(DeclarativeBase):
    pass


class User(BaseA):
    ...


class Address(BaseA):
    ...


class GameInfo(BaseB):
    ...


class GameStats(BaseB):
    ...


Session = sessionmaker()

# all User/Address operations will be on engine 1, all
# Game operations will be on engine 2
Session.configure(binds={BaseA: engine1, BaseB: engine2})

Выше, классы, которые происходят от BaseA и BaseB, будут направлять свои SQL-операции на один из двух движков в зависимости от того, от какого суперкласса они происходят, если таковой имеется. В случае класса, который происходит от более чем одного «связанного» суперкласса, суперкласс, который является самым высоким в иерархии целевого класса, будет выбран для представления того, какой движок должен быть использован.

См.также

Session.binds

Координация транзакций для многомоторной сессии

Одним из предостережений при использовании нескольких связанных движков является случай, когда операция фиксации может завершиться неудачей на одном бэкенде после того, как фиксация прошла успешно на другом. Это проблема несогласованности, которая в реляционных базах данных решается с помощью «двухфазной транзакции», которая добавляет дополнительный шаг «подготовки» к последовательности фиксации, что позволяет нескольким базам данных договориться о фиксации до фактического завершения транзакции.

Из-за ограниченной поддержки в DBAPIs, SQLAlchemy имеет ограниченную поддержку двухфазных транзакций между бэкендами. Как правило, она хорошо работает с бэкендом PostgreSQL и в меньшей степени с бэкендом MySQL. Тем не менее, Session полностью способен использовать преимущества двухфазной транзакции, если бэкенд поддерживает ее, путем установки флага Session.use_twophase в sessionmaker или Session. Пример смотрите в Включение двухфазной фиксации.

Нестандартные вертикальные перегородки

Более полное разделение на уровне классов на основе правил можно построить, переопределив метод Session.get_bind(). Ниже мы иллюстрируем пользовательский Session, который обеспечивает следующие правила:

  1. Операции промывки, а также массовые операции «обновления» и «удаления» доставляются на движок с именем leader.

  2. Все операции над объектами, подкласса MyOtherClass, происходят на движке other.

  3. Операции чтения для всех остальных классов происходят на случайном выборе базы данных follower1 или follower2.

engines = {
    "leader": create_engine("sqlite:///leader.db"),
    "other": create_engine("sqlite:///other.db"),
    "follower1": create_engine("sqlite:///follower1.db"),
    "follower2": create_engine("sqlite:///follower2.db"),
}

from sqlalchemy.sql import Update, Delete
from sqlalchemy.orm import Session, sessionmaker
import random


class RoutingSession(Session):
    def get_bind(self, mapper=None, clause=None):
        if mapper and issubclass(mapper.class_, MyOtherClass):
            return engines["other"]
        elif self._flushing or isinstance(clause, (Update, Delete)):
            return engines["leader"]
        else:
            return engines[random.choice(["follower1", "follower2"])]

Приведенный выше класс Session подключается с помощью аргумента class_ к sessionmaker:

Session = sessionmaker(class_=RoutingSession)

Этот подход можно объединить с несколькими объектами MetaData, используя подход, подобный тому, что используется декларативное ключевое слово __abstract__, описанное в __abstract__.

См.также

Django-style Database Routers in SQLAlchemy - запись в блоге о более полном примере Session.get_bind()

Горизонтальные перегородки

Горизонтальное разделение разделяет строки одной таблицы (или набора таблиц) по нескольким базам данных. SQLAlchemy Session содержит поддержку этой концепции, однако для ее полноценного использования требуется использование подклассов Session и Query. Базовая версия этих подклассов доступна в расширении Горизонтальное разделение ORM. Пример использования приведен на: Горизонтальное разделение.

Операции с сыпучими материалами

Legacy Feature

SQLAlchemy 2.0 интегрировал возможности Session «bulk insert» и «bulk update» в метод Session.execute() стиля 2.0, напрямую используя конструкции Insert и Update. Документацию см. в документе по адресу Операции INSERT, UPDATE и DELETE с поддержкой ORM, включая Legacy Session Bulk INSERT Methods, который иллюстрирует переход от старых методов к новым.

Вернуться на верх