Работа с двигателями и соединениями

В этом разделе описывается прямое использование объектов Engine, Connection и связанных с ними объектов. Важно отметить, что при использовании SQLAlchemy ORM к этим объектам обычно не обращаются; вместо этого в качестве интерфейса к базе данных используется объект Session. Однако для приложений, построенных на прямом использовании текстовых операторов SQL и/или конструкций выражений SQL без участия служб управления более высокого уровня ORM, Engine и Connection являются королем (и королевой?) - читайте далее.

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

Вспомним из Конфигурация двигателя, что Engine создается с помощью вызова create_engine():

engine = create_engine("mysql://scott:tiger@localhost/test")

Обычно create_engine() используется один раз для каждого конкретного URL базы данных, хранящегося глобально в течение всего времени жизни одного прикладного процесса. Один Engine управляет множеством отдельных соединений DBAPI от имени процесса и предназначен для одновременного обращения к нему. Engine является не синонимом функции DBAPI connect, которая представляет только один ресурс соединения - Engine наиболее эффективен, когда создается только один раз на уровне модуля приложения, а не на каждый объект или вызов каждой функции.

Самая основная функция Engine заключается в предоставлении доступа к Connection, который затем может вызывать SQL-запросы. Вызов текстового оператора к базе данных выглядит следующим образом:

from sqlalchemy import text

with engine.connect() as connection:
    result = connection.execute(text("select username from users"))
    for row in result:
        print("username:", row["username"])

Выше, метод Engine.connect() возвращает объект Connection, и при использовании его в менеджере контекста Python (например, оператор with:) метод Connection.close() автоматически вызывается в конце блока. Метод Connection представляет собой прокси объект для фактического DBAPI-соединения. DBAPI-соединение извлекается из пула соединений в тот момент, когда создается Connection.

Возвращаемый объект известен как CursorResult, который ссылается на курсор DBAPI и предоставляет методы для получения строк, аналогичные методам курсора DBAPI. Курсор DBAPI будет закрыт CursorResult, когда все его строки результатов (если таковые имеются) будут исчерпаны. Курсор CursorResult, не возвращающий никаких строк, например, оператор UPDATE (без возвращаемых строк), освобождает ресурсы курсора сразу же после создания.

Когда Connection закрывается в конце блока with:, ссылающееся DBAPI-соединение released передается в пул соединений. С точки зрения самой базы данных, пул соединений фактически не «закрывает» соединение, предполагая, что в пуле есть место для хранения этого соединения для следующего использования. Когда соединение возвращается в пул для повторного использования, механизм пула выполняет вызов rollback() на соединении DBAPI, так что все транзакционные состояния или блокировки удаляются, и соединение готово к следующему использованию.

Наш пример выше иллюстрировал выполнение текстовой строки SQL, которая должна быть вызвана с помощью конструкции text(), чтобы указать, что мы хотим использовать текстовый SQL. Метод Connection.execute(), конечно, может использовать не только это, включая различные конструкции выражений SQL, описанные в Учебник по языку выражений SQL (API 1.x).

Использование транзакций

Примечание

В этом разделе описывается, как использовать транзакции при работе непосредственно с объектами Engine и Connection. При использовании SQLAlchemy ORM публичный API для управления транзакциями осуществляется через объект Session, который внутренне использует объект Transaction. Дополнительную информацию см. в разделе Управление транзакциями.

Объект Connection предоставляет метод Connection.begin(), который возвращает объект Transaction. Как и сам Connection, этот объект обычно используется внутри блока Python with:, так что его область видимости управляется:

with engine.connect() as connection:
    with connection.begin():
        r1 = connection.execute(table1.select())
        connection.execute(table1.insert(), {"col1": 7, "col2": "this is some data"})

Приведенный выше блок можно изложить более просто, используя метод Engine.begin() из Engine:

# runs a transaction
with engine.begin() as connection:
    r1 = connection.execute(table1.select())
    connection.execute(table1.insert(), {"col1": 7, "col2": "this is some data"})

Блок, управляемый каждым методом .begin(), имеет такое поведение, что транзакция фиксируется, когда блок завершается. Если возникает исключение, транзакция откатывается, а исключение распространяется наружу.

Базовым объектом, используемым для представления транзакции, является объект Transaction. Этот объект возвращается методом Connection.begin() и включает методы Transaction.commit() и Transaction.rollback(). В качестве лучшей практики рекомендуется использовать форму вызова контекстного менеджера, которая вызывает эти методы автоматически.

Вложенность блоков транзакций

Не рекомендуется, начиная с версии 1.4: Функция «вложенности транзакций» в SQLAlchemy - это унаследованная функция, которая устарела в версии 1.4 и будет удалена в SQLAlchemy 2.0. Этот шаблон оказался слишком неудобным и сложным, если только приложение не создаст вокруг него первоклассный фреймворк. См. следующий подраздел Произвольное вложение транзакций как антипаттерн.

Объект Transaction также обрабатывает «вложенное» поведение, отслеживая крайнюю пару begin/commit. В этом примере две функции обе выдают транзакцию на Connection, но только крайний объект Transaction фактически вступает в силу после фиксации.

# method_a starts a transaction and calls method_b
def method_a(connection):
    with connection.begin():  # open a transaction
        method_b(connection)


# method_b also starts a transaction
def method_b(connection):
    with connection.begin():  # open a transaction - this runs in the
        # context of method_a's transaction
        connection.execute(text("insert into mytable values ('bat', 'lala')"))
        connection.execute(mytable.insert(), {"col1": "bat", "col2": "lala"})


# open a Connection and call method_a
with engine.connect() as conn:
    method_a(conn)

Выше, сначала вызывается method_a, который вызывает connection.begin(). Затем вызывается method_b. Когда method_b вызывает connection.begin(), он просто увеличивает счетчик, который уменьшается при вызове commit(). Если либо method_a, либо method_b вызывает rollback(), вся транзакция откатывается. Транзакция не фиксируется до тех пор, пока method_a не вызовет метод commit(). Такое «вложенное» поведение позволяет создавать функции, которые «гарантируют» использование транзакции, если таковой еще не было, но автоматически участвуют в охватывающей транзакции, если таковая существует.

Произвольное вложение транзакций как антипаттерн

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

Наиболее идеальным способом организации приложения было бы наличие единственной или, по крайней мере, очень немногих точек, в которых разграничиваются «начало» и «фиксация» всех транзакций базы данных. Это также общая идея, обсуждаемая в терминах ORM в Когда я строю Session, когда я фиксирую его и когда я закрываю его?. Адаптация примера из предыдущего раздела к этой практике выглядит следующим образом:

# method_a calls method_b
def method_a(connection):
    method_b(connection)


# method_b uses the connection and assumes the transaction
# is external
def method_b(connection):
    connection.execute(text("insert into mytable values ('bat', 'lala')"))
    connection.execute(mytable.insert(), {"col1": "bat", "col2": "lala"})


# open a Connection inside of a transaction and call method_a
with engine.begin() as conn:
    method_a(conn)

То есть method_a() и method_b() вообще не имеют дело с деталями транзакции; транзакционная область действия соединения определяется внешне для функций, которые ведут SQL-диалог с соединением.

Можно заметить, что в приведенном выше коде меньше строк и меньше отступов, что, как правило, коррелирует с меньшим cyclomatic complexity. Приведенный выше код организован таким образом, что method_a() и method_b() всегда вызываются из точки, в которой начинается транзакция. В предыдущей версии примера присутствуют method_a() и method_b(), которые стараются не замечать этого факта, что говорит о том, что они готовы, по крайней мере, к вдвое большему количеству потенциальных кодовых путей через них.

Переход от модели «вложенности»

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

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

import contextlib


@contextlib.contextmanager
def transaction(connection):
    if not connection.in_transaction():
        with connection.begin():
            yield connection
    else:
        yield connection

Приведенный выше контекст-менеджер будет использоваться как:

# method_a starts a transaction and calls method_b
def method_a(connection):
    with transaction(connection):  # open a transaction
        method_b(connection)


# method_b either starts a transaction, or uses the one already
# present
def method_b(connection):
    with transaction(connection):  # open a transaction
        connection.execute(text("insert into mytable values ('bat', 'lala')"))
        connection.execute(mytable.insert(), {"col1": "bat", "col2": "lala"})


# open a Connection and call method_a
with engine.connect() as conn:
    method_a(conn)

Аналогичный подход может быть использован и для того, чтобы соединение устанавливалось по требованию; в приведенном ниже подходе используется одноразовый менеджер контекста, который обращается к вложенному состоянию, чтобы проверить, присутствует ли уже соединение:

import contextlib


def connectivity(engine):
    connection = None

    @contextlib.contextmanager
    def connect():
        nonlocal connection

        if connection is None:
            connection = engine.connect()
            with connection:
                with connection.begin():
                    yield connection
        else:
            yield connection

    return connect

При использовании вышеприведенной схемы это будет выглядеть так:

# method_a passes along connectivity context, at the same time
# it chooses to establish a connection by calling "with"
def method_a(connectivity):
    with connectivity():
        method_b(connectivity)


# method_b also wants to use a connection from the context, so it
# also calls "with:", but also it actually uses the connection.
def method_b(connectivity):
    with connectivity() as connection:
        connection.execute(text("insert into mytable values ('bat', 'lala')"))
        connection.execute(mytable.insert(), {"col1": "bat", "col2": "lala"})


# create a new connection/transaction context object and call
# method_a
method_a(connectivity(engine))

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

Уровень библиотеки (например, эмулированный) Автокоммит

Не рекомендуется, начиная с версии 1.4: Функция «autocommit» в SQLAlchemy Core устарела и не будет присутствовать в версии 2.0 SQLAlchemy. Теперь широко доступен АВТОКОММИТ на уровне DBAPI, который обеспечивает более высокую производительность и происходит прозрачно. Смотрите «Автокоммит» на уровне библиотеки (но не на уровне драйвера) удален из Core и ORM для справки.

