Что нового в SQLAlchemy 1.4?

О данном документе

В данном документе описаны изменения между SQLAlchemy версии 1.3 и SQLAlchemy версии 1.4.

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

О текущем состоянии SQLAlchemy 2.0 см. в разделе SQLAlchemy 2.0 - руководство по миграции.

Основные изменения и возможности API - Общие сведения

Python 3.6 - минимальная версия Python 3; Python 2.7 по-прежнему поддерживается

Поскольку Python 3.5 выйдет из эксплуатации в сентябре 2020 года, в SQLAlchemy 1.4 в качестве минимальной версии Python 3 теперь используется версия 3.6. Python 2.7 по-прежнему поддерживается, однако серия SQLAlchemy 1.4 станет последней серией, поддерживающей Python 2.

ORM Query внутренне унифицирован с select, update, delete; доступно выполнение в стиле 2.0

Самым большим концептуальным изменением SQLAlchemy в версии 2.0, а по существу и в 1.4, является то, что устранено большое разделение между конструкцией Select в Core и объектом Query в ORM, а также между методами Query.update() и Query.delete() в их отношении к Update и Delete.

Что касается Select и Query, то эти два объекта на протяжении многих версий имели схожие, во многом пересекающиеся API и даже некоторую возможность перехода от одного к другому, оставаясь при этом совершенно разными по своим моделям использования и поведению. Исторически это объясняется тем, что объект Query был введен для устранения недостатков объекта Select, который лежал в основе запросов к объектам ORM, за исключением того, что они должны были запрашиваться только в терминах метаданных Table. Однако Query имел лишь упрощенный интерфейс для загрузки объектов, и только с течением многих крупных релизов он приобрел большую часть гибкости объекта Select, что привело к постоянной неловкости, когда эти два объекта стали очень похожими, но все еще в значительной степени несовместимыми друг с другом.

В версии 1.4 все операторы Core и ORM SELECT выполняются непосредственно из объекта Select; когда используется объект Query, во время вызова оператора он копирует свое состояние в объект Select, который затем вызывается изнутри с помощью исполнения 2.0 style. В дальнейшем объект Query станет только унаследованным, и приложениям будет рекомендовано перейти на исполнение 2.0 style, что позволит свободно использовать конструкции Core против сущностей ORM:

with Session(engine, future=True) as sess:
    stmt = (
        select(User)
        .where(User.name == "sandy")
        .join(User.addresses)
        .where(Address.email_address.like("%gmail%"))
    )

    result = sess.execute(stmt)

    for user in result.scalars():
        print(user)

Следует обратить внимание на приведенный пример:

Во всей документации SQLAlchemy будут встречаться ссылки на выполнение 1.x style и 2.0 style. Это делается для того, чтобы провести различие между двумя стилями выполнения запросов и попытаться документировать новый стиль вызова в дальнейшем. В SQLAlchemy 2.0, хотя объект Query может остаться в качестве унаследованной конструкции, он больше не будет фигурировать в большинстве документации.

Аналогичные изменения были внесены и в «массовые обновления и удаления»: теперь для массовых операций можно использовать ядра update() и delete(). Массовое обновление выглядит следующим образом:

session.query(User).filter(User.name == "sandy").update(
    {"password": "foobar"}, synchronize_session="fetch"
)

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

with Session(engine, future=True) as sess:
    stmt = (
        update(User)
        .where(User.name == "sandy")
        .values(password="foobar")
        .execution_options(synchronize_session="fetch")
    )

    sess.execute(stmt)

Обратите внимание на использование метода Executable.execution_options() для передачи опций, связанных с ORM. В настоящее время использование «опций выполнения» стало гораздо более распространенным как в Core, так и в ORM, и многие методы, связанные с ORM, из Query теперь реализуются как опции выполнения (примеры см. в Query.execution_options()).

#5159

ORM Session.execute() во всех случаях использует наборы Result в стиле «будущее»

Как отмечалось в RowProxy больше не является «прокси»; теперь он называется Row и ведет себя как расширенный именованный кортеж, объекты Result и Row теперь имеют поведение «именованного кортежа», когда используются с Engine, включающим параметр create_engine.future, установленный в True. Эти строки «именованного кортежа», в частности, включают в себя изменение поведения, которое заключается в том, что содержащие выражения Python, использующие in, такие как:

>>> engine = create_engine("...", future=True)
>>> conn = engine.connect()
>>> row = conn.execute.first()
>>> "name" in row
True

Приведенный выше тест на сдерживание будет использовать значение сдерживания, а не ключ сдерживания; row должен иметь значение «name», чтобы вернуть True.

В SQLAlchemy 1.4, когда параметр create_engine.future установлен в значение False, возвращаются объекты LegacyRow в стиле legacy, которые имеют поведение частичного именованного кортежа, характерное для предыдущих версий SQLAlchemy, где проверка содержимого продолжает использовать содержимое ключа; "name" in row возвращает True, если в строке есть колонка с именем «name», а не значение.

При использовании Session.execute() полный стиль именованных кортежей включается безусловно, то есть "name" in row будет использовать значение containment в качестве теста, а не key containment. Это сделано для того, чтобы учесть, что Session.execute() теперь возвращает Result, что также учитывает результаты ORM, где даже старые строки результатов ORM, такие как те, которые возвращаются Query.all(), используют сдерживание значений.

Это изменение в поведении по сравнению с SQLAlchemy 1.3 и 1.4. Чтобы продолжать получать коллекции, содержащие ключи, используйте метод Result.mappings() для получения MappingResult, который возвращает строки в виде словарей:

for dict_row in session.execute(text("select id from table")).mappings():
    assert "id" in dict_row

Прозрачное кэширование компиляции SQL добавлено ко всем операциям DQL, DML в ядре, ORM

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

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

Чтобы представить общую идею этой функции, приведем код из набора Производительность, который будет вызывать очень простой запрос «n» раз, для значения по умолчанию n=10000. Запрос возвращает только одну строку, так как накладные расходы, которые мы хотим уменьшить, связаны с многими небольшими запросами. Для запросов, возвращающих много строк, оптимизация не столь существенна:

session = Session(bind=engine)
for id_ in random.sample(ids, n):
    result = session.query(Customer).filter(Customer.id == id_).one()

Этот пример в версии 1.3 SQLAlchemy на компьютере Dell XPS13 под управлением Linux завершается следующим образом:

test_orm_query : (10000 iterations); total time 3.440652 sec

В 1.4 приведенный выше код без модификации завершается:

test_orm_query : (10000 iterations); total time 2.367934 sec

Этот первый тест показывает, что обычные ORM-запросы при использовании кэширования могут выполняться в течение многих итераций в пределах 30% быстрее.

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

Использование этого API выглядит следующим образом:

session = Session(bind=engine)
for id_ in random.sample(ids, n):
    stmt = lambda_stmt(lambda: future_select(Customer))
    stmt += lambda s: s.where(Customer.id == id_)
    session.execute(stmt).scalar_one()

Приведенный выше код завершается:

test_orm_query_newstyle_w_lambdas : (10000 iterations); total time 1.247092 sec

Этот тест показывает, что использование нового стиля запросов ORM «select()» в сочетании с полным вызовом в стиле «baked», который кэширует всю конструкцию, может работать на протяжении многих итераций в диапазоне 60% быстрее и обеспечивает производительность примерно такую же, как и система запросов «baked», которая теперь вытеснена системой собственного кэширования.

Новая система использует существующий вариант выполнения Connection.execution_options.compiled_cache, а также добавляет непосредственно кэш Engine, который настраивается с помощью параметра Engine.query_cache_size.

Значительная часть изменений в API и поведении в версии 1.4 была сделана для поддержки этой новой возможности.

#4639 #5380 #4645 #4808 #5004

Декларативность теперь интегрирована в ORM с новыми возможностями

После десяти лет популярности пакет sqlalchemy.ext.declarative теперь интегрирован в пространство имен sqlalchemy.orm, за исключением декларативных «расширительных» классов, которые остались в виде расширений Declarative.

В число новых классов, добавленных в sqlalchemy.orm, входят:

  • registry - новый класс, заменяющий роль «декларативного базового» класса, служащий реестром сопоставленных классов, на которые можно ссылаться по строковому имени в вызовах relationship(), и не зависящий от стиля, в котором был сопоставлен тот или иной класс.

  • declarative_base() - это тот же декларативный базовый класс, который использовался на протяжении всего времени существования декларативной системы, только теперь он внутренне ссылается на объект registry и реализуется методом registry.generate_base(), который может быть вызван из registry напрямую. Функция declarative_base() создает этот реестр автоматически, поэтому никакого влияния на существующий код не оказывается. Имя sqlalchemy.ext.declarative.declarative_base по-прежнему присутствует, выдавая предупреждение об устаревании версии 2.0, когда функция 2.0 deprecations mode включена.

  • declared_attr() - тот же вызов функции «declared attr» теперь является частью sqlalchemy.orm. Имя sqlalchemy.ext.declarative.declared_attr по-прежнему присутствует, выдавая предупреждение об устаревании версии 2.0 при включении 2.0 deprecations mode.

  • Другие имена, перемещенные в sqlalchemy.orm, включают has_inherited_table(), synonym_for(), DeclarativeMeta, as_declarative().

Кроме того, функция instrument_declarative() устарела и заменена на registry.map_declaratively(). Классы ConcreteBase, AbstractConcreteBase и DeferredReflection остаются в качестве расширений в пакете Декларативные расширения.

В настоящее время стили отображения организованы таким образом, что все они исходят из объекта registry и делятся на следующие категории:

Существующая классическая функция отображения sqlalchemy.orm.mapper() осталась, однако обращение к sqlalchemy.orm.mapper() напрямую устарело; новый метод registry.map_imperatively() теперь направляет запрос через sqlalchemy.orm.registry(), что позволяет однозначно интегрировать его с другими декларативными отображениями.

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

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

#5508

Поддерживаются классы данных и атрибуты Python с декларативными и императивными отображениями

