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

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

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

class SomeClass(Base):
    __tablename__ = "some_table"

    # ...

    value = Column(Integer)


someobject = session.query(SomeClass).get(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, например, PostgreSQL, Oracle или SQL Server, или, как особый случай при использовании SQLite с драйвером pysqlite и единственным автоинкрементным столбцом, выражение SQL может быть присвоено столбцу первичного ключа. Это позволяет оценивать как SQL-выражение, так и любые триггеры на стороне сервера, которые изменяют значение первичного ключа при INSERT, могут быть успешно получены ORM как часть первичного ключа объекта:

class Foo(Base):
    __tablename__ = 'foo'
    pk = Column(Integer, primary_key=True)
    bar = Column(Integer)

e = create_engine("postgresql://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 = Column(Integer, primary_key=True)
    data = 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 = Column(Integer, primary_key=True)
    data = 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 = Column(Integer, primary_key=True)
    data = 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 = Column(Integer, primary_key=True)
    data = 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

Добавлено в версии 1.1: добавлен метод TypeEngine.evaluates_none() для указания того, что значение «None» должно рассматриваться как значимое.

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

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

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

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

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

В этом случае столбцы должны быть помечены как FetchedValue или с явным Column.server_default. Параметр mapper.eager_defaults может быть использован для указания того, что эти столбцы должны быть извлечены немедленно при INSERT и иногда UPDATE:

class MyModel(Base):
    __tablename__ = "my_table"

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

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

    __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: не первичный ключ, RETURNING или эквивалент не поддерживается или не нужен

Этот случай аналогичен случаю 1, за исключением того, что мы не указываем mapper.eager_defaults:

class MyModel(Base):
    __tablename__ = "my_table"

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

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

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

Если mapper.eager_defaults все еще используется, а внутренняя база данных не поддерживает RETURNING или его эквивалент, ORM будет выдавать этот оператор SELECT сразу после оператора INSERT. Это часто нежелательно, поскольку добавляет в процесс flush дополнительные операторы SELECT, которые могут быть не нужны. Использование приведенного выше отображения с флагом mapper.eager_defaults, установленным в True для MySQL, приводит к такому 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

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

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

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

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

Для явной последовательности, как мы используем в Oracle, это означает, что мы используем конструкцию Sequence:

class MyOracleModel(Base):
    __tablename__ = "my_table"

    id = Column(Integer, Sequence("my_sequence"), primary_key=True)
    data = Column(String(50))

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

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

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

Для типов данных, которые генерируют значения автоматически, или столбцов, которые заполняются триггером, мы используем FetchedValue. Ниже приведена модель, в которой в качестве первичного ключа используется столбец SQL Server TIMESTAMP, который генерирует значения автоматически:

class MyModel(Base):
    __tablename__ = "my_table"

    timestamp = Column(TIMESTAMP(), server_default=FetchedValue(), primary_key=True)

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

INSERT INTO my_table OUTPUT inserted.timestamp DEFAULT VALUES

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

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

Примечание

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

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

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

class MyModel(Base):
    __tablename__ = "my_table"

    timestamp = 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 = 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',)

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

Для SQLite новые временные метки можно генерировать с помощью SQL-функции datetime('now', 'localtime') (или указать 'utc' для UTC), однако все усложняется тем, что эта функция возвращает строковое значение, которое несовместимо с типом данных SQLAlchemy DateTime (хотя этот тип преобразует информацию обратно в строку для бэкенда SQLite, она должна быть передана как Python datetime). Поэтому мы также должны указать, что хотим привести возвращаемое значение к DateTime, когда оно будет возвращено из функции, чего мы добиваемся, передавая его в качестве параметра type_:

class MyModel(Base):
    __tablename__ = "my_table"

    timestamp = Column(
        DateTime,
        default=func.datetime("now", "localtime", type_=DateTime),
        primary_key=True,
    )

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

SELECT datetime(?, ?) AS datetime_1
('now', 'localtime')
INSERT INTO my_table (timestamp) VALUES (?)
('2018-10-02 13:37:33.000000',)

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

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

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

Эти SQL-выражения в настоящее время подвержены тем же ограничениям в ORM, что и настоящие умолчания на стороне сервера; они не будут с нетерпением подбираться с RETURNING при использовании mapper.eager_defaults, если директива 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 = Column(Integer, primary_key=True)

    created = Column(DateTime(), default=func.now(), server_default=FetchedValue())
    updated = 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

Deep Alchemy

Возможность связывать объекты ORM с RETURNING является новой и экспериментальной функцией.

Добавлено в версии 1.4.0.

Конструкции DML insert(), update() и delete() имеют метод UpdateBase.returning(), который на бэкендах баз данных, поддерживающих RETURNING (PostgreSQL, SQL Server, некоторые версии MariaDB), может использоваться для возврата строк базы данных, созданных или сопоставленных оператором, как если бы они были выбраны. Операторы UPDATE и DELETE с поддержкой ORM могут быть объединены с этой функцией, так что они возвращают строки, соответствующие всем строкам, которые были сопоставлены по критериям:

from sqlalchemy import update

stmt = (
    update(User)
    .where(User.name == "squidward")
    .values(name="spongebob")
    .returning(User.id)
)

for row in session.execute(stmt):
    print(f"id: {row.id}")

Приведенный выше пример возвращает атрибут User.id для каждого сопоставленного ряда. При условии, что каждая строка содержит, по крайней мере, значение первичного ключа, мы можем получить эти строки как объекты ORM, что позволит загружать объекты ORM из базы данных, соответствующие атомарно оператору UPDATE для этих строк. Для этого мы можем объединить конструкцию Update, которая возвращает User строк, с конструкцией select(), которая приспособлена для запуска этого оператора UPDATE в контексте ORM с помощью метода Select.from_statement():

stmt = (
    update(User)
    .where(User.name == "squidward")
    .values(name="spongebob")
    .returning(User)
)

orm_stmt = select(User).from_statement(stmt).execution_options(populate_existing=True)

for user in session.execute(orm_stmt).scalars():
    print("updated user: %s" % user)

Выше мы создаем конструкцию update(), которая включает Update.returning(), заданную полной сущностью User, которая будет создавать полные строки из таблицы базы данных по мере их обновления; может быть указан любой произвольный набор столбцов для загрузки при условии, что включен полный первичный ключ. Далее эти строки адаптируются к ORM-загрузке путем создания select() для нужной сущности, затем адаптируются к оператору UPDATE путем передачи конструкции Update методу Select.from_statement(); этот специальный ORM-метод, представленный в Получение результатов ORM из текстовых и основных утверждений, создает специфичный для ORM адаптер, который позволяет данному оператору действовать так, как если бы это был SELECT строк, описанный вначале. На самом деле никакой SELECT в базу данных не передается, только UPDATE…RETURNING, который мы создали.

Наконец, мы используем Заполнить существующие в конструкции, чтобы все данные, возвращенные UPDATE, включая столбцы, которые мы обновили, были заполнены в возвращаемых объектах, заменяя все значения, которые там уже были. Это имеет тот же эффект, как если бы мы использовали стратегию synchronize_session='fetch', описанную ранее в Выбор стратегии синхронизации.

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

Приведенный выше подход можно использовать и для INSERT с RETURNING. В качестве более продвинутого примера ниже показано, как использовать конструкцию PostgreSQL postgresql_insert_on_conflict для INSERT или UPDATE строк в базе данных, одновременно создавая эти объекты как экземпляры ORM:

from sqlalchemy.dialects.postgresql import insert

stmt = insert(User).values(
    [
        dict(name="sandy", fullname="Sandy Cheeks"),
        dict(name="squidward", fullname="Squidward Tentacles"),
        dict(name="spongebob", fullname="Spongebob Squarepants"),
    ]
)

stmt = stmt.on_conflict_do_update(
    index_elements=[User.name], set_=dict(fullname=stmt.excluded.fullname)
).returning(User)

orm_stmt = select(User).from_statement(stmt).execution_options(populate_existing=True)
for user in session.execute(
    orm_stmt,
).scalars():
    print("inserted or updated: %s" % user)

Для начала мы убедимся, что используем PostgreSQL-вариант конструкции insert(). Затем мы создадим многозначный оператор INSERT, где один оператор INSERT будет содержать несколько строк для вставки. В базе данных PostgreSQL этот синтаксис обеспечивает наиболее эффективное средство для одновременной отправки сотен строк для вставки.

Отсюда мы могли бы при желании добавить предложение RETURNING для создания массового INSERT. Однако, чтобы сделать пример еще более интересным, мы также добавим специфический для PostgreSQL синтаксис ON CONFLICT..DO UPDATE, чтобы строки, которые уже существуют на основе уникального критерия, были UPDATEd вместо этого. Мы предполагаем, что существует ограничение INDEX или UNIQUE на столбце name в таблице user_account выше, а затем укажем соответствующий критерий Insert.on_conflict_do_update(), который обновит столбец fullname для уже существующих строк.

Наконец, мы добавляем предложение Insert.returning(), как мы делали в предыдущем примере, и выбираем наши объекты User, используя тот же подход Select.from_statement(), что и ранее. Предположим, что в базе данных присутствует только строка для (1, "squidward", NULL); эта строка вызовет процедуру ON CONFLICT в нашем вышеприведенном операторе, другими словами, выполнит эквивалент оператора UPDATE. Две другие строки, (NULL, "sandy", "Sandy Cheeks") и (NULL, "spongebob", "Spongebob Squarepants"), еще не существуют в базе данных и будут вставлены с использованием обычной семантики INSERT; столбец первичного ключа id использует либо SERIAL, либо IDENTITY для автоматической генерации новых целочисленных значений.

Используя приведенную выше форму, мы видим SQL, выдаваемый базе данных PostgreSQL в виде:

INSERT INTO user_account (name, fullname) VALUES (%(name_m0)s, %(fullname_m0)s), (%(name_m1)s, %(fullname_m1)s), (%(name_m2)s, %(fullname_m2)s) ON CONFLICT (name) DO UPDATE SET fullname = excluded.fullname RETURNING user_account.id, user_account.name, user_account.fullname {'name_m0': 'sandy', 'fullname_m0': 'Sandy Cheeks', 'name_m1': 'squidward', 'fullname_m1': 'Squidward Tentacles', 'name_m2': 'spongebob', 'fullname_m2': 'Spongebob Squarepants'}
inserted or updated: User(id=2, name='sandy', fullname='Sandy Cheeks') inserted or updated: User(id=3, name='squidward', fullname='Squidward Tentacles') inserted or updated: User(id=1, name='spongebob', fullname='Spongebob Squarepants')

Выше мы также видим, что объекты INSERTed User имеют вновь созданное значение первичного ключа, как мы и ожидали от любого другого оператора INSERT, ориентированного на ORM.

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

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

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

engine1 = create_engine("postgresql://db1")
engine2 = create_engine("postgresql://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) для определенного сопоставленного класса. Предположим, что две декларативные базы представляют два разных подключения к базе данных:

BaseA = declarative_base()

BaseB = declarative_base()

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. Пример использования приведен на: Горизонтальное разделение.

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

Deep Alchemy

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

Обычно в использовании этих процедур нет необходимости, и их нелегко использовать, так как в них отсутствует много поведений, которые обычно ожидаются при использовании объектов ORM; для эффективной массовой вставки лучше использовать непосредственно конструкцию Core Insert. Пожалуйста, прочитайте все предостережения в Совместимость с ORM / предостережения.

Примечание

Bulk INSERT и UPDATE не следует путать с более распространенной функцией, известной как UPDATE и DELETE с произвольным предложением WHERE. Эта функция позволяет выпустить один оператор UPDATE или DELETE с произвольными критериями WHERE. В некоторых бэкендах также есть возможность использовать истинный «upsert» с ORM, например, в PostgreSQL. Примеры смотрите в разделе Использование INSERT, UPDATE и ON CONFLICT (т.е. upsert) для возврата объектов ORM.

См.также

UPDATE и DELETE с произвольным предложением WHERE - использование прямых многорядных операторов UPDATE и DELETE в контексте ORM.

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

Добавлено в версии 1.0.0.

Массовые операции INSERT/per-row UPDATE на Session включают Session.bulk_save_objects(), Session.bulk_insert_mappings() и Session.bulk_update_mappings(). Цель этих методов - напрямую раскрыть внутренние элементы системы единицы работы, так, чтобы средства для создания операторов INSERT и UPDATE, заданных словарями или состояниями объектов, можно было использовать самостоятельно, минуя обычную механику работы единицы работы по управлению состояниями, отношениями и атрибутами. Преимущество такого подхода заключается исключительно в снижении накладных расходов на Python:

  • Процесс flush(), включая опрос всех объектов, их состояние, их каскадное состояние, состояние всех объектов, связанных с ними через relationship(), и топологическую сортировку всех выполняемых операций, полностью обходится. Это значительно снижает накладные расходы Python.

  • Указанные объекты не имеют определенного отношения к цели Session, даже когда операция завершена, что означает отсутствие накладных расходов на их присоединение или управление их состоянием с точки зрения карты идентификации или сессии.

  • Методы Session.bulk_insert_mappings() и Session.bulk_update_mappings() принимают списки обычных словарей Python, а не объектов; это дополнительно снижает большие накладные расходы, связанные с инстанцированием отображаемых объектов и присвоением им состояния, которое обычно также подвергается дорогостоящему отслеживанию истории на основе каждого атрибута.

  • Набор объектов, переданных всем массовым методам, обрабатывается в порядке их поступления. В случае Session.bulk_save_objects(), когда передаются объекты разных типов, операторы INSERT и UPDATE обязательно разбиваются на группы по типам. Чтобы уменьшить количество пакетных операторов INSERT или UPDATE, передаваемых в DBAPI, убедитесь, что входящий список объектов сгруппирован по типам.

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

  • Операторы UPDATE также можно настроить таким образом, чтобы все атрибуты безоговорочно подчинялись условию SET, что опять же повышает вероятность использования блоков executemany().

Поведение процедур массовых операций следует изучать с помощью набора примеров Производительность. Это серия примеров сценариев, которые иллюстрируют количество вызовов Python в различных сценариях, включая сценарии массовой вставки и обновления.

См.также

Производительность - включает подробные примеры массовых операций в сравнении с традиционными методами Core и ORM, включая метрики производительности.

Использование

Каждый из методов работает в контексте транзакции объекта Session, как и любой другой:

s = Session()
objects = [User(name="u1"), User(name="u2"), User(name="u3")]
s.bulk_save_objects(objects)

Для Session.bulk_insert_mappings() и Session.bulk_update_mappings() передаются словари:

s.bulk_insert_mappings(User, [dict(name="u1"), dict(name="u2"), dict(name="u3")])

См.также

Session.bulk_save_objects()

Session.bulk_insert_mappings()

Session.bulk_update_mappings()

Сравнение с основными конструкциями вставки/обновления

Методы bulk обеспечивают производительность, которая при определенных обстоятельствах может быть близка к производительности использования основных конструкций Insert и Update в контексте «executemany» (для описания «executemany» см. раздел Отправка нескольких параметров в учебнике по Core). Для достижения этого флаг Session.bulk_insert_mappings.return_defaults должен быть отключен, чтобы строки можно было объединять в пакет. Набор примеров в Производительность следует внимательно изучить, чтобы понять, насколько быстро можно добиться производительности пакетной обработки.

Совместимость с ORM / предостережения

Предупреждение

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

Методы массовой вставки / обновления теряют значительную часть функциональности по сравнению с традиционным использованием ORM. Ниже приведен список функций, которые не доступны при использовании этих методов:

  • устойчивость вдоль связей relationship()

  • сортировка строк в порядке зависимости; строки вставляются или обновляются непосредственно в том порядке, в котором они передаются в методы

  • Управление сессиями на данных объектах, включая прикрепление к сессии, управление картой идентификации.

  • Функциональность, связанная с мутацией первичного ключа, каскад ON UPDATE - мутация столбцов первичного ключа не будет работать - поскольку исходное значение PK для каждой строки недоступно, поэтому критерии WHERE не могут быть сгенерированы.

  • Вставка/обновление SQL-выражений (например, Встраивание выражений вставки/обновления SQL в промывку) - необходимость их оценки не позволит объединять операторы INSERT и UPDATE в один вызов executemany(), поскольку они изменяют SQL-компиляцию самого оператора.

  • События ORM, такие как MapperEvents.before_insert() и т.д. Методы массовых сессий не имеют поддержки событий.

Особенности, которые в наличии включают:

  • INSERTs и UPDATEs отображенных объектов

  • Поддержка идентификаторов версий

  • Многотабличные отображения, такие как объединенное наследование - однако, объект, который будет вставлен в несколько таблиц, должен либо заранее иметь полностью заполненные идентификаторы первичных ключей, либо должен быть использован флаг Session.bulk_save_objects.return_defaults, что значительно снизит преимущества в производительности

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