Примечание

В этом разделе обсуждается функция в SQLAlchemy, которая автоматически вызывает метод .commit() на DBAPI-соединении, однако это происходит на DBAPI-соединении, которое само является транзакционным. Для настоящего AUTOCOMMIT смотрите следующий раздел Установка уровней изоляции транзакций, включая DBAPI Autocommit.

В предыдущем примере транзакции показано, как использовать Transaction, чтобы в одной транзакции могло участвовать несколько исполнений. Что произойдет, если мы выполним вызов INSERT, UPDATE или DELETE без использования Transaction? Хотя некоторые реализации DBAPI предоставляют различные специальные «нетранзакционные» режимы, основное поведение DBAPI согласно PEP-0249 заключается в том, что транзакция всегда находится в процессе, предоставляя только методы rollback() и commit(), но не begin(). SQLAlchemy предполагает, что это так для любого данного DBAPI.

Учитывая это требование, SQLAlchemy реализует собственную функцию «autocommit», которая работает абсолютно последовательно во всех бэкендах. Это достигается путем обнаружения утверждений, которые представляют собой операции по изменению данных, т.е. INSERT, UPDATE, DELETE, а также утверждений языка определения данных (DDL), таких как CREATE TABLE, ALTER TABLE, и последующей автоматической выдачи COMMIT, если транзакция не выполняется. Обнаружение основано на наличии опции выполнения autocommit=True в операторе. Если оператор является только текстовым и флаг не установлен, то для обнаружения INSERT, UPDATE, DELETE, а также ряда других команд для конкретного бэкенда используется регулярное выражение:

conn = engine.connect()
conn.execute(text("INSERT INTO users VALUES (1, 'john')"))  # autocommits

Функция «autocommit» действует только в том случае, если не было объявлено никакого Transaction. Это означает, что функция обычно не используется в ORM, так как объект Session по умолчанию всегда поддерживает текущий Transaction.

Полный контроль над поведением «автокоммита» доступен с помощью генеративного метода Connection.execution_options(), предоставляемого в Connection и Engine, используя флаг «autocommit», который включает или выключает автокоммит для выбранной области видимости. Например, конструкция text(), представляющая хранимую процедуру, которая фиксирует, может использовать его так, что оператор SELECT будет выдавать COMMIT:

with engine.connect().execution_options(autocommit=True) as conn:
    conn.execute(text("SELECT my_mutating_procedure()"))

Установка уровней изоляции транзакций, включая DBAPI Autocommit

Большинство DBAPI поддерживают концепцию настраиваемых уровней транзакций isolation. Традиционно это четыре уровня «READ UNCOMMITTED», «READ COMMITTED», «REPEATABLE READ» и «SERIALIZABLE». Они обычно применяются к соединению DBAPI перед началом новой транзакции, при этом следует отметить, что большинство DBAPI начинают транзакцию неявно при первом выполнении SQL-запросов.

DBAPI, поддерживающие уровни изоляции, обычно также поддерживают концепцию истинного «автокоммита», что означает, что само соединение DBAPI будет переведено в нетранзакционный режим автокоммита. Это обычно означает, что типичное поведение DBAPI, заключающееся в автоматической передаче «BEGIN» в базу данных, больше не происходит, но оно может включать и другие директивы. SQLAlchemy рассматривает концепцию «autocommit» как любой другой уровень изоляции; в том смысле, что это уровень изоляции, который теряет не только «read committed», но и теряет атомарность.

Совет

Важно отметить, как будет обсуждаться далее в разделе Понимание уровня изоляции автокоммита на уровне DBAPI, что уровень изоляции «autocommit», как и любой другой уровень изоляции, не влияет на «транзакционное» поведение объекта Connection, который продолжает вызывать методы DBAPI .commit() и .rollback() (они просто не имеют эффекта при autocommit), и для которого метод .begin() предполагает, что DBAPI начнет транзакцию неявно (это означает, что «begin» SQLAlchemy не изменяет режим autocommit).

Диалекты SQLAlchemy должны поддерживать эти уровни изоляции, а также автокоммит в максимально возможной степени. Уровни устанавливаются с помощью семейства параметров «execution_options» и методов, которые присутствуют во всем ядре, например, метод Connection.execution_options(). Параметр известен как Connection.execution_options.isolation_level, а значения представляют собой строки, которые обычно являются подмножеством следующих имен:

# possible values for Connection.execution_options(isolation_level="<value>")

"AUTOCOMMIT"
"READ COMMITTED"
"READ UNCOMMITTED"
"REPEATABLE READ"
"SERIALIZABLE"

Не каждый DBAPI поддерживает все значения; если для определенного бэкенда используется неподдерживаемое значение, выдается ошибка.

Например, чтобы заставить REPEATABLE READ на определенном соединении, начните транзакцию:

with engine.connect().execution_options(isolation_level="REPEATABLE READ") as connection:
    with connection.begin():
        connection.execute(<statement>)

Примечание

Возвращаемое значение метода Connection.execution_options() представляет собой так называемое «разветвленное» соединение в рамках серии SQLAlchemy 1.x, когда не используется режим create_engine.future, которое является неглубокой копией исходного объекта Connection. Несмотря на это, опция выполнения isolation_level применяется к исходному объекту Connection и всем «ветвям» в целом.

При использовании режима create_engine.future (т.е. при использовании 2.0 style) концепция этих так называемых «разветвленных» соединений удаляется, и Connection.execution_options() возвращает тот же объект Connection без создания каких-либо копий.

Параметр Connection.execution_options.isolation_level также может быть установлен на весь движок, что часто предпочтительнее. Это достигается путем передачи его в параметре create_engine.execution_options к create_engine():

from sqlalchemy import create_engine

eng = create_engine(
    "postgresql://scott:tiger@localhost/test",
    execution_options={"isolation_level": "REPEATABLE READ"},
)

При указанной выше настройке соединение DBAPI будет настроено на использование установки уровня изоляции "REPEATABLE READ" для каждой новой начатой транзакции.

Приложение, которое часто выбирает выполнение операций на разных уровнях изоляции, может пожелать создать несколько «под-движков» ведущего Engine, каждый из которых будет настроен на разный уровень изоляции. Одним из таких случаев является приложение, в котором операции разделяются на «транзакционные» и «только для чтения», отдельный Engine, использующий "AUTOCOMMIT", может быть отделен от основного двигателя:

from sqlalchemy import create_engine

eng = create_engine("postgresql://scott:tiger@localhost/test")

autocommit_engine = eng.execution_options(isolation_level="AUTOCOMMIT")

Выше, метод Engine.execution_options() создает неглубокую копию исходного Engine. Оба eng и autocommit_engine используют один и тот же диалект и пул соединений. Однако режим «AUTOCOMMIT» будет установлен на соединениях, когда они будут получены от autocommit_engine.

Настройка уровня изоляции, независимо от того, какая она, безоговорочно возвращается, когда соединение возвращается в пул соединений.

Примечание

Параметр Connection.execution_options.isolation_level обязательно не применяется к опциям уровня оператора, таким как Executable.execution_options(). Это связано с тем, что опция должна быть установлена на соединении DBAPI на основе каждой транзакции.

См.также

SQLite Transaction Isolation

PostgreSQL Transaction Isolation

MySQL Transaction Isolation

SQL Server Transaction Isolation

Настройка уровней изоляции транзакций / DBAPI AUTOCOMMIT - для ORM

Использование автокоммита DBAPI позволяет использовать версию прозрачного восстановления только для чтения - рецепт, использующий DBAPI autocommit для прозрачного повторного подключения к базе данных для операций только для чтения

Понимание уровня изоляции автокоммита на уровне DBAPI

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

Если бы мы хотели проверить объект Connection и использовать его в режиме «autocommit», мы бы поступили следующим образом:

with engine.connect() as connection:
    connection.execution_options(isolation_level="AUTOCOMMIT")
    connection.execute(<statement>)
    connection.execute(<statement>)

Выше показано обычное использование режима «DBAPI autocommit». Нет необходимости использовать такие методы, как Connection.begin() или Connection.commit() (примечание: последнее относится к использованию 2.0 style).

Однако важно отметить, что указанный выше режим автокоммита постоянный для данного конкретного соединения, пока мы не изменим его непосредственно с помощью isolation_level снова. Уровень изоляции также сбрасывается для соединения DBAPI, когда мы release возвращаем соединение обратно в пул соединений. Однако вызов Connection.begin() не изменит уровень изоляции, что означает, что мы остаемся в автокоммите. Пример ниже иллюстрирует это:

with engine.connect() as connection:
    connection = connection.execution_options(isolation_level="AUTOCOMMIT")

    # this begin() does nothing, isolation stays at AUTOCOMMIT
    with connection.begin() as trans:
        connection.execute(<statement>)
        connection.execute(<statement>)

Когда мы запускаем блок, подобный приведенному выше, с включенным протоколированием, протоколирование попытается указать, что, хотя вызывается уровень DBAPI .commit(), он, вероятно, не будет иметь никакого эффекта из-за режима autocommit:

INFO sqlalchemy.engine.Engine BEGIN (implicit)
...
INFO sqlalchemy.engine.Engine COMMIT using DBAPI connection.commit(), DBAPI should ignore due to autocommit mode

Аналогично, при использовании режима 2.0 style create_engine.future, Connection будет использовать поведение autobegin, что означает, что приведенный ниже шаблон вызовет ошибку:

engine = create_engine(..., future=True)

with engine.connect() as connection:
    connection = connection.execution_options(isolation_level="AUTOCOMMIT")

    # "transaction" is autobegin (but has no effect due to autocommit)
    connection.execute(<statement>)

    # this will raise; "transaction" is already begun
    with connection.begin() as trans:
        connection.execute(<statement>)