Наряду с новыми декларативными стилями декораторов, представленными в Декларативность теперь интегрирована в ORM с новыми возможностями, Mapper теперь явно знает о модуле Python dataclasses и будет распознавать атрибуты, сконфигурированные таким образом, и переходить к их отображению, не пропуская их, как это было ранее. В случае с модулем attrs модуль attrs уже удаляет свои собственные атрибуты из класса, поэтому он уже был совместим с классическими отображениями SQLAlchemy. С добавлением декоратора registry.mapped() обе системы атрибутов теперь могут взаимодействовать и с декларативными отображениями.

#5027

Поддержка асинхронного ввода-вывода в ядре и ORM

SQLAlchemy теперь поддерживает Python asyncio-совместимые драйверы баз данных, используя совершенно новый внешний интерфейс asyncio для Connection для использования в Core, а также Session для использования в ORM, используя объекты AsyncConnection и AsyncSession.

Примечание

Новая функция asyncio должна рассматриваться как альфа-уровень для первых релизов SQLAlchemy 1.4. Это очень новая вещь, которая использует некоторые ранее незнакомые приемы программирования.

Первоначально поддерживается API базы данных asyncpg asyncio driver for PostgreSQL.

Внутренние возможности SQLAlchemy полностью интегрированы за счет использования библиотеки greenlet для адаптации потока выполнения внутри SQLAlchemy с целью распространения ключевых слов asyncio await от драйвера базы данных к API конечного пользователя, в котором используются методы async. Благодаря такому подходу драйвер asyncpg полностью работоспособен в собственном тестовом наборе SQLAlchemy и совместим с большинством функций psycopg2. Данный подход был проверен и улучшен разработчиками проекта greenlet, за что SQLAlchemy выражает им благодарность.

Сам API async ориентирован на работу с IO-методами, такими как AsyncEngine.connect() и AsyncConnection.execute(). Новые конструкции Core строго поддерживают только использование 2.0 style; это означает, что все операторы должны вызываться с объектом соединения, в данном случае AsyncConnection.

В рамках ORM поддерживается выполнение запросов 2.0 style, использующих конструкции select() в сочетании с AsyncSession.execute(); сам унаследованный объект Query классом AsyncSession не поддерживается.

Такие возможности ORM, как ленивая загрузка связанных атрибутов, а также неистекший срок действия атрибутов, по определению недопустимы в традиционной модели программирования asyncio, поскольку они указывают на операции ввода-вывода, которые неявно выполняются в рамках операции Python getattr(). Чтобы преодолеть это, традиционное приложение asyncio должно разумно использовать технику eager loading, а также отказаться от использования таких функций, как expire on commit, чтобы не было необходимости в подобных нагрузках.

Для разработчиков приложений asyncio, которые хотят нарушить традиции, в новом API предусмотрена строго необязательная возможность: приложения, желающие использовать такие возможности ORM, могут организовать код, связанный с базой данных, в виде функций, которые затем могут быть запущены внутри гринлетов с помощью метода AsyncSession.run_sync(). Демонстрацию смотрите в примере greenlet_orm.py на Интеграция Asyncio.

Поддержка асинхронных курсоров также обеспечивается с помощью новых методов AsyncConnection.stream() и AsyncSession.stream(), которые поддерживают новый объект AsyncResult, который сам предоставляет ожидаемые версии таких распространенных методов, как AsyncResult.all() и AsyncResult.fetchmany(). Как в Core, так и в ORM интегрирована возможность, соответствующая использованию «курсоров на стороне сервера» в традиционной SQLAlchemy.

#3414

Многие объекты утверждений Core и ORM теперь выполняют большую часть своих построений и проверок на этапе компиляции

Основной инициативой в серии 1.4 является подход к модели как операторов Core SQL, так и ORM Query, позволяющий реализовать эффективную, кэшируемую модель создания и компиляции операторов, где шаг компиляции будет кэшироваться на основе ключа кэша, генерируемого созданным объектом statement, который сам создается заново для каждого использования. Для достижения этой цели большая часть вычислений в Python, происходящих при построении утверждений, в частности, вычислений ORM Query, а также вычислений select() при вызове ORM-запросов, переносится на этап компиляции утверждения, который происходит только после вызова утверждения и только в том случае, если скомпилированная форма утверждения еще не была кэширована.

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

В эту категорию попадают такие ошибочные состояния, как:

  • Если при построении оператора _selectable.CompoundSelect (например, UNION, EXCEPT и т.д.) передаваемые операторы SELECT не имеют одинакового количества столбцов, то теперь выдается сообщение CompileError; ранее сообщение ArgumentError выдавалось сразу при построении оператора.

  • Различные условия ошибок, которые могут возникнуть при вызове Query.join(), будут оцениваться на этапе компиляции оператора, а не при первом вызове метода.

Другие изменения могут касаться непосредственно объекта Query:

  • Поведение при обращении к аксессору Query.statement может быть несколько иным. Возвращаемый объект Select теперь является прямой копией того же состояния, которое было в Query, без какой-либо компиляции, специфичной для ORM (что означает значительное ускорение). Однако Select не будет иметь того же внутреннего состояния, что и в версии 1.3, включая такие вещи, как явное написание предложений FROM, если они не были явно указаны в Query. Это означает, что код, который полагается на манипуляции с этим оператором Select, например, на вызов методов типа Select.with_only_columns(), может потребовать приспособления к клаузе FROM.

Исправлены внутренние соглашения об импорте для корректной работы линтеров кода

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

@util.dependency_for("sqlalchemy.sql.dml")
def insert(self, dml, *args, **kw):
    ...

Где вышеприведенная функция будет переписана так, чтобы в ней больше не было параметра dml. Это могло бы сбить с толку средства линтинга кода, увидев отсутствие параметра в функции. В связи с этим был реализован новый подход, при котором сигнатура функции больше не модифицируется, а объект модуля приобретается внутри функции.

#4656

#4689

Поддержка операторов регулярных выражений SQL

Долгожданная функция, дополняющая набор операций ColumnOperators.like() и ColumnOperators.match(), добавляет рудиментарную поддержку операторов регулярных выражений для баз данных. Новые возможности включают ColumnOperators.regexp_match(), реализующую функцию совпадения регулярных выражений, и ColumnOperators.regexp_replace(), реализующую функцию замены строки регулярного выражения.

Поддерживаются такие бэкенды, как SQLite, PostgreSQL, MySQL / MariaDB и Oracle. Бэкенд SQLite поддерживает только «regexp_match», но не «regexp_replace».

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

Для SQLite в качестве реализации установлена функция Python re.search() без дополнительных аргументов.

#1390

Режим исключений в SQLAlchemy 2.0

Одной из основных целей выпуска 1.4 является создание «переходного» релиза, чтобы приложения могли постепенно переходить на SQLAlchemy 2.0. С этой целью в релизе 1.4 появилась функция «2.0 deprecations mode», которая представляет собой серию предупреждений об обесценивании, выдаваемых на каждый обнаруженный паттерн API, который будет работать по-другому в версии 2.0. Все предупреждения используют класс RemovedIn20Warning. Поскольку эти предупреждения затрагивают фундаментальные паттерны, включая конструкции select() и Engine, даже простые приложения могут генерировать большое количество предупреждений до тех пор, пока не будут внесены соответствующие изменения в API. Поэтому по умолчанию режим предупреждений отключен до тех пор, пока разработчик не включит переменную окружения SQLALCHEMY_WARN_20=1.

Полное описание использования режима 2.0 Deprecations приведено в разделе Переход на 2.0 Шаг второй - включение функции RemovedIn20Warnings.

API и поведенческие изменения - Core

Оператор SELECT больше не считается неявным предложением FROM

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

Наиболее заметное влияние оказывает то, что select() больше не может быть встроен внутрь другого select() непосредственно, без явного превращения внутреннего select() в подзапрос. Исторически это выполнялось с помощью метода SelectBase.alias(), который остался, однако более явно для этого подходит новый метод SelectBase.subquery(); оба метода делают одно и то же. Возвращаемый объект теперь Subquery, который очень похож на объект Alias и имеет общую базу AliasedReturnsRows.

То есть теперь это приведет к повышению:

stmt1 = select(user.c.id, user.c.name)
stmt2 = select(addresses, stmt1).select_from(addresses.join(stmt1))

Райзинг:

sqlalchemy.exc.ArgumentError: Column expression or FROM clause expected,
got <...Select object ...>. To create a FROM clause from a <class
'sqlalchemy.sql.selectable.Select'> object, use the .subquery() method.

Правильная форма вызова вместо этого (отметим также, что brackets are no longer required for select()):

sq1 = select(user.c.id, user.c.name).subquery()
stmt2 = select(addresses, sq1).select_from(addresses.join(sq1))

Выше отмечалось, что метод SelectBase.subquery() по сути эквивалентен использованию метода SelectBase.alias().

Это изменение обосновывается следующим:

  • Для поддержки унификации Select с Query, объект Select должен иметь методы Select.join() и Select.outerjoin(), которые действительно добавляют критерии JOIN к существующему предложению FROM, чего пользователи всегда ожидали от него в любом случае. Предыдущее поведение, которое должно было соответствовать поведению FromClause, заключалось в том, что он генерировал безымянный подзапрос и затем присоединял к нему JOIN, что было совершенно бесполезной функцией, которая только запутывала тех пользователей, которым не повезло попробовать это сделать. Это изменение обсуждается в select().join() и outerjoin() добавляют критерии JOIN к текущему запросу, а не создают подзапрос.

  • Если включить SELECT в предложение FROM другого SELECT без предварительного создания псевдонима или подзапроса, то это приведет к созданию неименованного подзапроса. Хотя стандартный SQL поддерживает такой синтаксис, на практике большинство баз данных его отвергают. Например, MySQL и PostgreSQL категорически не приемлют использование неименованных подзапросов:

    # MySQL / MariaDB:
    
    MariaDB [(none)]> select * from (select 1);
    ERROR 1248 (42000): Every derived table must have its own alias
    
    
    # PostgreSQL:
    
    test=> select * from (select 1);
    ERROR:  subquery in FROM must have an alias
    LINE 1: select * from (select 1);
                          ^
    HINT:  For example, FROM (SELECT ...) [AS] foo.

    Такие базы данных, как SQLite, принимают их, однако часто имена, получаемые в результате такого подзапроса, оказываются слишком двусмысленными, чтобы быть полезными:

    sqlite> CREATE TABLE a(id integer);
    sqlite> CREATE TABLE b(id integer);
    sqlite> SELECT * FROM a JOIN (SELECT * FROM b) ON a.id=id;
    Error: ambiguous column name: id
    sqlite> SELECT * FROM a JOIN (SELECT * FROM b) ON a.id=b.id;
    Error: no such column: b.id
    
    # use a name
    sqlite> SELECT * FROM a JOIN (SELECT * FROM b) AS anon_1 ON a.id=anon_1.id;

Поскольку объекты SelectBase больше не являются объектами FromClause, атрибуты, подобные атрибуту .c, а также методы, подобные .select(), теперь устарели, поскольку они подразумевают неявное создание подзапроса. Методы .join() и .outerjoin() теперь repurposed to append JOIN criteria to the existing query аналогичны методу Query.join(), что в любом случае всегда ожидалось пользователями.

Вместо атрибута .c добавляется новый атрибут SelectBase.selected_columns. Этот атрибут преобразуется в коллекцию столбцов, которая, как многие надеются, делает то, что делает .c (но не делает), то есть ссылается на столбцы, которые находятся в предложении columns оператора SELECT. Частой ошибкой новичков является код, подобный следующему:

stmt = select(users)
stmt = stmt.where(stmt.c.name == "foo")

Приведенный выше код кажется интуитивно понятным, что он сгенерирует запрос «SELECT * FROM users WHERE name=“foo“», однако опытные пользователи SQLAlchemy поймут, что на самом деле он генерирует бесполезный подзапрос, напоминающий «SELECT * FROM (SELECT * FROM users) WHERE name=“foo“».

Однако новый атрибут SelectBase.selected_columns **очень подходит для приведенного выше случая, поскольку в таком случае он напрямую ссылается на столбцы, присутствующие в коллекции users.c:

stmt = select(users)
stmt = stmt.where(stmt.selected_columns.name == "foo")

#4617

select().join() и outerjoin() добавляют критерии JOIN к текущему запросу, а не создают подзапрос

Для достижения цели унификации Query и Select, особенно для 2.0 style использования Select, было крайне важно иметь рабочий метод Select.join(), который ведет себя подобно методу Query.join(), добавляя дополнительные записи в предложение FROM существующего SELECT и затем возвращая новый объект Select для дальнейшей модификации, вместо того, чтобы заворачивать объект внутрь безымянного подзапроса и возвращать JOIN из этого подзапроса, что всегда было практически бесполезно и вводило пользователей в заблуждение.

Для этого сначала был реализован Оператор SELECT больше не считается неявным предложением FROM, который отделяет Select от FromClause; это устранило требование, что Select.join() должен возвращать объект Join, а не новую версию этого объекта Select, включающую новый JOIN в предложение FROM.

С этого момента, поскольку Select.join() и Select.outerjoin() действительно имели существующее поведение, первоначально планировалось, что эти методы будут устаревшими, а новая «полезная» версия методов будет доступна на альтернативном, «будущем» объекте Select, доступном в виде отдельного импорта.

Однако после некоторого времени работы с данной конкретной кодовой базой было решено, что наличие двух различных типов объектов Select, каждый из которых имеет на 95% одинаковое поведение, за исключением некоторых тонких различий в поведении некоторых методов, будет более вводящим в заблуждение и неудобным, чем простое жесткое изменение поведения этих двух методов, учитывая, что существующее поведение Select.join() и Select.outerjoin() практически никогда не используется и только вызывает путаницу.

Поэтому было решено, учитывая, насколько бесполезным является текущее поведение и насколько чрезвычайно полезным, важным и нужным будет новое поведение, произвести жестокое изменение поведения в этой единственной области, а не ждать еще год и иметь более неудобный API в промежутке. Разработчики SQLAlchemy не считают нужным вносить подобные изменения, однако это особый случай, и крайне маловероятно, что предыдущая реализация этих методов использовалась; как отмечалось в Оператор SELECT больше не считается неявным предложением FROM, основные базы данных, такие как MySQL и PostgreSQL, в любом случае не позволяют использовать неименованные подзапросы, и с синтаксической точки зрения практически невозможно использовать JOIN из неименованного подзапроса, поскольку очень сложно однозначно ссылаться на столбцы внутри него.

В новой реализации Select.join() и Select.outerjoin() ведут себя аналогично Query.join(), добавляя критерии JOIN к существующему оператору путем сопоставления с левой сущностью:

stmt = select(user_table).join(
    addresses_table, user_table.c.id == addresses_table.c.user_id
)

производство:

SELECT user.id, user.name FROM user JOIN address ON user.id=address.user_id

Как и в случае с Join, условие ON определяется автоматически, если это возможно:

stmt = select(user_table).join(addresses_table)

Если в операторе используются сущности ORM, то именно так, по сути, строятся запросы ORM с использованием вызова 2.0 style. ORM-сущности присваивают оператору внутренний «плагин», благодаря которому при компиляции оператора в SQL-строку будут выполняться правила компиляции, связанные с ORM. Более непосредственно, метод Select.join() может учитывать отношения ORM, не нарушая жесткого разделения между Core и внутренними элементами ORM:

stmt = select(User).join(User.addresses)

Также добавлен еще один новый метод Select.join_from(), позволяющий проще задавать сразу левую и правую стороны объединения:

stmt = select(Address.email_address, User.name).join_from(User, Address)

производство:

SELECT address.email_address, user.name FROM user JOIN address ON user.id == address.user_id

Объект URL теперь является неизменяемым

Объект URL был формализован таким образом, что теперь он представляется как namedtuple с фиксированным числом полей, которые являются неизменяемыми. Кроме того, словарь, представленный атрибутом URL.query, также является неизменяемым отображением. Мутация объекта URL не была официально поддержана или документирована, что привело к появлению некоторых неопределенных вариантов использования, которые затрудняли перехват некорректного использования, наиболее распространенной из которых была мутация словаря URL.query для включения в него нестроковых элементов. Это также привело к обычным проблемам, связанным с разрешением мутабельности фундаментального объекта данных, а именно к нежелательным мутациям в других местах, просачивающимся в код, который не ожидал изменения URL. Наконец, конструкция namedtuple вдохновлена конструкцией urllib.parse.urlparse() в Python, которая возвращает разобранный объект в виде именованного кортежа.

Решение о полном изменении API основано на расчетах, в которых взвешивается нецелесообразность пути депривации (который предполагает изменение словаря URL.query на специальный словарь, выдающий предупреждения о депривации при вызове любых методов мутации стандартной библиотеки, Кроме того, когда словарь будет содержать какой-либо список элементов, список также должен будет выдавать предупреждения об устаревании при мутации) против маловероятного случая использования проектов, уже мутирующих объекты URL в первую очередь, а также того, что небольшие изменения, подобные изменениям #5341, в любом случае создают обратную несовместимость. Основным случаем мутации объекта URL является разбор аргументов плагина в точке расширения CreateEnginePlugin, которая появилась сравнительно недавно и, судя по поиску кода на Github, используется в двух репозиториях, причем ни один из них не мутирует объект URL.

Объект URL теперь предоставляет богатый интерфейс, проверяющий и генерирующий новые объекты URL. Существующий механизм создания объекта URL, функция make_url(), остается неизменным:

>>> from sqlalchemy.engine import make_url
>>> url = make_url("postgresql+psycopg2://user:pass@host/dbname")

При программном конструировании код, который мог использовать конструктор URL или метод __init__ напрямую, получит предупреждение об устаревании, если аргументы будут переданы как аргументы ключевого слова, а не как точный 7-кортеж. Теперь конструктор в стиле ключевого слова доступен через метод URL.create():

>>> from sqlalchemy.engine import URL
>>> url = URL.create("postgresql", "user", "pass", host="host", database="dbname")
>>> str(url)
'postgresql://user:pass@host/dbname'

Поля могут быть изменены типичным образом с помощью метода URL.set(), который возвращает новый объект URL с примененными изменениями:

>>> mysql_url = url.set(drivername="mysql+pymysql")
>>> str(mysql_url)
'mysql+pymysql://user:pass@host/dbname'

Для изменения содержимого словаря URL.query можно использовать методы типа URL.update_query_dict():

>>> url.update_query_dict({"sslcert": "/path/to/crt"})
postgresql://user:***@host/dbname?sslcert=%2Fpath%2Fto%2Fcrt

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

def set_url_drivername(some_url, some_drivername):
    # check for 1.4
    if hasattr(some_url, "set"):
        return some_url.set(drivername=some_drivername)
    else:
        # SQLAlchemy 1.3 or earlier, mutate in place
        some_url.drivername = some_drivername
        return some_url


def set_ssl_cert(some_url, ssl_cert):
    # check for 1.4
    if hasattr(some_url, "update_query_dict"):
        return some_url.update_query_dict({"sslcert": ssl_cert})
    else:
        # SQLAlchemy 1.3 or earlier, mutate in place
        some_url.query["sslcert"] = ssl_cert
        return some_url

Строка запроса сохраняет свой существующий формат словаря строк к строкам, используя последовательности строк для представления нескольких параметров. Например:

>>> from sqlalchemy.engine import make_url
>>> url = make_url(
...     "postgresql://user:pass@host/dbname?alt_host=host1&alt_host=host2&sslcert=%2Fpath%2Fto%2Fcrt"
... )
>>> url.query
immutabledict({'alt_host': ('host1', 'host2'), 'sslcert': '/path/to/crt'})

Для работы с содержимым атрибута URL.query таким образом, чтобы все значения были нормализованы в последовательности, используйте атрибут URL.normalized_query:

>>> url.normalized_query
immutabledict({'alt_host': ('host1', 'host2'), 'sslcert': ('/path/to/crt',)})

Строка запроса может быть дополнена с помощью таких методов, как URL.update_query_dict(), URL.update_query_pairs(), URL.update_query_string():

>>> url.update_query_dict({"alt_host": "host3"}, append=True)
postgresql://user:***@host/dbname?alt_host=host1&alt_host=host2&alt_host=host3&sslcert=%2Fpath%2Fto%2Fcrt

См.также

URL

Изменения в CreateEnginePlugin

Это изменение затронуло и CreateEnginePlugin, поскольку в документации к пользовательским плагинам указывалось, что для удаления потребляемых аргументов из объекта URL следует использовать метод dict.pop(). Теперь для этого следует использовать метод CreateEnginePlugin.update_url(). Обратно совместимый подход будет выглядеть следующим образом:

from sqlalchemy.engine import CreateEnginePlugin


class MyPlugin(CreateEnginePlugin):
    def __init__(self, url, kwargs):
        # check for 1.4 style
        if hasattr(CreateEnginePlugin, "update_url"):
            self.my_argument_one = url.query["my_argument_one"]
            self.my_argument_two = url.query["my_argument_two"]
        else:
            # legacy
            self.my_argument_one = url.query.pop("my_argument_one")
            self.my_argument_two = url.query.pop("my_argument_two")

        self.my_argument_three = kwargs.pop("my_argument_three", None)

    def update_url(self, url):
        # this method runs in 1.4 only and should be used to consume
        # plugin-specific arguments
        return url.difference_update_query(["my_argument_one", "my_argument_two"])

Полную информацию об использовании этого класса см. в docstring по адресу CreateEnginePlugin.

#5526

select(), case() теперь принимают позиционные выражения

Как видно из других мест этого документа, конструкция select() теперь будет принимать аргументы «column clause» позиционно, а не требовать их передачи в виде списка:

# new way, supports 2.0
stmt = select(table.c.col1, table.c.col2, ...)

При позиционной передаче аргументов никакие другие ключевые слова-аргументы не допускаются. В SQLAlchemy 2.0 указанный выше стиль вызова будет единственным поддерживаемым стилем вызова.

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

# old way, still works in 1.4
stmt = select([table.c.col1, table.c.col2, ...])

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

# very much the old way, but still works in 1.4
stmt = select([table.c.col1, table.c.col2, ...], whereclause=table.c.col1 == 5)

Различие между этими двумя стилями основано на том, является ли первый позиционный аргумент списком или нет. К сожалению, все еще возможны случаи, когда ключевое слово «whereclause» опускается:

# very much the old way, but still works in 1.4
stmt = select([table.c.col1, table.c.col2, ...], table.c.col1 == 5)

В рамках этого изменения конструкция Select также получила API «будущего» в стиле 2.0, который включает обновленный метод Select.join(), а также методы Select.filter_by() и Select.join_from().

В связи с этим изменением конструкция case() также была модифицирована для позиционного принятия своего списка WHEN-клаузул, с аналогичной дорожкой выбытия для старого стиля вызова:

stmt = select(users_table).where(
    case(
        (users_table.c.name == "wendy", "W"),
        (users_table.c.name == "jack", "J"),
        else_="E",
    )
)

Для конструкций SQLAlchemy, принимающих *args, а не список значений, как в случае с конструкцией ColumnOperators.in_(), принято, что позиционные аргументы используются для структурной спецификации, а списки - для спецификации данных.

#5284

Все IN-выражения выводят параметры для каждого значения в списке на лету (например, расширяющие параметры)

Функция «расширения IN», впервые появившаяся в Расширенные в последнее время наборы параметров IN позволяют использовать IN-выражения с кэшированными утверждениями, достаточно развита, чтобы явно превосходить предыдущий способ отображения IN-выражений. Поскольку этот подход был усовершенствован для работы с пустыми списками значений, теперь это единственное средство, которое Core / ORM будет использовать для отображения списков параметров IN.

Предыдущий подход, присутствовавший в SQLAlchemy с момента ее первого выпуска, заключался в том, что при передаче методу ColumnOperators.in_() списка значений, во время построения оператора этот список разворачивался в серию отдельных объектов BindParameter. Это имело то ограничение, что во время выполнения оператора нельзя было изменять список параметров на основе словаря параметров, что означало невозможность кэширования строковых SQL-операторов независимо от их параметров, а также невозможность полного использования словаря параметров для операторов, включающих выражения IN в целом.

Для обслуживания функции «запеченного запроса», описанной в Запеченные запросы, потребовалась кэшируемая версия IN, что и привело к появлению функции «расширяющийся IN». В отличие от существующего поведения, когда список параметров расширяется во время построения оператора на отдельные объекты BindParameter, в данной функции используется один BindParameter, который хранит список значений сразу; когда оператор выполняется Engine, он «расширяется» на лету на отдельные связанные позиции параметров на основе параметров, переданных в вызов Connection.execute(), а существующая строка SQL, которая могла быть получена при предыдущем выполнении, модифицируется с помощью регулярного выражения для соответствия текущему набору параметров. Таким образом, один и тот же объект Compiled, хранящий рендеринговый строковый оператор, может быть вызван несколько раз с различными наборами параметров, изменяющими содержимое списка, передаваемого в IN-выражения, при этом сохраняется поведение отдельных скалярных параметров, передаваемых в DBAPI. Хотя некоторые DBAPI поддерживают эту функциональность напрямую, в целом она недоступна; теперь функция «расширяющийся IN» поддерживает это поведение последовательно для всех бэкендов.

Поскольку одним из основных направлений 1.4 является обеспечение истинного кэширования выражений в Core и ORM без неудобств, связанных с «запеченной» системой, и поскольку функция «расширения IN» в любом случае представляет собой более простой подход к построению выражений, она теперь вызывается автоматически всякий раз, когда в выражение IN передается список значений:

stmt = select(A.id, A.data).where(A.id.in_([1, 2, 3]))

Строковое представление перед выполнением имеет вид:

>>> print(stmt)
{printsql}SELECT a.id, a.data
FROM a
WHERE a.id IN ([POSTCOMPILE_id_1])

Чтобы вывести значения напрямую, используйте literal_binds, как это было ранее:

>>> print(stmt.compile(compile_kwargs={"literal_binds": True}))
{printsql}SELECT a.id, a.data
FROM a
WHERE a.id IN (1, 2, 3)

В качестве помощника добавлен новый флаг «render_postcompile», позволяющий выводить текущее связанное значение в том виде, в котором оно будет передано в базу данных:

>>> print(stmt.compile(compile_kwargs={"render_postcompile": True}))
{printsql}SELECT a.id, a.data
FROM a
WHERE a.id IN (:id_1_1, :id_1_2, :id_1_3)

В лог-выводах двигателя также отображается конечный вывод:

INFO sqlalchemy.engine.base.Engine SELECT a.id, a.data
FROM a
WHERE a.id IN (?, ?, ?)
INFO sqlalchemy.engine.base.Engine (1, 2, 3)

В рамках этого изменения поведение выражений «пустой IN», в которых параметр списка пуст, теперь стандартизировано на использовании оператора IN против так называемого «пустого набора». Поскольку стандартного синтаксиса SQL для пустых наборов не существует, используется SELECT, который не возвращает ни одной строки, а для каждого бэкенда настраивается особым образом, чтобы база данных воспринимала его как пустой набор; эта возможность впервые появилась в версии 1.3 и описана в Функция расширения IN теперь поддерживает пустые списки. Параметр create_engine.empty_in_strategy, введенный в версии 1.2 в качестве средства переноса того, как этот случай рассматривался в предыдущей системе IN, теперь устарел, и этот флаг больше не действует; как описано в Поведение оператора IN / NOT IN в отношении пустой коллекции теперь настраивается; упрощено выражение по умолчанию, этот флаг позволял диалекту переключаться между первоначальной системой сравнения столбца с самим собой, что оказалось большой проблемой производительности, и более новой системой сравнения «1 != 1», чтобы получить «ложное» выражение. Поведение, реализованное в 1.3, которое теперь имеет место во всех случаях, более корректно, чем оба подхода, поскольку оператор IN по-прежнему используется, и не имеет проблем с производительностью, характерных для исходной системы.

Кроме того, система «расширяющихся» параметров была обобщена таким образом, чтобы она обслуживала и другие специфические для диалекта случаи использования, когда параметр не может быть учтен DBAPI или базой данных; подробнее см. раздел Новые «посткомпиляционные» связанные параметры, используемые для LIMIT/OFFSET в Oracle, SQL Server.

#4645

Встроенный линтинг FROM предупреждает о возможных картезианских произведениях в операторе SELECT

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

SQLAlchemy, несмотря на все свои замечательные возможности, особенно подвержена подобным проблемам, поскольку в оператор SELECT автоматически добавляются элементы из любой таблицы, встречающейся в других пунктах, в предложение FROM. Типичный сценарий выглядит следующим образом: две таблицы соединены JOIN, но дополнительная запись в предложении WHERE, которая, возможно, случайно не совпадает с этими двумя таблицами, создает дополнительную запись FROM:

address_alias = aliased(Address)

q = (
    session.query(User)
    .join(address_alias, User.addresses)
    .filter(Address.email_address == "foo")
)

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

SELECT
    users.id AS users_id, users.name AS users_name,
    users.fullname AS users_fullname,
    users.nickname AS users_nickname
FROM addresses, users JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id
WHERE addresses.email_address = :email_address_1

В приведенном выше SQL мы видим то, что разработчики SQLAlchemy называют «страшной запятой», поскольку в предложении FROM мы видим «FROM addresses, users JOIN addresses», что является классическим признаком декартова произведения; когда запрос использует JOIN для объединения предписаний FROM вместе, но поскольку одно из них не объединено, в нем используется запятая. Приведенный выше запрос вернет полный набор строк, объединяющих таблицы «user» и «addresses» по столбцу «id / user_id», а затем применит все эти строки в виде декартова произведения к каждой строке таблицы «addresses» напрямую. То есть, если имеется десять строк пользователей и 100 строк адресов, то приведенный выше запрос вернет ожидаемый результат строк, который, скорее всего, будет равен 100, поскольку будут выбраны все строки адресов, умноженные еще раз на 100, так что общий размер результата будет равен 10000 строк.

Шаблон «table1, table2 JOIN table3» довольно часто встречается и в SQLAlchemy ORM, что связано как с неправильным применением функций ORM, в частности, связанных с загрузкой объединенных таблиц или наследованием объединенных таблиц, так и с ошибками SQLAlchemy ORM в этих системах. Аналогичные проблемы возникают и с операторами SELECT, использующими «неявные соединения», в которых ключевое слово JOIN не используется, а каждый элемент FROM связывается с другим элементом через предложение WHERE.