Это все для того, чтобы продемонстрировать, что установка уровня изоляции autocommit полностью независима от поведения begin/commit объекта SQLAlchemy Connection. Режим «autocommit» никак не взаимодействует с Connection.begin(), а Connection не обращается к этому статусу при выполнении своих собственных изменений состояния транзакции (за исключением сообщения в журнале движка о том, что эти блоки на самом деле не фиксируются). Обоснованием такой конструкции является поддержание полностью последовательной схемы использования Connection, в которой режим DBAPI-автокоммита может быть изменен независимо без указания каких-либо изменений кода в других местах.

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

Чтобы проиллюстрировать, как использовать «autocommit» в ad-hoc режиме в рамках одной проверки Connection, параметр Connection.execution_options.isolation_level должен быть повторно применен с предыдущим уровнем изоляции. Мы можем «правильно» записать наш вышеприведенный блок как (отмечая использование стиля 2.0 ниже):

# if we wanted to flip autocommit on and off on a single connection/
# which... we usually don't.

engine = create_engine(..., future=True)

with engine.connect() as connection:

    connection.execution_options(isolation_level="AUTOCOMMIT")

    # run statement(s) in autocommit mode
    connection.execute(<statement>)

    # "commit" the autobegun "transaction" (2.0/future mode only)
    connection.commit()

    # switch to default isolation level
    connection.execution_options(isolation_level=connection.default_isolation_level)

    # use a begin block
    with connection.begin() as trans:
        connection.execute(<statement>)

Выше, чтобы вручную изменить уровень изоляции, мы использовали Connection.default_isolation_level для восстановления уровня изоляции по умолчанию (предполагая, что это то, что мы хотим здесь). Однако, вероятно, лучше работать с архитектурой Connection, которая уже обрабатывает сброс уровня изоляции автоматически при регистрации. предпочтительный способ написать вышеописанное - использовать два блока

engine = create_engine(..., future=True)

# use an autocommit block
with engine.connect().execution_options(isolation_level="AUTOCOMMIT") as connection:

    # run statement in autocommit mode
    connection.execute(<statement>)

# use a regular block
with engine.begin() as connection:
    connection.execute(<statement>)

Подведем итоги:

  1. Уровень изоляции «автокоммит на уровне DBAPI» полностью независим от понятия «начать» и «зафиксировать» объекта Connection.

  2. используйте отдельные проверки Connection для каждого уровня изоляции. Избегайте попыток переключения туда и обратно между «autocommit» при проверке одного соединения; позвольте движку выполнить работу по восстановлению уровней изоляции по умолчанию

Использование курсоров на стороне сервера (они же потоковые результаты)

Некоторые бэкенды имеют явную поддержку концепции «курсоров на стороне сервера» и «курсоров на стороне клиента». Курсор на стороне клиента означает, что драйвер базы данных полностью забирает все строки из набора результатов в память перед возвратом после выполнения оператора. Такие драйверы, как PostgreSQL и MySQL/MariaDB, обычно используют курсоры на стороне клиента по умолчанию. Курсор на стороне сервера, напротив, указывает на то, что строки результатов остаются в состоянии ожидания на сервере базы данных, пока строки результатов потребляются клиентом. Например, драйверы для Oracle обычно используют модель «на стороне сервера», а диалект SQLite, хотя и не использует настоящую архитектуру «клиент/сервер», все же использует подход небуферизованной выборки результатов, который оставляет строки результатов вне памяти процесса до того, как они будут потреблены.

Из этой базовой архитектуры следует, что «курсор на стороне сервера» более эффективен при получении очень больших наборов результатов, но в то же время может усложнить процесс взаимодействия клиента и сервера и быть менее эффективным для небольших наборов результатов (обычно менее 10000 строк).

Для тех диалектов, которые имеют условную поддержку буферизованных или небуферизованных результатов, обычно существуют ограничения на использование «небуферизованного» режима, или режима курсора на стороне сервера. При использовании диалекта psycopg2, например, ошибка возникает, если курсор на стороне сервера используется с любым типом оператора DML или DDL. При использовании драйверов MySQL с курсором на стороне сервера, соединение DBAPI находится в более хрупком состоянии и не восстанавливается так же изящно после ошибок, а также не позволяет выполнить откат до тех пор, пока курсор не будет полностью закрыт.

По этой причине диалекты SQLAlchemy всегда по умолчанию используют менее подверженную ошибкам версию курсора, что означает, что для диалектов PostgreSQL и MySQL по умолчанию используется буферизованный курсор «на стороне клиента», где полный набор результатов забирается в память до вызова любых методов выборки из курсора. Такой режим работы подходит в подавляющем большинстве случаев; небуферизированные курсоры, как правило, не полезны, за исключением редких случаев, когда приложение получает очень большое количество строк по частям, где обработка этих строк может быть завершена до получения большего количества строк.

Для драйверов баз данных, предоставляющих опции курсора на стороне клиента и сервера, опции выполнения Connection.execution_options.stream_results и Connection.execution_options.yield_per предоставляют доступ к «курсорам на стороне сервера» на основе каждого Connection или каждого запроса. Аналогичные опции существуют и при использовании ORM Session.

Потоковая передача с фиксированным буфером через yield_per

Поскольку отдельные операции выборки строк с полностью небуферизованными курсорами на стороне сервера обычно обходятся дороже, чем выборка сразу нескольких строк, параметр выполнения Connection.execution_options.yield_per настраивает Connection или оператор на использование доступных курсоров на стороне сервера, в то же время настраивая буфер строк фиксированного размера, который будет получать строки с сервера партиями по мере их потребления. Этот параметр может иметь целочисленное положительное значение при использовании метода Connection.execution_options() на Connection или на операторе при использовании метода Executable.execution_options().

Добавлено в версии 1.4.40: Connection.execution_options.yield_per как опция только для Core является новой в SQLAlchemy 1.4.40; для предыдущих версий 1.4 используйте Connection.execution_options.stream_results непосредственно в сочетании с Result.yield_per().

Использование этой опции эквивалентно ручной установке опции Connection.execution_options.stream_results, описанной в следующем разделе, а затем вызову метода Result.yield_per() на объекте Result с заданным целочисленным значением. В обоих случаях эффект, который дает эта комбинация, включает:

  • режим курсоров на стороне сервера выбирается для данного бэкенда, если он доступен и не является поведением по умолчанию для данного бэкенда

  • по мере получения строк результата, они будут буферизироваться партиями, где размер каждой партии до последней партии будет равен целочисленному аргументу, переданному опции Connection.execution_options.yield_per или методу Result.yield_per(); последняя партия затем буферизируется относительно оставшихся строк, меньших, чем этот размер

  • Размер раздела по умолчанию, используемый методом Result.partitions(), если он используется, также будет сделан равным этому целочисленному размеру.

Эти три вида поведения проиллюстрированы в примере ниже:

with engine.connect() as conn:
    result = conn.execution_options(yield_per=100).execute(text("select * from table"))

    for partition in result.partitions():
        # partition is an iterable that will be at most 100 items
        for row in partition:
            print(f"{row}")

Приведенный выше пример иллюстрирует комбинацию yield_per=100 вместе с использованием метода Result.partitions() для запуска обработки строк в партиях, соответствующих размеру, получаемых с сервера. Использование Result.partitions() является необязательным, и если Result итерируется напрямую, новая партия строк будет буферизироваться для каждых 100 полученных строк. Вызов такого метода, как Result.all(), не должен использоваться, так как это приведет к полной выборке всех оставшихся строк сразу и нарушит цель использования yield_per.

Опция Connection.execution_options.yield_per переносится и в ORM, используется Session для получения объектов ORM, где она также ограничивает количество объектов ORM, создаваемых одновременно. Более подробную информацию об использовании Выборка больших наборов результатов с доходностью за с ORM смотрите в разделе Руководство по составлению запросов ORM - в Connection.execution_options.yield_per.

Добавлено в версии 1.4.40: Добавлено Connection.execution_options.yield_per в качестве опции выполнения на уровне ядра для удобной установки результатов потоковой обработки, размера буфера и размера раздела одновременно, что можно перенести на аналогичный случай использования ORM.

Потоковая передача с динамически растущим буфером с использованием stream_results

Чтобы включить курсоры на стороне сервера без определенного размера раздела, можно использовать опцию Connection.execution_options.stream_results, которая, как и Connection.execution_options.yield_per, может быть вызвана на объекте Connection или объекте statement.

Когда объект Result, переданный с помощью опции Connection.execution_options.stream_results, итерируется напрямую, строки извлекаются внутренне, используя схему буферизации по умолчанию, которая буферизирует сначала небольшой набор строк, затем все больший и больший буфер при каждом извлечении до предварительно настроенного предела в 1000 строк. На максимальный размер этого буфера можно повлиять с помощью опции выполнения Connection.execution_options.max_row_buffer:

with engine.connect() as conn:
    conn = conn.execution_options(stream_results=True, max_row_buffer=100)
    result = conn.execute(text("select * from table"))

    for row in result:
        print(f"{row}")

Хотя опция Connection.execution_options.stream_results может быть совмещена с использованием метода Result.partitions(), в опцию Result.partitions() следует передать определенный размер раздела, чтобы не получить весь результат. Обычно проще использовать опцию Connection.execution_options.yield_per при настройке на использование метода Result.partitions().

Выполнение без подключения, неявное выполнение

Не рекомендуется, начиная с версии 2.0: Функции «без соединения» и «неявного» выполнения в SQLAlchemy устарели и будут удалены в версии 2.0. Смотрите «Неявное» и «Бесконтактное» выполнение, «связанные метаданные» удалены для справки.

Помните, в первом разделе мы упоминали выполнение с явным использованием Connection и без него. «Выполнение без подключения относится к использованию метода execute() на объекте, который не является Connection. Это было проиллюстрировано на примере метода Engine.execute() из Engine:

result = engine.execute(text("select username from users"))
for row in result:
    print("username:", row["username"])

В дополнение к исполнению «без соединения», также возможно использовать метод Executable.execute() любой конструкции Executable, которая является маркером для объектов SQL-выражения, поддерживающих исполнение. Сам объект выражения SQL ссылается на Engine или Connection, известный как bind, который он использует для предоставления так называемых «неявных» услуг исполнения.

Дана таблица следующего вида:

from sqlalchemy import MetaData, Table, Column, Integer

metadata_obj = MetaData()
users_table = Table(
    "users",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
)

Явное выполнение передает текст SQL или построенное выражение SQL в метод Connection.execute() из Connection:

engine = create_engine('sqlite:///file.db')
with engine.connect() as connection:
    result = connection.execute(users_table.select())
    for row in result:
        # ....

Явное выполнение без подключения доставляет выражение в метод Engine.execute() из Engine:

engine = create_engine('sqlite:///file.db')
result = engine.execute(users_table.select())
for row in result:
    # ....
result.close()

Неявное выполнение также не имеет соединения и использует метод Executable.execute() в самом выражении. Этот метод предоставляется как часть класса Executable, который ссылается на оператор SQL, достаточный для вызова к базе данных. Метод использует предположение, что либо Engine, либо Connection были привязаны к объекту выражения. Под «привязкой» мы подразумеваем, что специальный атрибут MetaData.bind был использован для связывания серии объектов Table и всех SQL-конструкций, полученных из них, с определенным движком:

engine = create_engine('sqlite:///file.db')
metadata_obj.bind = engine
result = users_table.select().execute()
for row in result:
    # ....
result.close()

Выше мы связали Engine с объектом MetaData с помощью специального атрибута MetaData.bind. Конструкция select(), создаваемая из объекта Table, имеет метод Executable.execute(), который будет искать Engine, «привязанный» к Table.

В целом, использование «связанных метаданных» имеет три общих эффекта:

  • Объекты операторов SQL получают метод Executable.execute(), который автоматически находит «привязку» для выполнения.

  • Объект ORM Session поддерживает использование «связанных метаданных» для того, чтобы установить, какие Engine должны использоваться для вызова SQL-операторов от имени конкретного сопоставленного класса, хотя Session также имеет свою собственную явную систему создания сложных конфигураций Engine/сопоставленных классов.

  • Функции MetaData.create_all(), MetaData.drop_all(), Table.create(), Table.drop() и «autoload» используют связанный Engine автоматически, без необходимости передавать его явно.

Примечание

Концепции «связанных метаданных» и «неявного выполнения» не акцентируются в современной SQLAlchemy. Хотя они обеспечивают некоторое удобство, они больше не требуются ни в одном API и никогда не будут необходимы.

В приложениях, где присутствует несколько объектов Engine, каждый из которых логически связан с определенным набором таблиц (т.е. вертикальное разделение), можно использовать технику «связанных метаданных», чтобы отдельные Table могли автоматически ссылаться на соответствующие Engine; в частности, это поддерживается в ORM через объект Session как средство для связывания Table объектов с соответствующими Engine, как альтернатива использованию аргументов связывания, принимаемых непосредственно Session.

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

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

Современное использование SQLAlchemy, особенно ORM, делает большой акцент на работе в контексте транзакции в любое время; концепция «неявного выполнения» значительно усложняет работу по привязке выполнения оператора к конкретной транзакции. Метод Executable.execute() на конкретном операторе SQL обычно подразумевает, что выполнение не является частью какой-либо конкретной транзакции, что обычно не является желаемым эффектом.

В обоих примерах «без соединения» Connection создается за кулисами; CursorResult, возвращаемый вызовом execute(), ссылается на Connection, используемый для выдачи SQL-запроса. Когда CursorResult закрывается, базовый Connection закрывается за нас, в результате чего соединение DBAPI возвращается в пул с удаленными транзакционными ресурсами.

Перевод имен схем

Для поддержки многопользовательских приложений, которые распределяют общие наборы таблиц по нескольким схемам, опция выполнения Connection.execution_options.schema_translate_map может быть использована для переназначения набора объектов Table для отображения под разными именами схем без каких-либо изменений.

Дана таблица:

user_table = Table(
    "user",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
)

Схема» этого Table, определенная атрибутом Table.schema, будет None. Атрибут Connection.execution_options.schema_translate_map может указать, что все объекты Table со схемой None будут вместо этого отображать схему как user_schema_one:

connection = engine.connect().execution_options(
    schema_translate_map={None: "user_schema_one"}
)

result = connection.execute(user_table.select())

Приведенный выше код вызовет SQL на базе данных формы:

SELECT user_schema_one.user.id, user_schema_one.user.name FROM
user_schema_one.user

То есть, имя схемы заменяется нашим переведенным именем. В карте может быть указано любое количество схем target->destination:

connection = engine.connect().execution_options(
    schema_translate_map={
        None: "user_schema_one",  # no schema name -> "user_schema_one"
        "special": "special_schema",  # schema="special" becomes "special_schema"
        "public": None,  # Table objects with schema="public" will render with no schema
    }
)

Параметр Connection.execution_options.schema_translate_map влияет на все конструкции DDL и SQL, созданные на языке выражений SQL, как получено из объектов Table или Sequence. Он не влияет на буквенные строки SQL, используемые через конструкцию text() или через простые строки, передаваемые в Connection.execute().

Эта функция действует только в тех случаях, когда имя схемы непосредственно выводится из имени схемы Table или Sequence; она не влияет на методы, в которых строковое имя схемы передается напрямую. По этой схеме, она действует при проверках «может создавать» / «может уничтожать», выполняемых такими методами, как MetaData.create_all() или MetaData.drop_all(), и действует при использовании отражения таблицы, переданной объекту Table. Однако это не влияет на операции, выполняемые над объектом Inspector, поскольку имя схемы передается этим методам в явном виде.

Совет

Чтобы использовать функцию трансляции схемы с ORM Session, установите этот параметр на уровне Engine, затем передайте этот механизм в Session. В Session для каждой транзакции используется новый Connection:

schema_engine = engine.execution_options(schema_translate_map={...})

session = Session(schema_engine)

...

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

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

Чтобы использовать один Session с несколькими конфигурациями schema_translate_map, можно использовать расширение Горизонтальное разделение. См. пример в Горизонтальное разделение.

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

Кэширование компиляции SQL

Добавлено в версии 1.4: SQLAlchemy теперь имеет прозрачную систему кэширования запросов, которая существенно снижает вычислительные затраты Python на преобразование конструкций SQL-операторов в SQL-строки как в Core, так и в ORM. См. введение на Transparent SQL Compilation Caching added to All DQL, DML Statements in Core, ORM.

SQLAlchemy включает в себя комплексную систему кэширования для компилятора SQL, а также его ORM-вариантов. Эта система кэширования прозрачна в пределах Engine и обеспечивает, что процесс компиляции SQL для данного SQL-компилятора Core или ORM, а также связанные с ним вычисления, которые собирают механику выборки результатов для этого оператора, будут происходить только один раз для этого объекта оператора и всех других с идентичной структурой, в течение всего времени, пока конкретная структура остается в «скомпилированном кэше» движка. Под «объектами утверждений, имеющими идентичную структуру», обычно подразумевается SQL-оператор, который строится в рамках функции и создается каждый раз, когда эта функция выполняется:

def run_my_statement(connection, parameter):
    stmt = select(table)
    stmt = stmt.where(table.c.col == parameter)
    stmt = stmt.order_by(table.c.id)
    return connection.execute(stmt)

Приведенное выше утверждение сгенерирует SQL, напоминающий SELECT id, col FROM table WHERE col = :col ORDER BY id, отмечая, что хотя значение parameter является обычным объектом Python, таким как строка или целое число, строковая SQL-форма утверждения не включает это значение, поскольку использует связанные параметры. Последующие вызовы вышеуказанной функции run_my_statement() будут использовать кэшированную конструкцию компиляции в области видимости вызова connection.execute() для повышения производительности.

Примечание

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

Хотя SQLAlchemy имеет рудиментарный кэш операторов с ранних версий 1.x, а также расширение «Baked Query» для ORM, обе эти системы требовали высокой степени специального использования API для того, чтобы кэш был эффективным. Новый кэш в версии 1.4 является полностью автоматическим и не требует изменения стиля программирования для обеспечения эффективности.

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

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

Сам кэш представляет собой объект типа словаря LRUCache, который является внутренним подклассом словаря SQLAlchemy, который отслеживает использование определенных ключей и имеет периодический шаг «обрезки», который удаляет наименее недавно использованные элементы, когда размер кэша достигает определенного порога. Размер этого кэша по умолчанию равен 500 и может быть настроен с помощью параметра create_engine.query_cache_size:

engine = create_engine("postgresql://scott:tiger@localhost/test", query_cache_size=1200)

Размер кэша может вырасти в 150% от заданного размера, прежде чем он будет сокращен до целевого размера. Таким образом, кэш размером 1200 может вырасти до 1800 элементов, после чего он будет обрезан до 1200.

Размер кэша основан на одной записи для каждого уникального SQL-запроса, созданного на каждом движке. SQL-запросы, генерируемые как ядром, так и ORM, обрабатываются одинаково. Операторы DDL обычно не кэшируются. Для того чтобы определить, что делает кэш, в журнале регистрации движка будут содержаться подробности о поведении кэша, описанные в следующем разделе.

Оценка производительности кэша с помощью протоколирования

Указанный выше размер кэша 1200 на самом деле довольно велик. Для небольших приложений, скорее всего, будет достаточно размера 100. Для оценки оптимального размера кэша, при условии наличия достаточного количества памяти на целевом узле, размер кэша должен быть основан на количестве уникальных строк SQL, которые могут быть отображены для используемого целевого механизма. Наиболее целесообразным способом увидеть это является использование эха SQL, которое наиболее непосредственно включается с помощью флага create_engine.echo или с помощью протоколирования Python; см. раздел Настройка ведения журнала о конфигурации протоколирования.