В течение нескольких лет в Wiki существовал рецепт, который применял графовый алгоритм к конструкции select() во время выполнения запроса и проверял структуру запроса на наличие несвязанных предложений FROM, анализируя предложение WHERE и все предложения JOIN, чтобы определить, как элементы FROM связаны между собой, и убедиться, что все элементы FROM связаны в единый граф. Этот рецепт был адаптирован для того, чтобы стать частью самого SQLCompiler, и теперь он опционально выдает предупреждение для оператора при обнаружении этого условия. Предупреждение включается с помощью флага create_engine.enable_from_linting и включено по умолчанию. Вычислительные затраты линтера очень малы, к тому же он работает только во время компиляции оператора, что означает, что для кэшированного SQL-оператора он работает только один раз.

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

>>> q.all()
SAWarning: SELECT statement has a cartesian product between FROM
element(s) "addresses_1", "users" and FROM element "addresses".
Apply join condition(s) between each element to resolve.

Функция linter учитывает не только таблицы, связанные между собой с помощью условий JOIN, но и с помощью условия WHERE Выше мы можем добавить условие WHERE, чтобы связать новую сущность Address с предыдущей сущностью address_alias, и это устранит предупреждение:

q = (
    session.query(User)
    .join(address_alias, User.addresses)
    .filter(Address.email_address == "foo")
    .filter(Address.id == address_alias.id)
)  # resolve cartesian products,
# will no longer warn

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

q = (
    session.query(User)
    .join(address_alias, User.addresses)
    .filter(Address.email_address == "foo")
    .filter(Address.id > address_alias.id)
)  # will generate a lot of rows,
# but no warning

Также допускается использование полных декартовых произведений, если они явно указаны; если нам нужно, например, декартово произведение User и Address, мы можем выполнить JOIN на true() так, что каждая строка будет совпадать с каждой другой; следующий запрос вернет все строки и не выдаст никаких предупреждений:

from sqlalchemy import true

# intentional cartesian product
q = session.query(User).join(Address, true())  # intentional cartesian product

По умолчанию предупреждение выдается только при компиляции оператора Connection для выполнения; вызов метода ClauseElement.compile() не будет выдавать предупреждение, если не установлен флаг linting:

>>> from sqlalchemy.sql import FROM_LINTING
>>> print(q.statement.compile(linting=FROM_LINTING))
SAWarning: SELECT statement has a cartesian product between FROM element(s) "addresses" and FROM element "users".  Apply join condition(s) between each element to resolve.
{printsql}SELECT users.id, users.name, users.fullname, users.nickname
FROM addresses, users JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id
WHERE addresses.email_address = :email_address_1

#4737

Новый объект Result

Основной целью SQLAlchemy 2.0 является унификация работы с «результатами» в ORM и Core. Для достижения этой цели в версии 1.4 представлены новые версии объектов ResultProxy и RowProxy, которые были частью SQLAlchemy с самого начала.

Новые объекты документированы по адресам Result и Row, и используются не только для наборов результатов Core, но и для результатов 2.0 style в рамках ORM.

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

Result.one() - возвращает ровно один ряд, либо повышает:

with engine.connect() as conn:
    row = conn.execute(table.select().where(table.c.id == 5)).one()

Result.one_or_none() - то же самое, но также возвращает None при отсутствии строк

Result.all() - возвращает все строки

Result.partitions() - получение строк кусками:

with engine.connect() as conn:
    result = conn.execute(
        table.select().order_by(table.c.id),
        execution_options={"stream_results": True},
    )
    for chunk in result.partitions(500):
        # process up to 500 records
        ...

Result.columns() - позволяет производить нарезку и реорганизацию строк:

with engine.connect() as conn:
    # requests x, y, z
    result = conn.execute(select(table.c.x, table.c.y, table.c.z))

    # iterate rows as y, x
    for y, x in result.columns("y", "x"):
        print("Y: %s  X: %s" % (y, x))

Result.scalars() - возвращает списки скалярных объектов, по умолчанию из первого столбца, но может быть и выбран:

result = session.execute(select(User).order_by(User.id))
for user_obj in result.scalars():
    ...

Result.mappings() - вместо строк именованных кортежей возвращает словари:

with engine.connect() as conn:
    result = conn.execute(select(table.c.x, table.c.y, table.c.z))

    for map_ in result.mappings():
        print("Y: %(y)s  X: %(x)s" % map_)

При использовании Core объект, возвращаемый Connection.execute(), является экземпляром CursorResult, который продолжает использовать те же возможности API, что и ResultProxy, в отношении вставляемых первичных ключей, значений по умолчанию, подсчета строк и т.д. Для ORM будет возвращен подкласс Result, который выполняет трансляцию строк Core в строки ORM, а затем позволяет выполнять все те же операции.

См.также

ORM Query Unified with Core Select - в документации по миграции 2.0

#5087

#4395

#4959

RowProxy больше не является «прокси»; теперь он называется Row и ведет себя как расширенный именованный кортеж

Класс RowProxy, представляющий отдельные строки результатов базы данных в наборе результатов Core, теперь называется Row и больше не является «прокси» объектом; Это означает, что при возврате объекта Row строка представляет собой простой кортеж, содержащий данные в их окончательном виде, уже обработанные функциями обработки строк результатов, связанными с типами данных (примеры: превращение строки даты из базы данных в объект datetime, строки JSON в результат Python json.loads() и т.д.). ).

Это объясняется тем, что строка может действовать скорее как именованный кортеж Python, а не как отображение, где значения в кортеже являются объектом оператора __contains__ на кортеже, а не ключами. Поскольку Row работает как именованный кортеж, его можно использовать в качестве замены объекта KeyedTuple в ORM, что в конечном итоге приведет к созданию API, в котором и ORM, и Core будут предоставлять одинаковые наборы результатов. Унификация основных паттернов в ORM и Core является основной целью SQLAlchemy 2.0, и релиз 1.4 нацелен на то, чтобы большинство или все базовые архитектурные паттерны были реализованы для поддержки этого процесса. В примечании Объект «KeyedTuple», возвращаемый запросом Query, заменяется на Row описывается использование в ORM класса Row.

В релизе 1.4 класс Row предоставляет дополнительный подкласс LegacyRow, который используется в Core и обеспечивает обратно совместимую версию RowProxy, выдавая при этом предупреждения об устаревании для тех функций и поведений API, которые будут перенесены. ORM Query теперь использует Row непосредственно в качестве замены KeyedTuple.

Класс LegacyRow является переходным классом, в котором метод __contains__ по-прежнему проверяет ключи, а не значения, выдавая при этом предупреждение об устаревании при успешном выполнении операции. Кроме того, все остальные методы, похожие на отображение предыдущего класса RowProxy, устарели, включая LegacyRow.keys(), LegacyRow.items() и т.д. В дальнейшем для получения маппинга из объекта Row, включая поддержку этих методов, а также оператора __contains__, ориентированного на ключ, API будет сначала обращаться к специальному атрибуту Row._mapping, который будет предоставлять полный интерфейс маппинга к строке, а не кортежу.

Обоснование: Чтобы вести себя более похоже на именованный кортеж, а не на отображение

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

row = (id: 5,  name: 'some name')

Самым большим кросс-совместимым отличием является поведение __contains__:

"id" in row  # True for a mapping, False for a named tuple
"some name" in row  # False for a mapping, True for a named tuple

В версии 1.4, когда набор результатов Core возвращает LegacyRow, приведенное выше сравнение "id" in row будет продолжаться, однако будет выдано предупреждение об устаревании. Чтобы использовать оператор «in» в качестве отображения, воспользуйтесь атрибутом Row._mapping:

"id" in row._mapping

Объект результата SQLAlchemy 2.0 будет содержать модификатор .mappings(), чтобы эти отображения можно было получать напрямую:

# using sqlalchemy.future package
for row in result.mappings():
    row["id"]

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

Рефакторинг Row на поведение кортежа требует, чтобы все значения данных были полностью доступны заранее. Это внутреннее изменение поведения по сравнению с RowProxy, в котором функции обработки строки-результата вызывались в момент обращения к элементу строки, а не при ее первом получении. Это означает, что, например, при получении значения времени даты из SQLite данные для строки, представленные в объекте RowProxy, ранее выглядели бы так:

row_proxy = (1, "2019-12-31 19:56:58.272106")

а затем при обращении к нему через __getitem__ функция datetime.strptime() будет использована на лету для преобразования строковой даты в объект datetime. В новой архитектуре объект datetime() присутствует в кортеже при его возврате, а функция datetime.strptime() была вызвана только один раз:

row = (1, datetime.datetime(2019, 12, 31, 19, 56, 58, 272106))

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

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

Существует множество причин, по которым приведенные выше предположения не выполняются:

  1. подавляющее большинство функций обработки строк вызывалось для декодирования байтовой строки в строку Python Unicode под Python 2. Это происходило в тот момент, когда Python Unicode только начинал использоваться, и до появления Python 3. После появления Python 3, в течение нескольких лет, все Python DBAPI стали поддерживать прямую передачу объектов Python Unicode как в Python 2, так и в Python 3, в первом случае как опцию, а во втором - как единственный способ. В конце концов, в большинстве случаев он стал использоваться по умолчанию и для Python 2. Поддержка Python 2 в SQLAlchemy по-прежнему позволяет явно преобразовывать строки в Unicode для некоторых DBAPI, таких как cx_Oracle, однако теперь это делается на уровне DBAPI, а не как стандартная функция обработки строк результатов SQLAlchemy.

  2. Приведенное выше преобразование строк, когда оно используется, было сделано чрезвычайно производительным с помощью расширений C, настолько, что даже в 1.4 хук кодека SQLAlchemy из байта в юникод подключен к cx_Oracle, где он был замечен более производительным, чем собственный хук cx_Oracle; это означает, что накладные расходы на преобразование всех строк подряд не столь значительны, как это было изначально в любом случае.

  3. В большинстве других случаев функции обработки строк не используются; исключение составляют поддержка времени даты в SQLite, поддержка JSON в некоторых бэкендах, некоторые числовые обработчики, такие как string to Decimal. В случае с Decimal в Python 3 также стандартизована высокопроизводительная реализация cdecimal, чего нельзя сказать о Python 2, где по-прежнему используется гораздо менее производительная версия чистого Python.

  4. В ранних версиях SQLAlchemy код баз данных на других языках в виде «row = fetch(„SELECT * FROM table“)» был обычным явлением; однако при использовании языка выражений SQLAlchemy код, наблюдаемый в реальности, обычно использует конкретные необходимые столбцы.