В качестве примера мы рассмотрим журнал, создаваемый следующей программой:

from sqlalchemy import Column
from sqlalchemy import create_engine
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session

Base = declarative_base()


class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    data = Column(String)
    bs = relationship("B")


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))
    data = Column(String)


e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)

s = Session(e)

s.add_all([A(bs=[B(), B(), B()]), A(bs=[B(), B(), B()]), A(bs=[B(), B(), B()])])
s.commit()

for a_rec in s.query(A):
    print(a_rec.bs)

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

  • [raw sql] - драйвер или конечный пользователь выдал необработанный SQL, используя Connection.exec_driver_sql() - кэширование не применяется

  • [no key] - объект оператора является оператором DDL, который не кэшируется, или объект оператора содержит некэшируемые элементы, такие как определяемые пользователем конструкции или произвольно большие предложения VALUES.

  • [generated in Xs] - утверждение было пропуском кэша и должно было быть скомпилировано, а затем сохранено в кэше. на создание скомпилированной конструкции ушло X секунд. Число X будет в мелких долях секунды.

  • [cached since Xs ago] - утверждение было попаданием в кэш и не требовало перекомпиляции. Утверждение было сохранено в кэше с X секунд назад. Число X будет пропорционально длительности работы приложения и длительности кэширования утверждения, например, 86400 для 24-часового периода.

Каждый бейдж более подробно описан ниже.

Первые утверждения, которые мы увидим для вышеприведенной программы, будут диалектом SQLite, проверяющим существование таблиц «a» и «b»:

INFO sqlalchemy.engine.Engine PRAGMA temp.table_info("a")
INFO sqlalchemy.engine.Engine [raw sql] ()
INFO sqlalchemy.engine.Engine PRAGMA main.table_info("b")
INFO sqlalchemy.engine.Engine [raw sql] ()

Для двух вышеприведенных утверждений SQLite PRAGMA значок читается как [raw sql], что указывает на то, что драйвер отправляет строку Python непосредственно в базу данных с помощью Connection.exec_driver_sql(). Кэширование не применяется к таким утверждениям, поскольку они уже существуют в строковой форме, и ничего не известно о том, какие типы строк результатов будут возвращены, поскольку SQLAlchemy не разбирает строки SQL заранее.

Следующие утверждения, которые мы видим, это утверждения CREATE TABLE:

INFO sqlalchemy.engine.Engine
CREATE TABLE a (
  id INTEGER NOT NULL,
  data VARCHAR,
  PRIMARY KEY (id)
)

INFO sqlalchemy.engine.Engine [no key 0.00007s] ()
INFO sqlalchemy.engine.Engine
CREATE TABLE b (
  id INTEGER NOT NULL,
  a_id INTEGER,
  data VARCHAR,
  PRIMARY KEY (id),
  FOREIGN KEY(a_id) REFERENCES a (id)
)

INFO sqlalchemy.engine.Engine [no key 0.00006s] ()

Для каждого из этих утверждений значок читается как [no key 0.00006s]. Это указывает на то, что в этих двух конкретных утверждениях кэширование не произошло, поскольку DDL-ориентированная конструкция CreateTable не создала ключ кэша. Конструкции DDL обычно не участвуют в кэшировании, потому что они обычно не повторяются во второй раз, а DDL также является этапом конфигурирования базы данных, где производительность не так критична.

Значок [no key] важен еще по одной причине, поскольку он может быть получен для SQL-запросов, которые можно кэшировать, за исключением некоторой определенной подконструкции, которая в настоящее время не является кэшируемой. Примерами этого являются пользовательские элементы SQL, которые не определяют параметры кэширования, а также некоторые конструкции, которые генерируют произвольно длинные и невоспроизводимые строки SQL, основными примерами которых являются конструкция Values, а также при использовании «многозначных вставок» с помощью метода Insert.values().

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

INFO sqlalchemy.engine.Engine INSERT INTO a (data) VALUES (?)
INFO sqlalchemy.engine.Engine [generated in 0.00011s] (None,)
INFO sqlalchemy.engine.Engine INSERT INTO a (data) VALUES (?)
INFO sqlalchemy.engine.Engine [cached since 0.0003533s ago] (None,)
INFO sqlalchemy.engine.Engine INSERT INTO a (data) VALUES (?)
INFO sqlalchemy.engine.Engine [cached since 0.0005326s ago] (None,)
INFO sqlalchemy.engine.Engine INSERT INTO b (a_id, data) VALUES (?, ?)
INFO sqlalchemy.engine.Engine [generated in 0.00010s] (1, None)
INFO sqlalchemy.engine.Engine INSERT INTO b (a_id, data) VALUES (?, ?)
INFO sqlalchemy.engine.Engine [cached since 0.0003232s ago] (1, None)
INFO sqlalchemy.engine.Engine INSERT INTO b (a_id, data) VALUES (?, ?)
INFO sqlalchemy.engine.Engine [cached since 0.0004887s ago] (1, None)

Выше мы видим, по сути, две уникальные SQL-строки: "INSERT INTO a (data) VALUES (?)" и "INSERT INTO b (a_id, data) VALUES (?, ?)". Поскольку SQLAlchemy использует связанные параметры для всех буквальных значений, даже если эти утверждения повторяются много раз для разных объектов, поскольку параметры разделены, фактическая строка SQL остается неизменной.

Примечание

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

Значок кэширования, который мы видим для первого появления каждого из этих двух утверждений, - [generated in 0.00011s]. Это означает, что утверждение не было в кэше, было скомпилировано в строку за .00011 с и затем было кэшировано. Когда мы видим значок [generated], мы знаем, что это означает, что произошел промах в кэше. Этого следует ожидать при первом появлении определенного оператора. Однако, если наблюдается много новых значков [generated] для долго работающего приложения, которое обычно использует одну и ту же серию SQL-запросов снова и снова, это может быть признаком того, что параметр create_engine.query_cache_size слишком мал. Когда оператор, который был кэширован, затем удаляется из кэша из-за обрезки менее используемых элементов кэша LRU, при следующем использовании он отобразит значок [generated].

Значок кэширования, который мы затем видим для последующих вхождений каждого из этих двух утверждений, выглядит как [cached since 0.0003533s ago]. Это означает, что утверждение было найдено в кэше, и первоначально было помещено в кэш .0003533 секунды назад. Важно отметить, что хотя значки [generated] и [cached since] означают количество секунд, они означают разные вещи; в случае [generated] это число является приблизительным отсчетом времени, которое потребовалось для компиляции утверждения, и будет чрезвычайно малым. В случае [cached since] это общее время, в течение которого утверждение находилось в кэше. Для приложения, работающего в течение шести часов, это число может быть [cached since 21600 seconds ago], и это хорошо. Большое число «кэшировано с тех пор» является признаком того, что эти утверждения не подвергались промахам кэша в течение длительного времени. Утверждения, которые часто имеют низкое число «cached since», даже если приложение работает долгое время, могут указывать на то, что эти утверждения слишком часто подвергаются промахам кэша, и что значение create_engine.query_cache_size может быть увеличено.

Затем наша примерная программа выполняет несколько SELECT’ов, где мы видим ту же схему «сгенерировано», затем «кэшировано», для SELECT’а таблицы «a», а также для последующих ленивых загрузок таблицы «b»:

INFO sqlalchemy.engine.Engine SELECT a.id AS a_id, a.data AS a_data
FROM a
INFO sqlalchemy.engine.Engine [generated in 0.00009s] ()
INFO sqlalchemy.engine.Engine SELECT b.id AS b_id, b.a_id AS b_a_id, b.data AS b_data
FROM b
WHERE ? = b.a_id
INFO sqlalchemy.engine.Engine [generated in 0.00010s] (1,)
INFO sqlalchemy.engine.Engine SELECT b.id AS b_id, b.a_id AS b_a_id, b.data AS b_data
FROM b
WHERE ? = b.a_id
INFO sqlalchemy.engine.Engine [cached since 0.0005922s ago] (2,)
INFO sqlalchemy.engine.Engine SELECT b.id AS b_id, b.a_id AS b_a_id, b.data AS b_data
FROM b
WHERE ? = b.a_id

Из нашей вышеприведенной программы, полный запуск показывает, что всего кэшируется четыре различных SQL-строки. Это указывает на то, что размер кэша четыре будет достаточным. Очевидно, что это очень маленький размер, и размер по умолчанию 500 вполне можно оставить по умолчанию.

Сколько памяти использует кэш?

В предыдущем разделе были описаны некоторые методы проверки того, нужно ли увеличивать размер create_engine.query_cache_size. Как узнать, не слишком ли велик кэш? Причина, по которой мы можем захотеть установить create_engine.query_cache_size не выше определенного числа, заключается в том, что у нас есть приложение, которое может использовать очень большое количество различных запросов, например, приложение, которое строит запросы на лету из поискового UX, и мы не хотим, чтобы у нашего хоста закончилась память, если, например, за последние 24 часа было выполнено сто тысяч различных запросов, и все они были закешированы.

Очень трудно измерить, сколько памяти занимают структуры данных Python, однако, используя процесс измерения роста памяти через top по мере добавления в кэш последовательной серии из 250 новых утверждений, можно предположить, что умеренное утверждение Core занимает около 12K, а небольшое утверждение ORM занимает около 20K, включая структуры выборки результатов, которые для ORM будут намного больше.

Отключение или использование альтернативного словаря для кэширования некоторых (или всех) утверждений

Используемый внутренний кэш известен как LRUCache, но в основном это просто словарь. Любой словарь можно использовать в качестве кэша для любой серии утверждений, используя опцию Connection.execution_options.compiled_cache в качестве опции выполнения. Опции выполнения могут быть установлены на операторе, на Engine или Connection, а также при использовании метода ORM Session.execute() для вызовов в стиле SQLAlchemy-2.0. Например, чтобы запустить серию SQL-запросов и кэшировать их в определенном словаре:

my_cache = {}
with engine.connect().execution_options(compiled_cache=my_cache) as conn:
    conn.execute(table.select())

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

Кэш также можно отключить с помощью этого аргумента, отправив значение None:

# disable caching for this connection
with engine.connect().execution_options(compiled_cache=None) as conn:
    conn.execute(table.select())

Кэширование для диалектов сторонних производителей

Функция кэширования требует, чтобы компилятор диалекта создавал SQL-строки, которые безопасно использовать повторно для многих вызовов операторов, учитывая определенный ключ кэша, привязанный к этой SQL-строке. Это означает, что любые буквальные значения в операторе, такие как значения LIMIT/OFFSET для SELECT, не могут быть жестко закодированы в схеме компиляции диалекта, поскольку скомпилированная строка не будет пригодна для повторного использования. SQLAlchemy поддерживает визуализацию связанных параметров с помощью метода BindParameter.render_literal_execute(), который может быть применен к существующим атрибутам Select._limit_clause и Select._offset_clause пользовательским компилятором, что проиллюстрировано далее в этом разделе.

Поскольку существует множество сторонних диалектов, многие из которых могут генерировать литеральные значения из SQL-операторов без использования новой функции «literal execute», SQLAlchemy в версии 1.4.5 добавила атрибут диалектов, известный как Dialect.supports_statement_cache. Этот атрибут проверяется во время выполнения на наличие непосредственно в классе конкретного диалекта, даже если он уже присутствует в суперклассе, так что даже сторонний диалект, который является подклассом существующего диалекта SQLAlchemy с возможностью кэширования, такого как sqlalchemy.dialects.postgresql.PGDialect, все равно должен явно включать этот атрибут, чтобы кэширование было включено. Атрибут должен только быть включен после того, как диалект был изменен по мере необходимости и протестирован на возможность повторного использования скомпилированных SQL-запросов с различными параметрами.

Для всех сторонних диалектов, которые не поддерживают этот атрибут, в журнале регистрации для такого диалекта будет указано dialect does not support caching.

Если диалект был протестирован на кэширование, и, в частности, компилятор SQL был обновлен, чтобы не отображать любой литерал LIMIT / OFFSET в строке SQL напрямую, авторы диалекта могут применять атрибут следующим образом:

from sqlalchemy.engine.default import DefaultDialect


class MyDialect(DefaultDialect):
    supports_statement_cache = True

Флаг должен применяться также ко всем подклассам диалекта:

class MyDBAPIForMyDialect(MyDialect):
    supports_statement_cache = True

Добавлено в версии 1.4.5: Добавлен атрибут Dialect.supports_statement_cache.

Типичный случай для модификации диалекта следующий.

Пример: Рендеринг LIMIT / OFFSET с параметрами после компиляции

В качестве примера, предположим, что диалект переопределяет метод SQLCompiler.limit_clause(), который создает предложение «LIMIT / OFFSET» для оператора SQL, например, так:

# pre 1.4 style code
def limit_clause(self, select, **kw):
    text = ""
    if select._limit is not None:
        text += " \n LIMIT %d" % (select._limit,)
    if select._offset is not None:
        text += " \n OFFSET %d" % (select._offset,)
    return text

Приведенная выше процедура выдает целочисленные значения Select._limit и Select._offset как буквальные целые числа, встроенные в оператор SQL. Это обычное требование для баз данных, которые не поддерживают использование связанного параметра в пунктах LIMIT/OFFSET оператора SELECT. Однако отображение целочисленного значения на начальном этапе компиляции напрямую несовместимо с кэшированием, поскольку целочисленные значения ограничения и смещения объекта Select не являются частью ключа кэша, поэтому многие операторы Select с различными значениями ограничения/смещения не будут отображаться с правильным значением.

Исправление приведенного выше кода заключается в перемещении литерального целого числа в средство SQLAlchemy post-compile, которое отображает литеральное целое число вне начальной стадии компиляции, а во время выполнения до отправки оператора в DBAPI. Доступ к нему осуществляется на этапе компиляции с помощью метода BindParameter.render_literal_execute() в сочетании с использованием атрибутов Select._limit_clause и Select._offset_clause, которые представляют LIMIT/OFFSET как полное SQL-выражение, следующим образом:

# 1.4 cache-compatible code
def limit_clause(self, select, **kw):
    text = ""

    limit_clause = select._limit_clause
    offset_clause = select._offset_clause

    if select._simple_int_clause(limit_clause):
        text += " \n LIMIT %s" % (
            self.process(limit_clause.render_literal_execute(), **kw)
        )
    elif limit_clause is not None:
        # assuming the DB doesn't support SQL expressions for LIMIT.
        # Otherwise render here normally
        raise exc.CompileError(
            "dialect 'mydialect' can only render simple integers for LIMIT"
        )
    if select._simple_int_clause(offset_clause):
        text += " \n OFFSET %s" % (
            self.process(offset_clause.render_literal_execute(), **kw)
        )
    elif offset_clause is not None:
        # assuming the DB doesn't support SQL expressions for OFFSET.
        # Otherwise render here normally
        raise exc.CompileError(
            "dialect 'mydialect' can only render simple integers for OFFSET"
        )

    return text

Приведенный выше подход создаст скомпилированный оператор SELECT, который будет выглядеть следующим образом:

SELECT x FROM y
LIMIT __[POSTCOMPILE_param_1]
OFFSET __[POSTCOMPILE_param_2]

Там, где указано выше, индикаторы __[POSTCOMPILE_param_1] и __[POSTCOMPILE_param_2] будут заполнены соответствующими целочисленными значениями во время выполнения оператора, после того как строка SQL будет извлечена из кэша.

После внесения изменений, подобных вышеуказанным, флаг Dialect.supports_statement_cache должен быть установлен на True. Настоятельно рекомендуется, чтобы диалекты сторонних разработчиков использовали флаг dialect third party test suite, который гарантирует, что такие операции, как SELECT с LIMIT/OFFSET, будут правильно отображаться и кэшироваться.

Использование ламбдас для значительного увеличения скорости создания операторов

Deep Alchemy

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

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

Функция лямбда-выражения SQL доступна в качестве функции повышения производительности, а также опционально используется в опции with_loader_criteria() ORM для предоставления общего фрагмента SQL.

Синопсис

Лямбда-выражения строятся с помощью функции lambda_stmt(), которая возвращает экземпляр StatementLambdaElement, который сам по себе является исполняемой конструкцией выражения. Дополнительные модификаторы и критерии добавляются к объекту с помощью оператора сложения Python + или альтернативного метода StatementLambdaElement.add_criteria(), который позволяет использовать больше вариантов.

Предполагается, что конструкция lambda_stmt() вызывается внутри объемлющей функции или метода, которые предполагается использовать много раз в приложении, так что последующие выполнения, кроме первого, могут воспользоваться преимуществами кэширования скомпилированного SQL. Когда лямбда строится внутри объемлющей функции в Python, она также должна иметь закрывающие переменные, которые важны для всего подхода:

from sqlalchemy import lambda_stmt


def run_my_statement(connection, parameter):
    stmt = lambda_stmt(lambda: select(table))
    stmt += lambda s: s.where(table.c.col == parameter)
    stmt += lambda s: s.order_by(table.c.id)

    return connection.execute(stmt)


with engine.connect() as conn:
    result = run_my_statement(some_connection, "some parameter")

Выше, три вызываемые функции lambda, которые используются для определения структуры оператора SELECT, вызываются ровно один раз, а полученная строка SQL кэшируется в кэше компиляции движка. С этого момента функция run_my_statement() может быть вызвана любое количество раз, а входящие в нее вызываемые переменные lambda не будут вызываться, а будут использоваться только как ключи кэша для получения уже скомпилированного SQL.

Примечание

Важно отметить, что кэширование SQL уже существует, когда система лямбда не используется. Система лямбд лишь добавляет дополнительный уровень сокращения работы на каждый вызываемый SQL-оператор за счет кэширования построения самой SQL-конструкции, а также использования более простого ключа кэша.