#4710

Объекты SELECT и производные предложения FROM допускают дублирование столбцов и меток столбцов

Благодаря этому изменению конструкция select() теперь позволяет дублировать метки столбцов, а также дублировать сами объекты столбцов, так что кортежи результатов будут организованы и упорядочены идентично тому, как были выбраны столбцы. ORM Query уже работает подобным образом, поэтому данное изменение обеспечивает большую кросс-совместимость между ними, что является ключевой целью перехода на версию 2.0:

>>> from sqlalchemy import column, select
>>> c1, c2, c3, c4 = column("c1"), column("c2"), column("c3"), column("c4")
>>> stmt = select(c1, c2, c3.label("c2"), c2, c4)
>>> print(stmt)
{printsql}SELECT c1, c2, c3 AS c2, c2, c4

Для поддержки этого изменения атрибуты ColumnCollection, используемые в SelectBase, а также в производных предложениях FROM, таких как подзапросы, также поддерживают дублирование столбцов; это включает новый атрибут SelectBase.selected_columns, устаревший атрибут SelectBase.c, а также атрибут FromClause.c, встречающийся в таких конструкциях, как Subquery и Alias:

>>> list(stmt.selected_columns)
[
    <sqlalchemy.sql.elements.ColumnClause at 0x7fa540bcca20; c1>,
    <sqlalchemy.sql.elements.ColumnClause at 0x7fa540bcc9e8; c2>,
    <sqlalchemy.sql.elements.Label object at 0x7fa540b3e2e8>,
    <sqlalchemy.sql.elements.ColumnClause at 0x7fa540bcc9e8; c2>,
    <sqlalchemy.sql.elements.ColumnClause at 0x7fa540897048; c4>
]

>>> print(stmt.subquery().select())
{printsql}SELECT anon_1.c1, anon_1.c2, anon_1.c2, anon_1.c2, anon_1.c4
FROM (SELECT c1, c2, c3 AS c2, c2, c4) AS anon_1

ColumnCollection также позволяет осуществлять доступ по целочисленному индексу для поддержки случаев, когда строка «ключ» неоднозначна:

>>> stmt.selected_columns[2]
<sqlalchemy.sql.elements.Label object at 0x7fa540b3e2e8>

Для удобства использования ColumnCollection в таких объектах, как Table и PrimaryKeyConstraint, в новом классе DedupeColumnCollection сохранено старое «дедуплицирующее» поведение, более критичное для этих объектов.

Изменение заключается в том, что привычное предупреждение "Column %r on table %r being replaced by %r, which has the same key.  Consider use_labels for select() statements." удалено; Select.apply_labels() по-прежнему доступно и используется ORM для всех операций SELECT, однако оно не подразумевает дедупликации объектов столбцов, хотя и дедуплицирует неявно сгенерированные метки:

>>> from sqlalchemy import table
>>> user = table("user", column("id"), column("name"))
>>> stmt = select(user.c.id, user.c.name, user.c.id).apply_labels()
>>> print(stmt)
SELECT "user".id AS user_id, "user".name AS user_name, "user".id AS id_1
FROM "user"

Наконец, это изменение упрощает создание UNION и других объектов _selectable.CompoundSelect, гарантируя, что количество и расположение столбцов в операторе SELECT будет соответствовать заданному, в таких случаях, как:

>>> s1 = select(user, user.c.id)
>>> s2 = select(c1, c2, c3)
>>> from sqlalchemy import union
>>> u = union(s1, s2)
>>> print(u)
{printsql}SELECT "user".id, "user".name, "user".id
FROM "user" UNION SELECT c1, c2, c3

#4753

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

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

test=> SELECT CAST(data AS VARCHAR) FROM foo;

data
------
 5
(1 row)

``»data»``T

MariaDB [test]> SELECT CAST(data AS CHAR) FROM foo;
+--------------------+
| CAST(data AS CHAR) |
+--------------------+
| 5                  |
+--------------------+
1 row in set (0.003 sec)

В выражениях SQLAlchemy Core мы никогда не имеем дело с сырым сгенерированным именем, подобным приведенному выше, поскольку SQLAlchemy применяет авторазметку к подобным выражениям, которые до сих пор всегда являются так называемыми «анонимными» выражениями:

>>> print(select(cast(foo.c.data, String)))
{printsql}SELECT CAST(foo.data AS VARCHAR) AS anon_1     # old behavior
FROM foo

ResultProxy String #918 cursor.description Объекты SELECT и производные предложения FROM допускают дублирование столбцов и меток столбцов T

>>> print(select(cast(foo.c.data, String)))
{printsql}SELECT CAST(foo.data AS VARCHAR) AS data
FROM foo

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

>>> print(select(cast("hi there," + foo.c.data, String)))
{printsql}SELECT CAST(:data_1 + foo.data AS VARCHAR) AS anon_1
FROM foo

cast() Label A

>>> print(select(cast(("hi there," + foo.c.data).label("hello_data"), String)))
{printsql}SELECT CAST(:data_1 + foo.data AS VARCHAR) AS hello_data
FROM foo

:class:`.Label`A

>>> print(select(cast(("hi there," + foo.c.data), String).label("hello_data")))
{printsql}SELECT CAST(:data_1 + foo.data AS VARCHAR) AS hello_data
FROM foo

#4449

Новые «посткомпиляционные» связанные параметры, используемые для LIMIT/OFFSET в Oracle, SQL Server

:class:`.Compiled`A

Хотя SQLAlchemy уже много лет использует связанные параметры для схем LIMIT/OFFSET, осталось несколько исключений, когда такие параметры не допускаются, в том числе оператор SQL Server «TOP N», например, такой:

SELECT TOP 5 mytable.id, mytable.data FROM mytable

optimize_limits=True create_engine() a

SELECT anon_1.id, anon_1.data FROM (
    SELECT /*+ FIRST_ROWS(5) */
    anon_2.id AS id,
    anon_2.data AS data,
    ROWNUM AS ora_rn FROM (
        SELECT mytable.id, mytable.data FROM mytable
    ) anon_2
    WHERE ROWNUM <= :param_1
) anon_1 WHERE ora_rn > :param_2

bindparam() cursor.execute() str(statement.compile(dialect=<dialect>)) I

SELECT TOP [POSTCOMPILE_param_1] mytable.id, mytable.data FROM mytable

a

SELECT anon_1.id, anon_1.data FROM (
    SELECT /*+ FIRST_ROWS([POSTCOMPILE__ora_frow_1]) */
    anon_2.id AS id,
    anon_2.data AS data,
    ROWNUM AS ora_rn FROM (
        SELECT mytable.id, mytable.data FROM mytable
    ) anon_2
    WHERE ROWNUM <= [POSTCOMPILE_param_1]
) anon_1 WHERE ora_rn > [POSTCOMPILE_param_2]

``[POSTCOMPILE_<param>]``T

При просмотре вывода SQL-журнала будет видна окончательная форма оператора:

SELECT anon_1.id, anon_1.data FROM (
    SELECT /*+ FIRST_ROWS(5) */
    anon_2.id AS id,
    anon_2.data AS data,
    ROWNUM AS ora_rn FROM (
        SELECT mytable.id AS id, mytable.data AS data FROM mytable
    ) anon_2
    WHERE ROWNUM <= 8
) anon_1 WHERE ora_rn > 3

bindparam.literal_execute TypeEngine.literal_processor() T

#4808

Транзакции уровня соединения теперь могут быть неактивными на основе субтранзакций

Connection Transaction Transaction A

Connection Session «Транзакция этого сеанса была откачена из-за предыдущего исключения во время промывки.» (или аналогично) T

Connection Session Присоединение сеанса к внешней транзакции (например, для тестовых наборов) W

Функция «субтранзакций» в Core и ORM сама по себе является устаревшей и в версии 2.0 больше не будет присутствовать. В связи с этим новое условие ошибки само по себе является временным, так как после удаления субтранзакций оно больше не будет применяться.

create_engine.future create_engine() I

:ref:`error_8s2a`T

Типы данных Enum и Boolean больше не имеют по умолчанию значения «создать ограничение»

Enum.create_constraint Boolean.create_constraint T

``True``T

class Spam(Base):
    __tablename__ = "spam"
    id = Column(Integer, primary_key=True)
    boolean = Column(Boolean(create_constraint=True))
    enum = Column(Enum("a", "b", "c", create_constraint=True))

#5367

Новые возможности - ORM

Raiseload для колонн

InvalidRequestError defer.raiseload defer() raiseload() T

book = session.query(Book).options(defer(Book.summary, raiseload=True)).first()

# would raise an exception
book.summary

deferred.raiseload deferred() undefer() T

class Book(Base):
    __tablename__ = "book"

    book_id = Column(Integer, primary_key=True)
    title = Column(String(200), nullable=False)
    summary = deferred(Column(String(2000)), raiseload=True)
    excerpt = deferred(Column(Text), raiseload=True)


book_w_excerpt = session.query(Book).options(undefer(Book.excerpt)).first()

raiseload() relationship() raiseload() I

session.query(Order).options(joinedload(Order.items), raiseload("*"))

raiseload() raiseload() defer() A

:func:`.raiseload`<

:paramref:`.orm.defer.raiseload`<

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

:func:`.defer`A

#4826

ORM Пакетные вставки с psycopg2 теперь в большинстве случаев выполняют пакетные операции с RETURNING

В диалекте psycopg2 по умолчанию реализована функция «execute_values» с RETURNING для операторов INSERT execute_values() execute_values() T

:ref:`performance suite <examples_performance>`S

# 1.3
$ python -m examples.performance bulk_inserts --dburl postgresql://scott:tiger@localhost/test
test_flush_no_pk : (100000 iterations); total time 14.051527 sec
test_bulk_save_return_pks : (100000 iterations); total time 15.002470 sec
test_flush_pk_given : (100000 iterations); total time 7.863680 sec
test_bulk_save : (100000 iterations); total time 6.780378 sec
test_bulk_insert_mappings :  (100000 iterations); total time 5.363070 sec
test_core_insert : (100000 iterations); total time 5.362647 sec

# 1.4 with enhancement
$ python -m examples.performance bulk_inserts --dburl postgresql://scott:tiger@localhost/test
test_flush_no_pk : (100000 iterations); total time 3.820807 sec
test_bulk_save_return_pks : (100000 iterations); total time 3.176378 sec
test_flush_pk_given : (100000 iterations); total time 4.037789 sec
test_bulk_save : (100000 iterations); total time 2.604446 sec
test_bulk_insert_mappings : (100000 iterations); total time 1.204897 sec
test_core_insert : (100000 iterations); total time 0.958976 sec

``execute_values()``N

2020-06-27 19:08:18,166 INFO sqlalchemy.engine.Engine INSERT INTO a (data) VALUES (%(data)s) RETURNING a.id
2020-06-27 19:08:18,166 INFO sqlalchemy.engine.Engine [generated in 0.00698s] ({'data': 'data 1'}, {'data': 'data 2'}, {'data': 'data 3'}, {'data': 'data 4'}, {'data': 'data 5'}, {'data': 'data 6'}, {'data': 'data 7'}, {'data': 'data 8'}  ... displaying 10 of 4999 total bound parameter sets ...  {'data': 'data 4998'}, {'data': 'data 4999'})
2020-06-27 19:08:18,254 INFO sqlalchemy.engine.Engine COMMIT

Конечный оператор INSERT можно увидеть, включив протоколирование операций на стороне PostgreSQL:

2020-06-27 19:08:18.169 EDT [26960] LOG:  statement: INSERT INTO a (data)
VALUES ('data 1'),('data 2'),('data 3'),('data 4'),('data 5'),('data 6'),('data
7'),('data 8'),('data 9'),('data 10'),('data 11'),('data 12'),
... ('data 999'),('data 1000') RETURNING a.id

2020-06-27 19:08:18.175 EDT
[26960] LOG:  statement: INSERT INTO a (data) VALUES ('data 1001'),('data
1002'),('data 1003'),('data 1004'),('data 1005 '),('data 1006'),('data
1007'),('data 1008'),('data 1009'),('data 1010'),('data 1011'), ...

executemany_values_page_size Помощники быстрого выполнения Psycopg2 T

#5263

ORM Bulk Update и Delete используют RETURNING для стратегии «fetch», когда это доступно

Массовое обновление или удаление в ORM, использующее стратегию «fetch»:

sess.query(User).filter(User.age > 29).update(
    {"age": User.age - 10}, synchronize_session="fetch"
)

Теперь будет использоваться RETURNING, если внутренняя база данных поддерживает его; в настоящее время это PostgreSQL и SQL Server (диалект Oracle не поддерживает RETURNING нескольких строк):

UPDATE users SET age_int=(users.age_int - %(age_int_1)s) WHERE users.age_int > %(age_int_2)s RETURNING users.id
[generated in 0.00060s] {'age_int_1': 10, 'age_int_2': 29}
Col ('id',)
Row (2,)
Row (4,)

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

SELECT users.id FROM users WHERE users.age_int > %(age_int_1)s
[generated in 0.00043s] {'age_int_1': 29}
Col ('id',)
Row (2,)
Row (4,)
UPDATE users SET age_int=(users.age_int - %(age_int_1)s) WHERE users.age_int > %(age_int_2)s
[generated in 0.00102s] {'age_int_1': 10, 'age_int_2': 29}

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

В рамках этого изменения стратегия «fetch» также стала гораздо более эффективной, поскольку она больше не будет исключать атрибуты объектов, расположенных в строках, для выражений Python, используемых в предложении SET, которые могут быть оценены в Python; вместо этого они присваиваются непосредственно объекту так же, как и в стратегии «evaluate». Только для SQL-выражений, которые не могут быть оценены, происходит возврат к истечению срока действия атрибутов. Стратегия «evaluate» также была усовершенствована для возврата к «expire» для значений, которые не могут быть оценены.

Поведенческие изменения - ORM

Объект «KeyedTuple», возвращаемый запросом Query, заменяется на Row

RowProxy больше не является «прокси»; теперь он называется Row и ведет себя как расширенный именованный кортеж RowProxy Row Row Query A

Row Row Row._mapping Row LegacyRow Row Query T

:class:`.Row`E

row = s.query(User, Address).join(User.addresses).first()

row._mapping[User]  # same as row[0]
row._mapping[Address]  # same as row[1]
row._mapping["User"]  # same as row[0]
row._mapping["Address"]  # same as row[1]

u1 = aliased(User)
row = s.query(u1).only_return_tuples(True).first()
row._mapping[u1]  # same as row[0]


row = s.query(User.id, Address.email_address).join(User.addresses).first()

row._mapping[User.id]  # same as row[0]
row._mapping["id"]  # same as row[0]
row._mapping[users.c.id]  # same as row[0]

:ticket:`4710`<

В сессиях реализовано новое поведение «автозапуска»

Session autocommit=False SessionTransaction Session.rollback() Session.commit() P

SessionTransaction Session.add() Session.execute() Session.begin() autocommit=False _base.Connection T

Поведенческие изменения, на которые это указывает, таковы:

См.также

Автозапуск

Обоснование

Session autocommit=False SessionTransaction Session Session.transaction SessionTransaction SessionTransaction Session.transaction T

#5056 Session.close() Session SessionTransaction Session Session.commit() Session.close() H

Session.close() self.transaction self._transaction SessionTransaction Session.close() A

SessionEvents.after_transaction_create() Session SessionEvents.after_transaction_end() SessionEvents.after_transaction_create() Session SessionTransaction Session Session.add() Session.delete() Session.transaction Session.flush() I

Session.commit() Session.rollback() Session.expire_all() I

SessionEvents.after_transaction_create() Session.commit() Session.rollback() Session Session Session.close() Session Engine B

#5074

Отношения с возможностью просмотра не синхронизируют обратные ссылки

#5149 relationship.backref relationship.back_populates relationship.viewonly #5237 backref() I

relationship.sync_backref relationship.sync_backref relationship.viewonly Session T

class User(Base):
    # ...

    addresses = relationship(Address, backref=backref("user", viewonly=True))


class Address(Base):
    ...


u1 = session.query(User).filter_by(name="x").first()

a1 = Address()
a1.user = u1

a1 u1.addresses a1 relationship.sync_backref False relationship.viewonly False A

#5237

поведение cascade_backrefs deprecated для удаления в 2.0

Session User Session Address.user Address Address Session S

u1 = User()
session.add(u1)

a1 = Address()
a1.user = u1  # <--- adds "a1" to the Session

a1.user u1.addresses.append(a1) a1 Session relationship.cascade_backrefs backref.cascade_backrefs relationship.backref Session T

relationship.cascade_backrefs backref.cascade_backrefs False Session.future 2.0-style I

Session = sessionmaker(engine, future=True)

with Session() as session:
    u1 = User()
    session.add(u1)

    a1 = Address()
    a1.user = u1  # <--- will not add "a1" to the Session

#5150

Погрузчики излучают во время работы в нерабочем состоянии

Давно напрашивалась идея, что при обращении к объекту с истекшим сроком годности будут запущены сконфигурированные ускоренные загрузчики для ускоренной загрузки отношений на объекте с истекшим сроком годности, когда объект будет обновлен или иным образом не истек. Теперь это поведение добавлено, так что загрузчики join будут добавлять inline JOIN, как обычно, а загрузчики selectin/subquery будут выполнять операцию «immediateload» для заданного отношения, когда объект с истекшим сроком действия будет неактуален или объект будет обновлен:

>>> a1 = session.query(A).options(joinedload(A.bs)).first()
>>> a1.data = "new data"
>>> session.commit()

A joinedload() bs session.commit() .data A

>>> a1.data
{execsql}SELECT a.id AS a_id, a.data AS a_data, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id
FROM a LEFT OUTER JOIN b AS b_1 ON a.id = b_1.a_id
WHERE a.id = ?

relationship() Query.options() T

Для «вторичных» загрузчиков «selectinload» и «subqueryload» стратегия SQL для этих загрузчиков не является необходимой для ускоренной загрузки атрибутов одного объекта; поэтому они будут вызывать стратегию «immediateload» в сценарии обновления, который напоминает запрос, выдаваемый «lazyload», выдаваемый как дополнительный запрос:

>>> a1 = session.query(A).options(selectinload(A.bs)).first()
>>> a1.data = "new data"
>>> session.commit()
>>> a1.data
{execsql}SELECT a.id AS a_id, a.data AS a_data
FROM a
WHERE a.id = ?
(1,)
SELECT b.id AS b_id, b.a_id AS b_a_id
FROM b
WHERE ? = b.a_id
(1,)

Session a1 Session N

#1763

deferred() with_expression() C

Примечание

Данное примечание к изменению отсутствовало в предыдущих версиях этого документа, однако актуально для всех версий SQLAlchemy 1.4.

defer() with_expression() A

q1 = session.query(User).options(with_expression(User.expr, literal("u1")))
q2 = session.query(User).options(with_expression(User.expr, literal("u2")))

q1.union_all(q2).all()

:func:`_orm.with_expression`I

SELECT anon_1.anon_2 AS anon_1_anon_2, anon_1.user_account_id AS anon_1_user_account_id,
anon_1.user_account_name AS anon_1_user_account_name
FROM (
    SELECT ? AS anon_2, user_account.id AS user_account_id, user_account.name AS user_account_name
    FROM user_account
    UNION ALL
    SELECT ? AS anon_3, user_account.id AS user_account_id, user_account.name AS user_account_name
    FROM user_account
) AS anon_1
('u1', 'u2')

В SQLAlchemy 1.4 понятие опций загрузчика стало более строгим, и поэтому они применяются только к внешней части запроса, то есть к SELECT, который предназначен для наполнения реальных сущностей ORM, которые будут возвращены; приведенный выше запрос в 1.4 даст результат:

SELECT ? AS anon_1, anon_2.user_account_id AS anon_2_user_account_id,
anon_2.user_account_name AS anon_2_user_account_name
FROM (
    SELECT user_account.id AS user_account_id, user_account.name AS user_account_name
    FROM user_account
    UNION ALL
    SELECT user_account.id AS user_account_id, user_account.name AS user_account_name
    FROM user_account
) AS anon_2
('u1',)

:class:`_orm.Query`t

Обоснование

:func:`_orm.joinedload`T

Query Query Query.union_all() T

:func:`_orm.defer`S

q1 = session.query(User).options(defer(User.name))
q2 = session.query(User).options(defer(User.name))

q1.union_all(q2).all()

В версии 1.3 приходилось неловко добавлять NULL во внутренние запросы и затем SELECT:

SELECT anon_1.anon_2 AS anon_1_anon_2, anon_1.user_account_id AS anon_1_user_account_id
FROM (
    SELECT NULL AS anon_2, user_account.id AS user_account_id
    FROM user_account
    UNION ALL
    SELECT NULL AS anon_2, user_account.id AS user_account_id
    FROM user_account
) AS anon_1

Если бы во всех запросах не были заданы одинаковые параметры, то в приведенном выше сценарии возникла бы ошибка, связанная с невозможностью сформировать правильное UNION.

``User.name``W

SELECT anon_1.user_account_id AS anon_1_user_account_id
FROM (
    SELECT user_account.id AS user_account_id, user_account.name AS user_account_name
    FROM user_account
    UNION ALL
    SELECT user_account.id AS user_account_id, user_account.name AS user_account_name
    FROM user_account
) AS anon_1

Правильный подход

2.0-style with_expression() with_expression() U

s1 = select(User).options(with_expression(User.expr, literal("u1")))
s2 = select(User).options(with_expression(User.expr, literal("u2")))

stmt = union_all(s1, s2)

session.scalars(select(User).from_statement(stmt)).all()

производство SQL:

SELECT user_account.id, user_account.name
FROM user_account
UNION ALL
SELECT user_account.id, user_account.name
FROM user_account

with_expression() User T

s1 = select(User, literal("u1").label("some_literal"))
s2 = select(User, literal("u2").label("some_literal"))

stmt = union_all(s1, s2)

session.scalars(
    select(User)
    .from_statement(stmt)
    .options(with_expression(User.expr, stmt.selected_columns.some_literal))
).all()

В результате чего будет получен ожидаемый SQL:

SELECT user_account.id, user_account.name, ? AS some_literal
FROM user_account
UNION ALL
SELECT user_account.id, user_account.name, ? AS some_literal
FROM user_account

User User.expr T

Доступ к неинициализированному атрибуту коллекции на переходном объекте больше не мутирует __dict__.

AttributeError None [] I

>>> u1 = User()
>>> u1.name
None
>>> u1.addresses
[]

``None``T

>>> u1 = User()
>>> u1.addresses.append(Address())  # no need to assign u1.addresses = []

None __dict__ U

>>> u1 = User()
>>> u1.name = None  # explicit assignment

>>> u2 = User()
>>> u2.name  # implicit assignment just by accessing it
None

u1 u2 None name W

Изменения в событиях атрибутов и другие операции над атрибутами, не имеющими заранее существующего значения None __dict__ None I

:ticket:`4519`T

>>> u1 = User(id=1)  # create an empty User to merge with id=1 in the database
>>> merged1 = session.merge(
...     u1
... )  # value of merged1.addresses is unchanged from that of the DB

>>> u2 = User(id=2)  # create an empty User to merge with id=2 in the database
>>> u2.addresses
[]
>>> merged2 = session.merge(u2)  # value of merged2.addresses has been emptied in the DB

.addresses merged1 Address() merged2 .addresses A

``AttributeError``W

``__dict__``T

>>> u1 = User()
>>> l1 = u1.addresses  # new list is created, associated with the state
>>> assert u1.addresses is l1  # you get the same list each time you access it
>>> assert (
...     "addresses" not in u1.__dict__
... )  # but it won't go into __dict__ until it's mutated
>>> from sqlalchemy import inspect
>>> inspect(u1).attrs.addresses.history
History(added=None, unchanged=None, deleted=None)

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

>>> l1.append(Address())
>>> assert "addresses" in u1.__dict__
>>> inspect(u1).attrs.addresses.history
History(added=[<__main__.Address object at 0x7f49b725eda0>], unchanged=[], deleted=[])

``__dict__``T

>>> u1 = User()
>>> u1.addresses
[]
# this will now fail, would pass before
>>> assert {k: v for k, v in u1.__dict__.items() if not k.startswith("_")} == {
...     "addresses": []
... }

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

>>> u1 = User()
>>> u1.addresses
[]
>>> s.add(u1)
>>> s.flush()
>>> s.close()
>>> u1.addresses  # <-- will fail, .addresses is not loaded and object is detached

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

>>> u1.addresses = []

#4519

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

:class:`.Session`S

class Product(Base):
    __tablename__ = "product"

    id = Column(Integer, primary_key=True)


session = Session(engine)

# add Product with primary key 1
session.add(Product(id=1))
session.flush()

# add another Product with same primary key
session.add(Product(id=1))
s.commit()  # <-- will raise FlushError

:class:`.FlushError`T

sqlalchemy/orm/persistence.py:408: SAWarning: New instance <Product at 0x7f1ff65e0ba8> with identity key (<class '__main__.Product'>, (1,), None) conflicts with persistent instance <Product at 0x7f1ff60a4550>

IntegrityError Session S

sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: product.id

IntegrityError Session T

# add another Product with same primary key
try:
    with session.begin_nested():
        session.add(Product(id=1))
except exc.IntegrityError:
    print("row already exists")

Product Session FlushError T

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

#4662

Более строгое поведение при запросе отображений наследования с помощью пользовательских запросов

:class:`_query.Query`T

Employee Engineer(Employee) Manager(Employee) G

s = Session(e)

s.add_all([Engineer(), Manager()])

s.commit()

print(s.query(Manager).select_entity_from(s.query(Employee).subquery()).all())

Engineer Manager Manager Manager T

SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee) AS anon_1
2020-01-29 18:04:13,524 INFO sqlalchemy.engine.base.Engine ()
[<__main__.Engineer object at 0x7f7f5b9a9810>, <__main__.Manager object at 0x7f7f5b9a9750>]

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

sqlalchemy.exc.InvalidRequestError: Row with identity key
(<class '__main__.Employee'>, (1,), None) can't be loaded into an object;
the polymorphic discriminator column '%(140205120401296 anon)s.type'
refers to mapped class Engineer->engineer, which is not a sub-mapper of
the requested mapped class Manager->manager

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

Engineer Manager Manager I

SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee) AS anon_1
WHERE anon_1.type IN (?)
2020-01-29 18:08:32,975 INFO sqlalchemy.engine.base.Engine ('manager',)
[<__main__.Manager object at 0x7ff1b0200d50>]