Краткое руководство по ламбдам

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

  • Поддерживается любой тип утверждений - хотя предполагается, что конструкции select() являются основным примером использования lambda_stmt(), DML утверждения, такие как insert() и update(), также могут быть использованы:

    def upd(id_, newname):
        stmt = lambda_stmt(lambda: users.update())
        stmt += lambda s: s.values(name=newname)
        stmt += lambda s: s.where(users.c.id == id_)
        return stmt
    
    
    with engine.begin() as conn:
        conn.execute(upd(7, "foo"))
  • Случаи прямого использования также поддерживаются - lambda_stmt() может полностью вмещать функциональность ORM и использоваться непосредственно с Session.execute():

    def select_user(session, name):
        stmt = lambda_stmt(lambda: select(User))
        stmt += lambda s: s.where(User.name == name)
    
        row = session.execute(stmt).first()
        return row
  • Связанные параметры учитываются автоматически - в отличие от предыдущей системы «запеченных запросов» SQLAlchemy, система лямбда SQL учитывает буквенные значения Python, которые становятся связанными параметрами SQL автоматически. Это означает, что даже если данная лямбда выполняется только один раз, значения, которые становятся связанными параметрами, извлекаются из закрытия лямбды при каждом выполнении:

    >>> def my_stmt(x, y):
    ...     stmt = lambda_stmt(lambda: select(func.max(x, y)))
    ...     return stmt
    >>> engine = create_engine("sqlite://", echo=True)
    >>> with engine.connect() as conn:
    ...     print(conn.scalar(my_stmt(5, 10)))
    ...     print(conn.scalar(my_stmt(12, 8)))
    
    SELECT max(?, ?) AS max_1 [generated in 0.00057s] (5, 10)
    10
    SELECT max(?, ?) AS max_1 [cached since 0.002059s ago] (12, 8)
    12

    Выше, StatementLambdaElement извлек значения x и y из закрытия лямбды, которая генерируется каждый раз, когда вызывается my_stmt(); они были подставлены в кэшированную конструкцию SQL в качестве значений параметров.

  • В идеале лямбда должна создавать идентичную структуру SQL во всех случаях - Избегайте использования условий или пользовательских callables внутри лямбд, которые могут заставить ее создавать различные SQL на основе входных данных; если функция может условно использовать два различных фрагмента SQL, используйте две отдельные лямбды:

    # **Don't** do this:
    
    def my_stmt(parameter, thing=False):
        stmt = lambda_stmt(lambda: select(table))
        stmt += (
            lambda s: s.where(table.c.x > parameter) if thing
            else s.where(table.c.y == parameter)
        return stmt
    
    # **Do** do this:
    
    def my_stmt(parameter, thing=False):
        stmt = lambda_stmt(lambda: select(table))
        if thing:
            stmt += lambda s: s.where(table.c.x > parameter)
        else:
            stmt += lambda s: s.where(table.c.y == parameter)
        return stmt

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

  • Не используйте функции внутри лямбды для создания связанных значений - подход отслеживания связанных значений требует, чтобы фактическое значение, которое будет использоваться в SQL-операторе, локально присутствовало в закрытии лямбды. Это невозможно, если значения генерируются из других функций, и LambdaElement обычно должен вызывать ошибку при такой попытке:

    >>> def my_stmt(x, y):
    ...     def get_x():
    ...         return x
    ...
    ...     def get_y():
    ...         return y
    ...
    ...     stmt = lambda_stmt(lambda: select(func.max(get_x(), get_y())))
    ...     return stmt
    >>> with engine.connect() as conn:
    ...     print(conn.scalar(my_stmt(5, 10)))
    Traceback (most recent call last):
      # ...
    sqlalchemy.exc.InvalidRequestError: Can't invoke Python callable get_x()
    inside of lambda expression argument at
    <code object <lambda> at 0x7fed15f350e0, file "<stdin>", line 6>;
    lambda SQL constructs should not invoke functions from closure variables
    to produce literal values since the lambda SQL system normally extracts
    bound values without actually invoking the lambda or any functions within it.

    Выше, использование get_x() и get_y(), если они необходимы, должно происходить вне лямбды и присваиваться локальной закрывающей переменной:

    >>> def my_stmt(x, y):
    ...     def get_x():
    ...         return x
    ...
    ...     def get_y():
    ...         return y
    ...
    ...     x_param, y_param = get_x(), get_y()
    ...     stmt = lambda_stmt(lambda: select(func.max(x_param, y_param)))
    ...     return stmt
  • Избегайте ссылаться на не-SQL конструкции внутри лямбд, поскольку они не кэшируются по умолчанию - этот вопрос относится к тому, как LambdaElement создает ключ кэша из других переменных закрытия в операторе. Для того чтобы обеспечить наилучшую гарантию точного ключа кэша, все объекты, находящиеся в закрытии лямбды, считаются значимыми, и ни один из них не будет считаться подходящим для ключа кэша по умолчанию. Поэтому в следующем примере также будет выдано довольно подробное сообщение об ошибке:

    >>> class Foo:
    ...     def __init__(self, x, y):
    ...         self.x = x
    ...         self.y = y
    >>> def my_stmt(foo):
    ...     stmt = lambda_stmt(lambda: select(func.max(foo.x, foo.y)))
    ...     return stmt
    >>> with engine.connect() as conn:
    ...     print(conn.scalar(my_stmt(Foo(5, 10))))
    Traceback (most recent call last):
      # ...
    sqlalchemy.exc.InvalidRequestError: Closure variable named 'foo' inside of
    lambda callable <code object <lambda> at 0x7fed15f35450, file
    "<stdin>", line 2> does not refer to a cacheable SQL element, and also
    does not appear to be serving as a SQL literal bound value based on the
    default SQL expression returned by the function.  This variable needs to
    remain outside the scope of a SQL-generating lambda so that a proper cache
    key may be generated from the lambda's state.  Evaluate this variable
    outside of the lambda, set track_on=[<elements>] to explicitly select
    closure elements to track, or set track_closure_variables=False to exclude
    closure variables from being part of the cache key.

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

    Лучший способ разрешить описанную выше ситуацию - не ссылаться на foo внутри лямбды, а ссылаться на нее вне:

    >>> def my_stmt(foo):
    ...     x_param, y_param = foo.x, foo.y
    ...     stmt = lambda_stmt(lambda: select(func.max(x_param, y_param)))
    ...     return stmt

    В некоторых ситуациях, если гарантируется, что SQL-структура лямбды никогда не изменится на основе ввода, передать track_closure_variables=False, что отключит любое отслеживание закрывающих переменных, кроме тех, которые используются для связанных параметров:

    >>> def my_stmt(foo):
    ...     stmt = lambda_stmt(
    ...         lambda: select(func.max(foo.x, foo.y)), track_closure_variables=False
    ...     )
    ...     return stmt

    Существует также возможность добавить объекты в элемент, чтобы явно сформировать часть ключа кэша, используя параметр track_on; использование этого параметра позволяет определенным значениям служить в качестве ключа кэша, а также предотвращает рассмотрение других переменных закрытия. Это полезно для случаев, когда часть конструируемого SQL берет начало от какого-либо контекстного объекта, который может иметь множество различных значений. В приведенном ниже примере первый сегмент оператора SELECT отключает отслеживание переменной foo, тогда как второй сегмент явно отслеживает self как часть ключа кэша:

    >>> def my_stmt(self, foo):
    ...     stmt = lambda_stmt(
    ...         lambda: select(*self.column_expressions), track_closure_variables=False
    ...     )
    ...     stmt = stmt.add_criteria(lambda: self.where_criteria, track_on=[self])
    ...     return stmt

    Использование track_on означает, что заданные объекты будут долго храниться во внутреннем кэше лямбды и будут иметь сильные ссылки до тех пор, пока кэш не очистится от этих объектов (по умолчанию используется схема LRU из 1000 записей).

Генерация ключей кэша

Для понимания некоторых опций и поведения, которые возникают при использовании лямбда-конструкций SQL, полезно понимание системы кэширования.

Система кэширования SQLAlchemy обычно генерирует ключ кэша из заданной конструкции SQL-выражения, создавая структуру, которая представляет все состояние внутри конструкции:

>>> from sqlalchemy import select, column
>>> stmt = select(column("q"))
>>> cache_key = stmt._generate_cache_key()
>>> print(cache_key)  # somewhat paraphrased
CacheKey(key=(
  '0',
  <class 'sqlalchemy.sql.selectable.Select'>,
  '_raw_columns',
  (
    (
      '1',
      <class 'sqlalchemy.sql.elements.ColumnClause'>,
      'name',
      'q',
      'type',
      (
        <class 'sqlalchemy.sql.sqltypes.NullType'>,
      ),
    ),
  ),
  # a few more elements are here, and many more for a more
  # complicated SELECT statement
),)

Указанный выше ключ хранится в кэше, который по сути является словарем, а значение - это конструкция, которая, помимо прочего, хранит строковую форму SQL-запроса, в данном случае фразу «SELECT q». Мы можем заметить, что даже для очень короткого запроса ключ кэша довольно многословен, поскольку он должен представлять все, что может варьироваться о том, что отображается и потенциально выполняется.

Система построения лямбд, напротив, создает другой вид ключа кэша:

>>> from sqlalchemy import lambda_stmt
>>> stmt = lambda_stmt(lambda: select(column("q")))
>>> cache_key = stmt._generate_cache_key()
>>> print(cache_key)
CacheKey(key=(
  <code object <lambda> at 0x7fed1617c710, file "<stdin>", line 1>,
  <class 'sqlalchemy.sql.lambdas.StatementLambdaElement'>,
),)

Выше мы видим, что ключ кэша значительно короче, чем у утверждения без лямбды, и, кроме того, производство самой конструкции select(column("q")) даже не было необходимым; сама лямбда Python содержит атрибут __code__, который ссылается на объект кода Python, который во время выполнения приложения является неизменяемым и постоянным.

Когда лямбда также включает закрывающие переменные, в обычном случае, когда эти переменные ссылаются на конструкции SQL, такие как объекты столбцов, они становятся частью ключа кэша, или если они ссылаются на литеральные значения, которые будут связанными параметрами, они помещаются в отдельный элемент ключа кэша:

>>> def my_stmt(parameter):
...     col = column("q")
...     stmt = lambda_stmt(lambda: select(col))
...     stmt += lambda s: s.where(col == parameter)
...     return stmt

Приведенный выше StatementLambdaElement включает две ламбды, обе из которых ссылаются на закрывающую переменную col, поэтому ключ кэша будет представлять оба этих сегмента, а также объект column():

>>> stmt = my_stmt(5)
>>> key = stmt._generate_cache_key()
>>> print(key)
CacheKey(key=(
  <code object <lambda> at 0x7f07323c50e0, file "<stdin>", line 3>,
  (
    '0',
    <class 'sqlalchemy.sql.elements.ColumnClause'>,
    'name',
    'q',
    'type',
    (
      <class 'sqlalchemy.sql.sqltypes.NullType'>,
    ),
  ),
  <code object <lambda> at 0x7f07323c5190, file "<stdin>", line 4>,
  <class 'sqlalchemy.sql.lambdas.LinkedLambdaElement'>,
  (
    '0',
    <class 'sqlalchemy.sql.elements.ColumnClause'>,
    'name',
    'q',
    'type',
    (
      <class 'sqlalchemy.sql.sqltypes.NullType'>,
    ),
  ),
  (
    '0',
    <class 'sqlalchemy.sql.elements.ColumnClause'>,
    'name',
    'q',
    'type',
    (
      <class 'sqlalchemy.sql.sqltypes.NullType'>,
    ),
  ),
),)

Вторая часть кэш-ключа извлекает связанные параметры, которые будут использоваться при вызове оператора:

>>> key.bindparams
[BindParameter('%(139668884281280 parameter)s', 5, type_=Integer())]

Для серии примеров кэширования «лямбда» со сравнением производительности, смотрите набор тестов «short_selects» в примере производительности Производительность.

Утилизация двигателей

Engine ссылается на пул соединений, что означает, что при нормальных обстоятельствах существуют открытые соединения с базой данных, пока объект Engine все еще находится в памяти. Когда Engine собирается в мусор, его пул соединений больше не упоминается этим Engine, и если ни одно из его соединений все еще не проверено, пул и его соединения также будут собраны в мусор, что приведет к закрытию фактических соединений с базой данных. Но в противном случае Engine будет удерживать открытые соединения с базой данных, если он использует обычно стандартную реализацию пула по умолчанию QueuePool.

Реестр Engine предназначен для того, чтобы быть постоянным элементом, созданным заранее и поддерживаемым в течение всего срока службы приложения. Он **не предназначен для создания и удаления каждого соединения; вместо этого он является реестром, который поддерживает пул соединений, а также конфигурационную информацию об используемой базе данных и DBAPI, а также некоторую степень внутреннего кэширования ресурсов каждой базы данных.

Однако существует множество случаев, когда желательно, чтобы все ресурсы соединения, на которые ссылается Engine, были полностью закрыты. Обычно в таких случаях не стоит полагаться на сборку мусора в Python; вместо этого Engine можно явно утилизировать с помощью метода Engine.dispose(). При этом утилизируется базовый пул соединений движка и заменяется новым, пустым. Если в этот момент Engine будет удален и больше не будет использоваться, все зарегистрированные соединения, на которые он ссылается, также будут полностью закрыты.

Важные случаи использования вызова Engine.dispose() включают:

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

  • Когда программа использует многопроцессорность или fork(), и объект Engine копируется в дочерний процесс, следует вызвать Engine.dispose(), чтобы движок создал совершенно новые соединения базы данных, локальные для этого форка. Соединения баз данных обычно не пересекают границы процессов. В этом случае используйте параметр Engine.dispose.close, установленный в False. Подробнее об этом случае см. в разделе Использование пулов соединений с многопроцессорной обработкой или os.fork().

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

Соединения, которые отмечены, не отбрасываются при утилизации или сборке мусора, поскольку эти соединения все еще активно используются приложением. Однако после вызова Engine.dispose() эти соединения больше не связаны с этим Engine; когда они будут закрыты, они будут возвращены в их теперь уже орфанный пул соединений, который в конечном итоге будет собран в мусор, когда все соединения, ссылающиеся на него, также перестанут на него ссылаться. Поскольку этот процесс нелегко контролировать, настоятельно рекомендуется вызывать Engine.dispose() только после того, как все проверенные соединения будут зарегистрированы или иным образом деассоциированы из их пула.

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

Работа с драйверами SQL и необработанными соединениями DBAPI

Во введении об использовании Connection.execute() использовалась конструкция text(), чтобы проиллюстрировать, как можно вызывать текстовые операторы SQL. При работе с SQLAlchemy текстовый SQL является скорее исключением, чем нормой, поскольку язык выражений Core и ORM абстрагируются от текстового представления SQL. Однако сама конструкция text() также обеспечивает некоторую абстракцию текстового SQL, поскольку она нормализует передачу связанных параметров, а также поддерживает поведение типизации данных для параметров и строк набора результатов.

Вызов строк SQL непосредственно драйвером

Для случая, когда необходимо вызвать текстовый SQL, непосредственно переданный базовому драйверу (известный как DBAPI) без вмешательства конструкции text(), можно использовать метод Connection.exec_driver_sql():

with engine.connect() as conn:
    conn.exec_driver_sql("SET param='bar'")

Добавлено в версии 1.4: Добавлен метод Connection.exec_driver_sql().

Работа с курсором DBAPI напрямую

В некоторых случаях SQLAlchemy не предоставляет обобщенного способа доступа к некоторым функциям DBAPI, таким как вызов хранимых процедур, а также работа с несколькими наборами результатов. В этих случаях целесообразно работать с необработанным DBAPI-соединением напрямую.

Наиболее распространенный способ доступа к необработанному соединению DBAPI - получить его из уже присутствующего объекта Connection напрямую. Он присутствует с помощью атрибута Connection.connection:

connection = engine.connect()
dbapi_conn = connection.connection

DBAPI-соединение здесь фактически является «проксированным» с точки зрения пула исходных соединений, однако это деталь реализации, которую в большинстве случаев можно игнорировать. Поскольку это DBAPI-соединение по-прежнему находится в области видимости объекта-владельца Connection, лучше всего использовать объект Connection для большинства функций, таких как управление транзакциями, а также вызов метода Connection.close(); если эти операции выполняются непосредственно на DBAPI-соединении, объект-владелец Connection не будет знать об этих изменениях в состоянии.

Чтобы преодолеть ограничения, накладываемые соединением DBAPI, которое поддерживается владельцем Connection, соединение DBAPI также доступно без необходимости сначала приобретать Connection, используя метод Engine.raw_connection() из Engine:

dbapi_conn = engine.raw_connection()

Это соединение DBAPI, как и в предыдущем случае, является «проксированной» формой. Цель этого проксирования теперь очевидна, поскольку когда мы вызываем метод .close() этого соединения, соединение DBAPI обычно не закрывается, а вместо этого released возвращается в пул соединений движка:

dbapi_conn.close()

Хотя SQLAlchemy может в будущем добавить встроенные паттерны для большего количества случаев использования DBAPI, отдача от них снижается, так как эти случаи, как правило, нужны редко, и они также сильно зависят от типа используемого DBAPI, поэтому в любом случае паттерн прямого вызова DBAPI всегда доступен для тех случаев, когда он необходим.

См.также

Как получить необработанное соединение DBAPI при использовании Engine? - включает дополнительные подробности о том, как осуществляется доступ к соединению DBAPI, а также к соединению «драйвера» при использовании драйверов asyncio.

Ниже приведены некоторые рецепты использования соединений DBAPI.

Вызов хранимых процедур и функций, определяемых пользователем

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

Для хранимых процедур или функций с особыми синтаксическими особенностями или параметрами потенциально может быть использован уровень DBAPI callproc с вашим DBAPI. Примером такого шаблона является:

connection = engine.raw_connection()
try:
    cursor_obj = connection.cursor()
    cursor_obj.callproc("my_procedure", ["x", "y", "z"])
    results = list(cursor_obj.fetchall())
    cursor_obj.close()
    connection.commit()
finally:
    connection.close()

Примечание

Не все DBAPI используют callproc, и общие детали использования будут отличаться. Приведенный выше пример является лишь иллюстрацией того, как может выглядеть использование той или иной функции DBAPI.

Ваш DBAPI может не иметь требования callproc или может требовать, чтобы хранимая процедура или функция, определяемая пользователем, вызывалась с другим шаблоном, например, с обычным использованием соединения SQLAlchemy. Одним из примеров такого использования является, на момент написания этой документации, выполнение хранимой процедуры в базе данных PostgreSQL с помощью psycopg2 DBAPI, которая должна вызываться с обычным использованием соединения:

connection.execute("CALL my_procedure();")

Приведенный выше пример является гипотетическим. Не гарантируется, что базовая база данных поддерживает «CALL» или «SELECT» в этих ситуациях, и ключевое слово может меняться в зависимости от того, является ли функция хранимой процедурой или функцией, определенной пользователем. В таких ситуациях следует обратиться к документации по DBAPI и базе данных, чтобы определить правильный синтаксис и шаблоны для использования.

Множественные наборы результатов

Поддержка нескольких наборов результатов доступна из необработанного курсора DBAPI с помощью метода nextset:

connection = engine.raw_connection()
try:
    cursor_obj = connection.cursor()
    cursor_obj.execute("select * from table1; select * from table2")
    results_one = cursor_obj.fetchall()
    cursor_obj.nextset()
    results_two = cursor_obj.fetchall()
    cursor_obj.close()
finally:
    connection.close()

Регистрация новых диалектов

Вызов функции create_engine() определяет местонахождение заданного диалекта с помощью точек входа setuptools. Эти точки входа могут быть установлены для диалектов сторонних разработчиков в сценарии setup.py. Например, чтобы создать новый диалект «foodialect://», необходимо выполнить следующие действия:

  1. Создайте пакет под названием foodialect.

  2. В пакете должен быть модуль, содержащий класс диалекта, который обычно является подклассом sqlalchemy.engine.default.DefaultDialect. В этом примере допустим, что он называется FooDialect и доступ к его модулю осуществляется через foodialect.dialect.

  3. Точка входа может быть установлена в файле setup.py следующим образом:

    entry_points = """
    [sqlalchemy.dialects]
    foodialect = foodialect.dialect:FooDialect
    """

Если диалект обеспечивает поддержку определенного DBAPI поверх существующей базы данных, поддерживаемой SQLAlchemy, имя может быть дано с учетом квалификации базы данных. Например, если бы FooDialect был диалектом MySQL, то точка входа могла бы быть установлена следующим образом:

entry_points = """
[sqlalchemy.dialects]
mysql.foodialect = foodialect.dialect:FooDialect
"""

Тогда доступ к вышеуказанной точке входа будет осуществляться как create_engine("mysql+foodialect://").

Регистрация диалектов в процессе

SQLAlchemy также позволяет регистрировать диалект в текущем процессе, минуя необходимость отдельной установки. Используйте функцию register() следующим образом:

from sqlalchemy.dialects import registry

registry.register("mysql.foodialect", "myapp.dialect", "MyMySQLDialect")

Вышеприведенное ответит на create_engine("mysql+foodialect://") и загрузит класс MyMySQLDialect из модуля myapp.dialect.

Подключение / API двигателя

API набора результатов

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