Query #3891 T

SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee) AS anon_1
2020-01-29 18:13:10,554 INFO sqlalchemy.engine.base.Engine ()
Traceback (most recent call last):
# ...
sqlalchemy.exc.InvalidRequestError: Row with identity key
(<class '__main__.Employee'>, (1,), None) can't be loaded into an object;
the polymorphic discriminator column '%(140700085268432 anon)s.type'
refers to mapped class Engineer->employee, which is not a sub-mapper of
the requested mapped class Manager->employee

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

print(
    s.query(Manager)
    .select_entity_from(
        s.query(Employee).filter(Employee.discriminator == "manager").subquery()
    )
    .all()
)
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee
WHERE employee.type = ?) AS anon_1
2020-01-29 18:14:49,770 INFO sqlalchemy.engine.base.Engine ('manager',)
[<__main__.Manager object at 0x7f70e13fca90>]

#5122

Диалектные изменения

Минимальная версия pg8000 - 1.16.6, поддерживает только Python 3

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

``SQLAlchemy<1.4``D

#5451

Для работы с диалектом PostgreSQL psycopg2 требуется версия 2.7 или выше

Диалект psycopg2 опирается на многие возможности psycopg2, появившиеся за последние несколько лет. Для упрощения диалекта минимально необходимой версией теперь является версия 2.7, выпущенная в марте 2017 года.

Диалект psycopg2 больше не имеет ограничений на имена связанных параметров

Column.key create_engine() S

#5941

#5653

В диалекте psycopg2 по умолчанию реализована функция «execute_values» с RETURNING для операторов INSERT

psycopg2.extras.execute_values() ORM Пакетные вставки с psycopg2 теперь в большинстве случаев выполняют пакетные операции с RETURNING T

insert() Insert.values() Connection.execute() T

execute_values() bulk_inserts.py Производительность A

$ python -m examples.performance bulk_inserts --test test_core_insert --num 100000 --dburl postgresql://scott:tiger@localhost/test

# 1.3
test_core_insert : A single Core INSERT construct inserting mappings in bulk. (100000 iterations); total time 5.229326 sec

# 1.4
test_core_insert : A single Core INSERT construct inserting mappings in bulk. (100000 iterations); total time 0.944007 sec

Поддержка пакетного режима / помощников быстрого выполнения execute_values #4623 execute_values S

execute_values insert() Insert.returning() I

``executemany_mode``T

  • "values_only" psycopg2.extras.execute_values() execute_batch() A

  • "values" "values_plus_batch" execute_values execute_batch cursor.rowcount executemany() T

  • "values_only" "values" R

  • execute_values execute_batch T

  • use_batch_mode executemany_mode T

  • CursorResult.inserted_primary_key_rows CursorResult.returned_default_rows T

#5401

Удалена логика «переписывания соединений» из диалекта SQLite; обновлен импорт

Для поддержки старых версий SQLite до 3.7.16, выпущенных в 2013 году, была отменена поддержка переписывания вложенных справа соединений. Не ожидается, что современные версии Python будут использовать это ограничение.

:ref:`feature_joins_09`T

Из сопутствующих изменений: модуль импорта для SQLite больше не пытается импортировать драйвер «pysqlite2» на Python 3, так как этот драйвер не существует на Python 3; также отменено очень старое предупреждение для старых версий pysqlite2.

#4895

Добавлена поддержка последовательностей для MariaDB 10.3

Sequence Sequence Table MetaData Sequence T

Sequence Sequence Sequence.optional Sequence Sequence S

Table(
    "some_table",
    metadata,
    Column(
        "id", Integer, Sequence("some_seq", start=1, optional=True), primary_key=True
    ),
)

Sequence Sequence T

#4976

В SQL Server добавлена поддержка последовательностей, отличных от IDENTITY

Sequence Column T

Sequence mssql_identity_start mssql_identity_increment T

#4235

#4633

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