Переход на SQLAlchemy 2.0

Об этом документе

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

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

Обзор

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

В качестве средства как для проверки архитектуры 2.0, так и для обеспечения полностью итеративной среды перехода, весь объем новых API и возможностей 2.0 присутствует и доступен в серии 1.4; это включает такие основные новые области функциональности, как система кэширования SQL, новая модель выполнения операторов ORM, новые парадигмы транзакций для ORM и Core, новая декларативная система ORM, которая объединяет классическое и декларативное отображение, поддержка классов данных Python и поддержка asyncio для Core и ORM.

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

Первое условие, шаг первый - работающее приложение 1.3

Первым шагом в переводе существующего приложения на версию 1.4, в случае типичного нетривиального приложения, является обеспечение его работы на SQLAlchemy 1.3 без предупреждений об устаревании. Релиз 1.4 действительно имеет несколько изменений, связанных с условиями, предупреждающими в предыдущей версии, включая некоторые предупреждения, которые были введены в 1.3, в частности, некоторые изменения в поведении флагов relationship.viewonly и relationship.sync_backref.

Для достижения наилучших результатов приложение должно работать или проходить все тесты с последней версией SQLAlchemy 1.3 без предупреждений об устаревании SQLAlchemy; это предупреждения, выдаваемые для класса SADeprecationWarning.

Первая предпосылка, шаг второй - работающее приложение 1.4

Как только приложение будет готово к работе на SQLAlchemy 1.3, следующий шаг - запустить его на SQLAlchemy 1.4. В подавляющем большинстве случаев приложения должны работать без проблем с SQLAlchemy 1.3 до 1.4. Однако между релизами 1.x и 1.y всегда происходит так, что API и поведение изменяются либо незначительно, либо, в некоторых случаях, менее значительно, и проект SQLAlchemy всегда получает большое количество отчетов о регрессиях в течение первых нескольких месяцев.

Процесс выпуска 1.x->1.y обычно имеет несколько изменений на полях, которые немного более драматичны и основаны на случаях использования, которые, как ожидается, будут использоваться очень редко, если вообще будут использоваться. Для 1.4 изменения, отнесенные к этой области, выглядят следующим образом:

  • The URL object is now immutable - это влияет на код, который будет манипулировать объектом URL и может повлиять на код, использующий точку расширения CreateEnginePlugin. Это редкий случай, но он может повлиять, в частности, на некоторые тестовые наборы, использующие специальную логику обеспечения базы данных. Поиск на github кода, использующего относительно новый и малоизвестный класс CreateEnginePlugin, обнаружил два проекта, на которые изменение не повлияло.

  • A SELECT statement is no longer implicitly considered to be a FROM clause - это изменение может повлиять на код, который каким-то образом полагался на поведение, которое было в основном непригодным в конструкции Select, где создавались безымянные подзапросы, которые обычно были запутанными и нерабочими. Такие подзапросы в любом случае будут отвергнуты большинством баз данных, поскольку имя обычно требуется, за исключением SQLite, однако возможно, что некоторым приложениям придется скорректировать некоторые запросы, которые непреднамеренно полагались на это.

  • select().join() and outerjoin() add JOIN criteria to the current query, rather than creating a subquery - в некоторой степени связано с этим, класс Select содержал методы .join() и .outerjoin(), которые неявно создавали подзапрос и затем возвращали конструкцию Join, что опять же было в основном бесполезно и создавало много путаницы. Было принято решение перейти на гораздо более полезный подход к созданию объединений в стиле 2.0, где эти методы теперь работают так же, как метод ORM Query.join().

  • Many Core and ORM statement objects now perform much of their construction and validation in the compile phase - некоторые сообщения об ошибках, связанные с построением Query или Select, могут выдаваться только после компиляции/выполнения, а не во время построения. Это может повлиять на некоторые тестовые наборы, тестирующие режимы отказов.

Полный обзор изменений SQLAlchemy 1.4 приведен в документе What’s New in SQLAlchemy 1.4?.

Миграция на 2.0 Шаг первый - только Python 3 (минимум Python 3.7 для совместимости с 2.0)

SQLAlchemy 2.0 был впервые вдохновлен тем фактом, что EOL Python 2 наступает в 2020 году. SQLAlchemy требуется больше времени, чем другим крупным проектам, чтобы отказаться от поддержки Python 2.7. Однако, чтобы использовать SQLAlchemy 2.0, приложение должно работать как минимум на Python 3.7. SQLAlchemy 1.4 поддерживает Python 3.6 или более новую версию в рамках серии Python 3; на протяжении всей серии 1.4 приложение может работать на Python 2.7 или, по крайней мере, на Python 3.6. Версия 2.0, однако, запускается на Python 3.7.

Миграция на 2.0 Шаг второй - Включите функцию RemovedIn20Warnings

В SQLAlchemy 1.4 реализована система условного предупреждения об устаревании, вдохновленная флагом Python «-3», который указывает на устаревшие шаблоны в работающем приложении. В SQLAlchemy 1.4 класс устаревания RemovedIn20Warning выдается только тогда, когда переменная окружения SQLALCHEMY_WARN_20 установлена в одно из значений true или 1.

Приведенный ниже пример программы:

from sqlalchemy import column
from sqlalchemy import create_engine
from sqlalchemy import select
from sqlalchemy import table


engine = create_engine("sqlite://")

engine.execute("CREATE TABLE foo (id integer)")
engine.execute("INSERT INTO foo (id) VALUES (1)")


foo = table("foo", column("id"))
result = engine.execute(select([foo.c.id]))

print(result.fetchall())

Приведенная выше программа использует несколько паттернов, которые многие пользователи уже идентифицируют как «наследие», а именно использование метода Engine.execute(), который является частью API «выполнение без соединения». Когда мы запускаем приведенную выше программу в версии 1.4, она возвращает единственную строку:

$ python test3.py
[(1,)]

Чтобы включить «режим 2.0 deprecations», мы включаем переменную SQLALCHEMY_WARN_20=1 и дополнительно убеждаемся, что выбрана переменная warnings filter, которая не будет подавлять никаких предупреждений:

SQLALCHEMY_WARN_20=1 python -W always::DeprecationWarning test3.py

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

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

$ SQLALCHEMY_WARN_20=1 python2 -W always::DeprecationWarning test3.py
test3.py:9: RemovedIn20Warning: The Engine.execute() function/method is considered legacy as of the 1.x series of SQLAlchemy and will be removed in 2.0. All statement execution in SQLAlchemy 2.0 is performed by the Connection.execute() method of Connection, or in the ORM by the Session.execute() method of Session. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9) (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  engine.execute("CREATE TABLE foo (id integer)")
/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py:2856: RemovedIn20Warning: Passing a string to Connection.execute() is deprecated and will be removed in version 2.0.  Use the text() construct, or the Connection.exec_driver_sql() method to invoke a driver-level SQL string. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  return connection.execute(statement, *multiparams, **params)
/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py:1639: RemovedIn20Warning: The current statement is being autocommitted using implicit autocommit.Implicit autocommit will be removed in SQLAlchemy 2.0.   Use the .begin() method of Engine or Connection in order to use an explicit transaction for DML and DDL statements. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  self._commit_impl(autocommit=True)
test3.py:10: RemovedIn20Warning: The Engine.execute() function/method is considered legacy as of the 1.x series of SQLAlchemy and will be removed in 2.0. All statement execution in SQLAlchemy 2.0 is performed by the Connection.execute() method of Connection, or in the ORM by the Session.execute() method of Session. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9) (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  engine.execute("INSERT INTO foo (id) VALUES (1)")
/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py:2856: RemovedIn20Warning: Passing a string to Connection.execute() is deprecated and will be removed in version 2.0.  Use the text() construct, or the Connection.exec_driver_sql() method to invoke a driver-level SQL string. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  return connection.execute(statement, *multiparams, **params)
/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py:1639: RemovedIn20Warning: The current statement is being autocommitted using implicit autocommit.Implicit autocommit will be removed in SQLAlchemy 2.0.   Use the .begin() method of Engine or Connection in order to use an explicit transaction for DML and DDL statements. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  self._commit_impl(autocommit=True)
/home/classic/dev/sqlalchemy/lib/sqlalchemy/sql/selectable.py:4271: RemovedIn20Warning: The legacy calling style of select() is deprecated and will be removed in SQLAlchemy 2.0.  Please use the new calling style described at select(). (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9) (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  return cls.create_legacy_select(*args, **kw)
test3.py:14: RemovedIn20Warning: The Engine.execute() function/method is considered legacy as of the 1.x series of SQLAlchemy and will be removed in 2.0. All statement execution in SQLAlchemy 2.0 is performed by the Connection.execute() method of Connection, or in the ORM by the Session.execute() method of Session. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9) (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
  result = engine.execute(select([foo.c.id]))
[(1,)]

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

from sqlalchemy import column
from sqlalchemy import create_engine
from sqlalchemy import select
from sqlalchemy import table
from sqlalchemy import text


engine = create_engine("sqlite://")

# don't rely on autocommit for DML and DDL
with engine.begin() as connection:
    # use connection.execute(), not engine.execute()
    # use the text() construct to execute textual SQL
    connection.execute(text("CREATE TABLE foo (id integer)"))
    connection.execute(text("INSERT INTO foo (id) VALUES (1)"))


foo = table("foo", column("id"))

with engine.connect() as connection:
    # use connection.execute(), not engine.execute()
    # select() now accepts column / table expressions positionally
    result = connection.execute(select(foo.c.id))

print(result.fetchall())

Цель «2.0 deprecations mode» заключается в том, чтобы программа, которая работает без предупреждений RemovedIn20Warning с включенным «2.0 deprecations mode», была готова к работе в SQLAlchemy 2.0.

Миграция на 2.0 Шаг третий - Устранение всех предупреждений RemovedIn20Warnings

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

  1. включить переменную окружения SQLALCHEMY_WARN_20=1 в тестовом наборе, для SQLAlchemy это делается в файле tox.ini

  2. В настройках тестового набора установите ряд фильтров предупреждений, которые будут выбирать для определенных подгрупп предупреждений либо вызывать исключение, либо игнорироваться (или записываться в журнал). Работайте только с одной подгруппой предупреждений за один раз. Ниже фильтр предупреждений настроен для приложения, в котором изменение вызовов уровня Core .execute() будет необходимо для прохождения всех тестов, но все остальные предупреждения в стиле 2.0 будут подавлены:

    import warnings
    from sqlalchemy import exc
    
    # for warnings not included in regex-based filter below, just log
    warnings.filterwarnings("always", category=exc.RemovedIn20Warning)
    
    # for warnings related to execute() / scalar(), raise
    for msg in [
        r"The (?:Executable|Engine)\.(?:execute|scalar)\(\) function",
        r"The current statement is being autocommitted using implicit " "autocommit,",
        r"The connection.execute\(\) method in SQLAlchemy 2.0 will accept "
        "parameters as a single dictionary or a single sequence of "
        "dictionaries only.",
        r"The Connection.connect\(\) function/method is considered legacy",
        r".*DefaultGenerator.execute\(\)",
    ]:
        warnings.filterwarnings(
            "error",
            message=msg,
            category=exc.RemovedIn20Warning,
        )
  3. По мере разрешения каждой подкатегории предупреждений в приложении, новые предупреждения, отловленные фильтром «всегда», могут быть добавлены в список «ошибок», подлежащих разрешению.

  4. Если предупреждения больше не выдаются, фильтр можно удалить.

Миграция на 2.0 Шаг четвертый - Используйте флаг future на Engine

В версии 2.0 объект Engine имеет обновленный API уровня транзакций. В версии 1.4 этот новый API доступен при передаче флага future=True в функцию create_engine().

При использовании флага create_engine.future объекты Engine и Connection полностью поддерживают API 2.0 и совсем не поддерживают унаследованные возможности, включая новый формат аргумента для Connection.execute(), удаление «неявного автокоммита», строковые операторы требуют конструкции text(), если не используется метод Connection.exec_driver_sql(), а выполнение без соединения из Engine удалено.

Если все предупреждения RemovedIn20Warning относительно использования Engine и Connection были устранены, то флаг create_engine.future может быть включен, и ошибок возникать не должно.

Новый механизм описан в Engine, который предоставляет новый объект Connection. В дополнение к вышеуказанным изменениям, объект Connection имеет методы Connection.commit() и Connection.rollback() для поддержки нового режима работы «commit-as-you-go»:

from sqlalchemy import create_engine

engine = create_engine("postgresql:///")

with engine.connect() as conn:
    conn.execute(text("insert into table (x) values (:some_x)"), {"some_x": 10})

    conn.commit()  # commit as you go

Миграция на 2.0 Шаг пятый - Использование флага future на сессии

В версии 2.0 объект Session также имеет обновленный API уровня транзакций/соединений. Этот API доступен в версии 1.4 с помощью флага Session.future на Session или на sessionmaker.

Объект Session поддерживает режим «будущего» на месте и включает эти изменения:

  1. Session больше не поддерживает «связанные метаданные» при определении движка, который будет использоваться для подключения. Это означает, что в конструктор **должен быть передан объект Engine (это может быть объект устаревшего или будущего стиля).

  2. Флаг Session.begin.subtransactions больше не поддерживается.

  3. Метод Session.commit() всегда отправляет COMMIT в базу данных, а не пытается согласовать «субтранзакции».

  4. Метод Session.rollback() всегда откатывает сразу весь стек транзакций, а не пытается сохранить «подтранзакции» на месте.

В версии 1.4 Session также поддерживает более гибкие шаблоны создания, которые теперь тесно связаны с шаблонами, используемыми объектом Connection. Основные моменты включают в себя то, что Session может использоваться в качестве менеджера контекста:

from sqlalchemy.orm import Session

with Session(engine) as session:
    session.add(MyObject())
    session.commit()

Кроме того, объект sessionmaker поддерживает менеджер контекста sessionmaker.begin(), который создаст Session и начнет/зафиксирует транзакцию в одном блоке:

from sqlalchemy.orm import sessionmaker

Session = sessionmaker(engine)

with Session.begin() as session:
    session.add(MyObject())

См. раздел Управление транзакциями на уровне сеанса и на уровне двигателя для сравнения креационных моделей Session по сравнению с моделями Connection.

Как только приложение пройдет все тесты/запустится с SQLALCHEMY_WARN_20=1 и все вхождения exc.RemovedIn20Warning будут вызывать ошибку, приложение готово!.

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

2.0 Миграция - основное соединение / транзакция

«Автокоммит» на уровне библиотеки (но не на уровне драйвера) удален из Core и ORM

Синопсис

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

conn = engine.connect()

# won't autocommit in 2.0
conn.execute(some_table.insert().values(foo="bar"))

Это также не приведет к автокоммиту:

conn = engine.connect()

# won't autocommit in 2.0
conn.execute(text("INSERT INTO table (foo) VALUES ('bar')"))

Обычное обходное решение для пользовательских DML, требующих фиксации, - опция выполнения «autocommit» - будет удалена:

conn = engine.connect()

# won't autocommit in 2.0
conn.execute(text("EXEC my_procedural_thing()").execution_options(autocommit=True))

Миграция на 2.0

Метод, совместимый с выполнением 1.x style и 2.0 style, заключается в использовании метода Connection.begin() или менеджера контекста Engine.begin():

with engine.begin() as conn:
    conn.execute(some_table.insert().values(foo="bar"))
    conn.execute(some_other_table.insert().values(bat="hoho"))

with engine.connect() as conn:
    with conn.begin():
        conn.execute(some_table.insert().values(foo="bar"))
        conn.execute(some_other_table.insert().values(bat="hoho"))

with engine.begin() as conn:
    conn.execute(text("EXEC my_procedural_thing()"))

При использовании 2.0 style с флагом create_engine.future можно также использовать стиль «commit as you go», поскольку Connection имеет поведение autobegin, которое происходит при первом вызове оператора в отсутствие явного вызова Connection.begin():

with engine.connect() as conn:
    conn.execute(some_table.insert().values(foo="bar"))
    conn.execute(some_other_table.insert().values(bat="hoho"))

    conn.commit()

Когда 2.0 deprecations mode включено, при использовании устаревшей функции «autocommit» будет выдаваться предупреждение, указывающее на те места, где следует отметить явную транзакцию.

Дискуссия

Первые релизы SQLAlchemy противоречили духу Python DBAPI (PEP 249), поскольку пытались скрыть акцент PEP 249 на «неявном начале» и «явной фиксации» транзакций. Пятнадцать лет спустя мы видим, что это было ошибкой, поскольку множество паттернов SQLAlchemy, которые пытаются «скрыть» присутствие транзакции, делают более сложный API, который работает непоследовательно и крайне запутанно для тех пользователей, которые являются новичками в реляционных базах данных и ACID транзакциях в целом. В SQLAlchemy 2.0 будут отменены все попытки неявной фиксации транзакций, а шаблоны использования всегда будут требовать, чтобы пользователь каким-то образом обозначал «начало» и «конец» транзакции, точно так же, как чтение или запись в файл в Python имеет «начало» и «конец».

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

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

with engine.begin() as conn:
    conn.execute(some_table.insert().values(foo="bar"))

Для использования «фиксация по ходу дела или откат вместо этого», что напоминает то, как Session обычно используется сегодня, «будущая» версия Connection, которая возвращается из Engine, созданного с использованием флага create_engine.future, включает новые методы Connection.commit() и Connection.rollback(), которые действуют на транзакцию, которая теперь начинается автоматически при первом вызове оператора:

# 1.4 / 2.0 code

from sqlalchemy import create_engine

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

with engine.connect() as conn:
    conn.execute(some_table.insert().values(foo="bar"))
    conn.commit()

    conn.execute(text("some other SQL"))
    conn.rollback()

Выше, метод engine.connect() вернет Connection, который имеет функцию autobegin, что означает, что событие begin() испускается при первом использовании метода execute (обратите внимание, что в Python DBAPI нет фактического «BEGIN»). «autobegin» - это новый паттерн в SQLAlchemy 1.4, который поддерживается как Connection, так и объектом ORM Session; autobegin позволяет явно вызывать метод Connection.begin() при первом получении объекта для схем, которые хотят обозначить начало транзакции, но если метод не вызывается, то это происходит неявно при первом выполнении работы над объектом.

Удаление «автокоммита» тесно связано с удалением выполнения «без соединения», о котором говорилось в «Неявное» и «Бесконтактное» выполнение, «связанные метаданные» удалены. Все эти унаследованные шаблоны возникли из-за того, что в Python не было менеджеров контекста или декораторов, когда SQLAlchemy только создавался, поэтому не было удобных идиоматических шаблонов для разграничения использования ресурса.

Автокоммит на уровне драйвера остается доступным

Истинное поведение «autocommit» теперь широко доступно в большинстве реализаций DBAPI, и поддерживается SQLAlchemy через параметр Connection.execution_options.isolation_level, как обсуждалось в Установка уровней изоляции транзакций, включая DBAPI Autocommit. Истинный автокоммит рассматривается как «уровень изоляции», так что структура кода приложения не меняется при использовании автокоммита; менеджер контекста Connection.begin(), а также такие методы, как Connection.commit(), могут по-прежнему использоваться, они просто не работают на уровне драйвера базы данных, когда включен автокоммит на уровне DBAPI.

«Неявное» и «Бесконтактное» выполнение, «связанные метаданные» удалены

Синопсис

Возможность связать Engine с объектом MetaData, который затем делает доступным ряд так называемых «бесконнектных» шаблонов выполнения, удалена:

from sqlalchemy import MetaData

metadata_obj = MetaData(bind=engine)  # no longer supported

metadata_obj.create_all()  # requires Engine or Connection

metadata_obj.reflect()  # requires Engine or Connection

t = Table("t", metadata_obj, autoload=True)  # use autoload_with=engine

result = engine.execute(t.select())  # no longer supported

result = t.select().execute()  # no longer supported

Миграция на 2.0

Для шаблонов уровня схемы требуется явное использование Engine или Connection. Объект Engine может по-прежнему использоваться непосредственно как источник связности для операции MetaData.create_all() или операции автозагрузки. Для выполнения операторов только объект Connection имеет метод Connection.execute() (в дополнение к методу Session.execute() уровня ORM):

from sqlalchemy import MetaData

metadata_obj = MetaData()

# engine level:

# create tables
metadata_obj.create_all(engine)

# reflect all tables
metadata_obj.reflect(engine)

# reflect individual table
t = Table("t", metadata_obj, autoload_with=engine)


# connection level:


with engine.connect() as connection:
    # create tables, requires explicit begin and/or commit:
    with connection.begin():
        metadata_obj.create_all(connection)

    # reflect all tables
    metadata_obj.reflect(connection)

    # reflect individual table
    t = Table("t", metadata_obj, autoload_with=connection)

    # execute SQL statements
    result = conn.execute(t.select())

Дискуссия

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

«Выполнение без подключения относится к все еще довольно популярной схеме вызова .execute() из Engine:

result = engine.execute(some_statement)

Приведенная выше операция неявно получает объект Connection и запускает на нем метод .execute(). Хотя это кажется простой удобной функцией, как было показано, она порождает несколько проблем:

  • Программы с расширенными строками вызовов engine.execute() получили широкое распространение, чрезмерно используя функцию, которая должна была использоваться редко, и приводя к неэффективным нетранзакционным приложениям. Новые пользователи путаются в разнице между engine.execute() и connection.execute(), и нюансы между этими двумя подходами часто не понятны.

  • Эта функция полагается на функцию «автокоммит на уровне приложения», которая также удаляется, поскольку она тоже inefficient and misleading.

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

Устранение выполнения «без соединения» приводит к устранению еще более унаследованного шаблона - «неявного выполнения без соединения»:

result = some_statement.execute()

Приведенная выше модель имеет все проблемы выполнения «без соединения», плюс она полагается на модель «связанных метаданных», которую SQLAlchemy пыталась отменить в течение многих лет. Это была самая первая разрекламированная модель использования SQLAlchemy в версии 0.1, которая устарела почти сразу после появления объекта Connection и более поздних контекстных менеджеров Python, обеспечивших лучшую модель использования ресурсов в фиксированной области видимости.

С удалением неявного выполнения, «связанные метаданные» сами по себе также больше не имеют смысла в этой системе. В современном использовании «связанные метаданные» по-прежнему удобны для работы внутри вызовов MetaData.create_all(), а также с объектами Session, однако явное получение этими функциями Engine обеспечивает более четкий дизайн приложения.

Много вариантов становится одним вариантом

В целом, вышеперечисленные шаблоны выполнения были представлены в самом первом выпуске 0.1 SQLAlchemy еще до появления объекта Connection. После многих лет ослабления внимания к этим паттернам, «неявное выполнение без подключения» и «связанные метаданные» уже не так широко используются, поэтому в версии 2.0 мы стремимся окончательно сократить количество вариантов выполнения оператора в Core с «многих вариантов»:

# many choices

# bound metadata?
metadata_obj = MetaData(engine)

# or not?
metadata_obj = MetaData()

# execute from engine?
result = engine.execute(stmt)

# or execute the statement itself (but only if you did
# "bound metadata" above, which means you can't get rid of "bound" if any
# part of your program uses this form)
result = stmt.execute()

# execute from connection, but it autocommits?
conn = engine.connect()
conn.execute(stmt)

# execute from connection, but autocommit isn't working, so use the special
# option?
conn.execution_options(autocommit=True).execute(stmt)

# or on the statement ?!
conn.execute(stmt.execution_options(autocommit=True))

# or execute from connection, and we use explicit transaction?
with conn.begin():
    conn.execute(stmt)

к «одному варианту», где под «одним вариантом» подразумевается «явное соединение с явной транзакцией»; существует еще несколько способов демаркации блоков транзакций в зависимости от необходимости. Один из вариантов» заключается в получении Connection, а затем в явном разграничении транзакции, в случае, если операция является операцией записи:

# one choice - work with explicit connection, explicit transaction
# (there remain a few variants on how to demarcate the transaction)

# "begin once" - one transaction only per checkout
with engine.begin() as conn:
    result = conn.execute(stmt)

# "commit as you go" - zero or more commits per checkout
with engine.connect() as conn:
    result = conn.execute(stmt)
    conn.commit()

# "commit as you go" but with a transaction block instead of autobegin
with engine.connect() as conn:
    with conn.begin():
        result = conn.execute(stmt)

метод execute() более строгий, опции выполнения более заметны

Синопсис

Шаблоны аргументов, которые можно использовать с методом sqlalchemy.engine.Connection() execute в SQLAlchemy 2.0, сильно упрощены, удалены многие ранее доступные шаблоны аргументов. Новый API в версии 1.4 описан в sqlalchemy.future.Connection(). Приведенные ниже примеры иллюстрируют шаблоны, требующие модификации:

connection = engine.connect()

# direct string SQL not supported; use text() or exec_driver_sql() method
result = connection.execute("select * from table")

# positional parameters no longer supported, only named
# unless using exec_driver_sql()
result = connection.execute(table.insert(), ("x", "y", "z"))

# **kwargs no longer accepted, pass a single dictionary
result = connection.execute(table.insert(), x=10, y=5)

# multiple *args no longer accepted, pass a list
result = connection.execute(
    table.insert(), {"x": 10, "y": 5}, {"x": 15, "y": 12}, {"x": 9, "y": 8}
)

Миграция на 2.0

Новый метод Connection.execute() теперь принимает подмножество стилей аргументов, которые принимаются методом 1.x Connection.execute(), поэтому следующий код совместим между 1.x и 2.0:

connection = engine.connect()

from sqlalchemy import text

result = connection.execute(text("select * from table"))

# pass a single dictionary for single statement execution
result = connection.execute(table.insert(), {"x": 10, "y": 5})

# pass a list of dictionaries for executemany
result = connection.execute(
    table.insert(), [{"x": 10, "y": 5}, {"x": 15, "y": 12}, {"x": 9, "y": 8}]
)

Дискуссия

Использование *args и **kwargs было удалено как для устранения сложности угадывания того, какие аргументы были переданы методу, так и для освобождения места для других опций, а именно словаря Connection.execute.execution_options, который теперь доступен для предоставления опций на основе каждого оператора. Метод также изменен таким образом, чтобы его использование совпадало с использованием метода Session.execute(), который является более заметным API в стиле 2.0.

Исключение прямого строкового SQL призвано устранить несоответствие между Connection.execute() и Session.execute(), где в первом случае строка передается драйверу необработанной, а во втором случае она сначала преобразуется в конструкцию text(). Разрешая только text(), это также ограничивает принимаемый формат параметров «именованным», а не «позиционным». Наконец, строковый SQL становится все более подверженным проверке с точки зрения безопасности, и конструкция text() стала представлять собой явную границу в текстовой области SQL, где необходимо уделять внимание ненадежному пользовательскому вводу.

Строки результатов действуют как именованные кортежи

Синопсис

Версия 1.4 вводит all new Result object, который в свою очередь возвращает объекты Row, которые ведут себя как именованные кортежи при использовании режима «future»:

engine = create_engine(..., future=True)  # using future mode

with engine.connect() as conn:
    result = conn.execute(text("select x, y from table"))

    row = result.first()  # suppose the row is (1, 2)

    "x" in row  # evaluates to False, in 1.x / future=False, this would be True

    1 in row  # evaluates to True, in 1.x / future=False, this would be False

Миграция на 2.0

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

Дискуссия

Уже в рамках 1. 4, предыдущий класс KeyedTuple, который использовался при выборе строк из объекта Query, был заменен классом Row, который является основой того же Row, который возвращается с результатами операторов Core при использовании флага create_engine.future с Engine (когда флаг create_engine.future не установлен, наборы результатов Core используют подкласс LegacyRow, который поддерживает обратно совместимое поведение для метода __contains__(); ORM использует исключительно класс Row напрямую).

Этот Row ведет себя как именованный кортеж, в том смысле, что он действует как последовательность, но также поддерживает доступ по имени атрибута, например, row.some_column. Однако он также обеспечивает предыдущее поведение «отображения» через специальный атрибут row._mapping, который создает отображение Python, чтобы можно было использовать доступ по ключу, например row["some_column"].

Для получения результатов в виде отображений можно использовать модификатор mappings() на результат:

from sqlalchemy.future.orm import Session

session = Session(some_engine)

result = session.execute(stmt)
for row in result.mappings():
    print("the user is: %s" % row["User"])

Класс Row, используемый в ORM, также поддерживает доступ через сущность или атрибут:

from sqlalchemy.future import select

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

for row in session.execute(stmt).mappings():
    print("the user is: %s  the address is: %s" % (row[User], row[Address]))

2.0 Миграция - основное использование

select() больше не принимает разнообразные аргументы конструктора, столбцы передаются позиционно

синопсис

Конструкция select(), а также связанный с ней метод FromClause.select() больше не будут принимать аргументы ключевых слов для построения таких элементов, как предложение WHERE, список FROM и ORDER BY. Список столбцов теперь может быть отправлен позиционно, а не в виде списка. Кроме того, конструкция case() теперь принимает свои критерии WHEN позиционно, а не в виде списка:

# select_from / order_by keywords no longer supported
stmt = select([1], select_from=table, order_by=table.c.id)

# whereclause parameter no longer supported
stmt = select([table.c.x], table.c.id == 5)

# whereclause parameter no longer supported
stmt = table.select(table.c.id == 5)

# list emits a deprecation warning
stmt = select([table.c.x, table.c.y])

# list emits a deprecation warning
case_clause = case(
    [(table.c.x == 5, "five"), (table.c.x == 7, "seven")],
    else_="neither five nor seven",
)

Миграция на 2.0

Будет поддерживаться только «генеративный» стиль select(). Список столбцов / таблиц для SELECT должен быть передан позиционно. Конструкция select() в SQLAlchemy 1.4 принимает как старые, так и новые стили, используя схему автоматического определения, поэтому приведенный ниже код совместим с 1.4 и 2.0:

# use generative methods
stmt = select(1).select_from(table).order_by(table.c.id)

# use generative methods
stmt = select(table).where(table.c.id == 5)

# use generative methods
stmt = table.select().where(table.c.id == 5)

# pass columns clause expressions positionally
stmt = select(table.c.x, table.c.y)

# case conditions passed positionally
case_clause = case(
    (table.c.x == 5, "five"), (table.c.x == 7, "seven"), else_="neither five nor seven"
)

Дискуссия

SQLAlchemy в течение многих лет разрабатывала соглашение для конструкций SQL, принимающих аргументы либо в виде списка, либо в виде позиционных аргументов. Эта конвенция гласит, что структурные элементы, те, которые формируют структуру SQL-оператора, должны передаваться позиционно. И наоборот, элементы данные, те, которые формируют параметризованные данные оператора SQL, должны передаваться в виде списков. В течение многих лет конструкция select() не могла спокойно участвовать в этом соглашении из-за унаследованного шаблона вызова, в котором предложение «WHERE» передавалось позиционно. В SQLAlchemy 2.0 эта проблема наконец-то решена путем изменения конструкции select(), чтобы она принимала только «генеративный» стиль, который в течение многих лет был единственным документированным стилем в учебнике Core.

Примеры «структурных» и «информационных» элементов следующие:

# table columns for CREATE TABLE - structural
table = Table("table", metadata_obj, Column("x", Integer), Column("y", Integer))

# columns in a SELECT statement - structural
stmt = select(table.c.x, table.c.y)

# literal elements in an IN clause - data
stmt = stmt.where(table.c.y.in_([1, 2, 3]))

insert/update/delete DML больше не принимают аргументы конструктора ключевых слов

Синопсис

Аналогично предыдущему изменению в select(), аргументы конструкторов insert(), update() и delete(), кроме аргумента таблицы, по существу удалены:

# no longer supported
stmt = insert(table, values={"x": 10, "y": 15}, inline=True)

# no longer supported
stmt = insert(table, values={"x": 10, "y": 15}, returning=[table.c.x])

# no longer supported
stmt = table.delete(table.c.x > 15)

# no longer supported
stmt = table.update(table.c.x < 15, preserve_parameter_order=True).values(
    [(table.c.y, 20), (table.c.x, table.c.y + 10)]
)

Миграция на 2.0

Следующие примеры иллюстрируют использование генеративного метода для описанных выше примеров:

# use generative methods, **kwargs OK for values()
stmt = insert(table).values(x=10, y=15).inline()

# use generative methods, dictionary also still  OK for values()
stmt = insert(table).values({"x": 10, "y": 15}).returning(table.c.x)

# use generative methods
stmt = table.delete().where(table.c.x > 15)

# use generative methods, ordered_values() replaces preserve_parameter_order
stmt = (
    table.update()
    .where(
        table.c.x < 15,
    )
    .ordered_values((table.c.y, 20), (table.c.x, table.c.y + 10))
)

Дискуссия

API и внутренние компоненты упрощаются для конструкций DML так же, как и для конструкции select().

2.0 Миграция - Конфигурация ORM

Декларативность становится первоклассным API

Синопсис

Пакет sqlalchemy.ext.declarative в основном, за некоторыми исключениями, перенесен в пакет sqlalchemy.orm. Функции declarative_base() и declared_attr() присутствуют без каких-либо поведенческих изменений. Новая суперреализация declarative_base(), известная как registry, теперь служит конфигурационной конструкцией верхнего уровня ORM, которая также обеспечивает декларативность на основе декораторов и новую поддержку классических отображений, интегрированных с декларативным реестром.

Миграция на 2.0

Изменение импорта:

from sqlalchemy.ext import declarative_base, declared_attr

To:

from sqlalchemy.orm import declarative_base, declared_attr

Дискуссия

После десяти лет популярности пакет sqlalchemy.ext.declarative теперь интегрирован в пространство имен sqlalchemy.orm, за исключением декларативных классов «расширения», которые остаются декларативными расширениями. Более подробно это изменение описано в руководстве по переходу на 1.4 по адресу Declarative is now integrated into the ORM with new features.

См.также

Обзор сопоставленных классов ORM - вся новая унифицированная документация по Declarative, классическому отображению, dataclasses, attrs и т.д.

Declarative is now integrated into the ORM with new features

Первоначальная функция «mapper()» теперь является основным элементом Declarative, переименована в

Синопсис

Функция mapper() перемещается за кулисы, чтобы быть вызванной API более высокого уровня. Новая версия этой функции - метод registry.map_imperatively(), взятый из объекта registry.

Миграция на 2.0

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

from sqlalchemy.orm import mapper


mapper(SomeClass, some_table, properties={"related": relationship(SomeRelatedClass)})

Для работы от центрального registry объекта:

from sqlalchemy.orm import registry

mapper_reg = registry()

mapper_reg.map_imperatively(
    SomeClass, some_table, properties={"related": relationship(SomeRelatedClass)}
)

Указанный выше registry также является источником для декларативных отображений, и классические отображения теперь имеют доступ к этому реестру, включая конфигурацию на основе строк на relationship():

from sqlalchemy.orm import registry

mapper_reg = registry()

Base = mapper_reg.generate_base()


class SomeRelatedClass(Base):
    __tablename__ = "related"

    # ...


mapper_reg.map_imperatively(
    SomeClass,
    some_table,
    properties={
        "related": relationship(
            "SomeRelatedClass",
            primaryjoin="SomeRelatedClass.related_id == SomeClass.id",
        )
    },
)

Дискуссия

По многочисленным просьбам, «классическое отображение» остается, однако его новая форма основана на объекте registry и доступна как registry.map_imperatively().

Кроме того, основное обоснование «классического отображения» заключается в том, чтобы сохранить установку Table отдельно от класса. Declarative всегда допускал такой стиль, используя так называемый hybrid declarative. Однако, чтобы устранить требование базового класса, была добавлена форма первого класса decorator.

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

См.также

Обзор сопоставленных классов ORM - вся новая унифицированная документация по Declarative, классическому отображению, dataclasses, attrs и т.д.

2.0 Миграция - использование ORM

Самым большим видимым изменением в SQLAlchemy 2.0 является использование Session.execute() в сочетании с select() для выполнения ORM-запросов, вместо использования Session.query(). Как уже упоминалось, не планируется удалять сам API Session.query(), так как сейчас он реализован с помощью нового API, внутри он останется как унаследованный API, и оба API можно будет использовать свободно.

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

Обзор основных шаблонов запросов ORM.

1.x style форма

2.0 style форма

См. также

session.query(User).get(42)
session.get(User, 42)

ORM Query - метод get() перемещается в сессию

session.query(User).all()
session.execute(
  select(User)
).scalars().all()

# or

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

ORM Query Unified с Core Select

Session.scalars() Result.scalars()

session.query(User).\
  filter_by(name="some user").\
  one()
session.execute(
  select(User).
  filter_by(name="some user")
).scalar_one()

ORM Query Unified с Core Select

Result.scalar_one()

session.query(User).\
  filter_by(name="some user").\
  first()
session.scalars(
  select(User).
  filter_by(name="some user").
  limit(1)
).first()

ORM Query Unified с Core Select

Result.first()

session.query(User).options(
  joinedload(User.addresses)
).all()
session.scalars(
  select(User).
  options(
    joinedload(User.addresses)
  )
).unique().all()

Строки ORM по умолчанию не являются уникальными

session.query(User).\
  join(Address).\
  filter(
    Address.email == "e@sa.us"
  ).\
  all()
session.execute(
  select(User).
  join(Address).
  where(
    Address.email == "e@sa.us"
  )
).scalars().all()

ORM Query Unified с Core Select

Присоединяется к

session.query(User).\
  from_statement(
    text("select * from users")
  ).\
  all()
session.scalars(
  select(User).
  from_statement(
    text("select * from users")
  )
).all()

Получение результатов ORM из текстовых и основных утверждений

session.query(User).\
  join(User.addresses).\
  options(
    contains_eager(User.addresses)
  ).\
  populate_existing().all()
session.execute(
  select(User)
  .join(User.addresses)
  .options(
    contains_eager(User.addresses)
  )
  .execution_options(
      populate_existing=True
  )
).scalars().all()

Варианты выполнения ORM

Заполнить существующие

session.query(User).\
  filter(User.name == "foo").\
  update(
    {"fullname": "Foo Bar"},
    synchronize_session="evaluate"
  )
session.execute(
  update(User)
  .where(User.name == "foo")
  .values(fullname="Foo Bar")
  .execution_options(
    synchronize_session="evaluate"
  )
)

UPDATE и DELETE с произвольным предложением WHERE

session.query(User).count()
session.scalar(
  select(func.count()).
  select_from(User)
)
session.scalar(
  select(func.count(User.id))
)

Session.scalar()

ORM Query Unified с Core Select

Синопсис

Объект Query (а также расширения BakedQuery и ShardedQuery) становятся объектами длительного наследия, заменяясь прямым использованием конструкции select() в сочетании с методом Session.execute(). Результаты, которые возвращаются из Query в виде списков объектов или кортежей, или как скалярные объекты ORM, возвращаются из Session.execute() единообразно как объекты Result, которые имеют интерфейс, соответствующий интерфейсу Core execution.

Примеры кода Legacy показаны ниже:

session = Session(engine)

# becomes legacy use case
user = session.query(User).filter_by(name='some user').one()

# becomes legacy use case
user = session.query(User).filter_by(name='some user').first()

# becomes legacy use case
user = session.query(User).get(5)

# becomes legacy use case
for user in session.query(User).join(User.addresses).filter(Address.email == 'some@email.com'):
    # ...

# becomes legacy use case
users = session.query(User).options(joinedload(User.addresses)).order_by(User.id).all()

# becomes legacy use case
users = session.query(User).from_statement(
    text("select * from users")
).all()

# etc

Миграция на 2.0

Поскольку ожидается, что подавляющее большинство ORM-приложений будет использовать объекты Query, а также то, что доступный интерфейс Query не влияет на новый интерфейс, объект останется в 2.0, но больше не будет входить в документацию и не будет поддерживаться по большей части. Конструкция select() теперь подходит для использования как в Core, так и в ORM, которая при вызове методом Session.execute() будет возвращать результаты, ориентированные на ORM, то есть объекты ORM, если это то, что было запрошено.

Конструкция Select() добавляет множество новых методов для совместимости с Query, включая Select.filter() Select.filter_by(), недавно переработанные методы Select.join() и Select.outerjoin(), Select.options() и т.д. Другие более дополнительные методы Query, такие как Query.populate_existing(), реализуются через опции выполнения.

Результаты возвращаются в терминах объекта Result, новой версии объекта SQLAlchemy ResultProxy, который также добавляет много новых методов для совместимости с Query, включая Result.one(), Result.all(), Result.first(), Result.one_or_none() и т.д.

Однако объект Result требует несколько иной схемы вызова, поскольку при первом возврате он всегда возвращает кортежи и не дедуплицирует результаты в памяти. Для того, чтобы вернуть единичные объекты ORM способом Query, сначала должен быть вызван модификатор Result.scalars(). Чтобы вернуть уникальные объекты, как это необходимо при использовании объединенной ускоренной загрузки, сначала должен быть вызван модификатор Result.unique().

Документация по всем новым возможностям select(), включая опции выполнения и т.д., находится на Руководство по составлению запросов ORM.

Ниже приведены примеры перехода на select():

session = Session(engine)

user = session.execute(
    select(User).filter_by(name="some user")
).scalar_one()

# for first(), no LIMIT is applied automatically; add limit(1) if LIMIT
# is desired on the query
user = session.execute(
    select(User).filter_by(name="some user").limit(1)
).scalars().first()

# get() moves to the Session directly
user = session.get(User, 5)

for user in session.execute(
    select(User).join(User.addresses).filter(Address.email == "some@email.case")
).scalars():
    # ...

# when using joinedload() against collections, use unique() on the result
users = session.execute(
    select(User).options(joinedload(User.addresses)).order_by(User.id)
).unique().all()

# select() has ORM-ish methods like from_statement() that only work
# if the statement is against ORM entities
users = session.execute(
    select(User).from_statement(text("select * from users"))
).scalars().all()

Дискуссия

Тот факт, что в SQLAlchemy есть как конструкция select(), так и отдельный объект Query, имеющий чрезвычайно похожий, но принципиально несовместимый интерфейс, вероятно, является самым большим несоответствием в SQLAlchemy, возникшим в результате небольших инкрементных дополнений, которые со временем превратились в два основных API, которые расходятся.

В первых выпусках SQLAlchemy объект Query вообще не существовал. Первоначальная идея заключалась в том, что сама конструкция Mapper будет способна выбирать строки, и что объекты Table, а не классы, будут использоваться для создания различных критериев в стиле Core. Конструкция Query появилась через несколько месяцев/лет в истории SQLAlchemy, когда было принято предложение пользователя о новом, «конструируемом» объекте запроса, первоначально названном SelectResults. Такие понятия, как метод .where(), который SelectResults назвали .filter(), ранее не присутствовали в SQLAlchemy, а конструкция select() использовала только стиль построения «all-at-once», который сейчас устарел в select() больше не принимает разнообразные аргументы конструктора, столбцы передаются позиционно.

По мере внедрения нового подхода объект эволюционировал в объект Query по мере добавления новых возможностей, таких как возможность выбора отдельных столбцов, возможность выбора нескольких сущностей одновременно, возможность построения подзапросов из объекта Query, а не из объекта select. Целью стало то, чтобы Query обладал полной функциональностью select, чтобы его можно было составлять для построения операторов SELECT полностью, без явного использования select(). В то же время, select() также развивал «генеративные» методы, такие как Select.where() и Select.order_by().

В современной SQLAlchemy эта цель была достигнута, и теперь эти два объекта полностью совпадают по функциональности. Основной проблемой при объединении этих объектов было то, что объект select() должен был оставаться полностью независимым от ORM. Чтобы достичь этого, подавляющее большинство логики из Query было перенесено в фазу компиляции SQL, где специфичные для ORM плагины компилятора получают конструкцию Select и интерпретируют ее содержимое в терминах запроса в стиле ORM, прежде чем передать компилятору уровня ядра для создания строки SQL. С появлением новой системы кэширования компиляции SQL <change_4639>`, большая часть этой логики ORM также кэшируется.

ORM Query - метод get() перемещается в сессию

Синопсис

Метод Query.get() остается для целей наследия, но основным интерфейсом теперь является метод Session.get():

# legacy usage
user_obj = session.query(User).get(5)

Миграция на 2.0

В версии 1.4 / 2.0 объект Session добавляет новый метод Session.get():

# 1.4 / 2.0 cross-compatible use
user_obj = session.get(User, 5)

Дискуссия

Объект Query в версии 2.0 должен стать объектом наследия, поскольку ORM-запросы теперь доступны с помощью объекта select(). Поскольку метод Query.get() определяет особое взаимодействие с Session и не обязательно даже испускает запрос, более целесообразно, чтобы он был частью Session, где он аналогичен другим «идентификационным» методам, таким как refresh и merge.

Изначально SQLAlchemy включал «get()», чтобы походить на метод Hibernate Session.load(). Как это часто бывает, мы немного ошиблись, поскольку этот метод действительно больше относится к Session, чем к написанию SQL-запроса.

ORM Query - объединение / загрузка по отношениям использует атрибуты, а не строки

Синопсис

Это относится к шаблонам, таким как Query.join(), а также к опциям запроса, таким как joinedload(), которые в настоящее время принимают смесь строковых имен атрибутов или фактических атрибутов класса. Все строковые формы будут удалены в версии 2.0:

# string use removed
q = session.query(User).join("addresses")

# string use removed
q = session.query(User).options(joinedload("addresses"))

# string use removed
q = session.query(Address).filter(with_parent(u1, "addresses"))

Миграция на 2.0

Современные версии SQLAlchemy 1.x поддерживают рекомендуемую технику, которая заключается в использовании сопоставленных атрибутов:

# compatible with all modern SQLAlchemy versions

q = session.query(User).join(User.addresses)

q = session.query(User).options(joinedload(User.addresses))

q = session.query(Address).filter(with_parent(u1, User.addresses))

Те же приемы применимы к использованию стиля 2.0-style:

# SQLAlchemy 1.4 / 2.0 cross compatible use

stmt = select(User).join(User.addresses)
result = session.execute(stmt)

stmt = select(User).options(joinedload(User.addresses))
result = session.execute(stmt)

stmt = select(Address).where(with_parent(u1, User.addresses))
result = session.execute(stmt)

Дискуссия

Строковая форма вызова неоднозначна и требует от внутренних компонентов дополнительной работы по определению соответствующего пути и получению правильного сопоставленного свойства. При передаче сопоставленного атрибута ORM напрямую, не только передается необходимая информация, но и атрибут типизируется и становится более потенциально совместимым с IDE и интеграциями pep-484.

ORM Query - Цепочка с использованием списков атрибутов, а не отдельных вызовов, удалена

Синопсис

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

# chaining removed
q = session.query(User).join("orders", "items", "keywords")

Миграция на 2.0

Используйте отдельные вызовы Query.join() для кросс-совместимого использования 1.x /2.0:

q = session.query(User).join(User.orders).join(Order.items).join(Item.keywords)

Для использования 2.0-style, Select имеет такое же поведение, как Select.join(), а также имеет новый метод Select.join_from(), который позволяет явно указывать левую часть:

# 1.4 / 2.0 cross compatible

stmt = select(User).join(User.orders).join(Order.items).join(Item.keywords)
result = session.execute(stmt)

# join_from can also be helpful
stmt = select(User).join_from(User, Order).join_from(Order, Item, Order.items)
result = session.execute(stmt)

Дискуссия

Устранение цепочки атрибутов соответствует упрощению интерфейса вызова таких методов, как Select.join().

ORM Query - join(…, aliased=True), from_joinpoint removed

Синопсис

Опция aliased=True на Query.join() удалена, как и флаг from_joinpoint:

# no longer supported
q = session.query(Node).\
  join("children", aliased=True).filter(Node.name == "some sub child").
  join("children", from_joinpoint=True, aliased=True).\
  filter(Node.name == 'some sub sub child')

Миграция на 2.0

Вместо этого используйте явные псевдонимы:

n1 = aliased(Node)
n2 = aliased(Node)

q = (
    select(Node)
    .join(Node.children.of_type(n1))
    .where(n1.name == "some sub child")
    .join(n1.children.of_type(n2))
    .where(n2.name == "some sub child")
)

Дискуссия

Опция aliased=True на Query.join() - это еще одна возможность, которая, похоже, почти никогда не используется, исходя из обширного поиска кода, чтобы найти фактическое использование этой возможности. Внутренняя сложность, которую требует флаг aliased=True, огромна, и в версии 2.0 она исчезнет.

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

Параметры aliased=True и from_joinpoint были разработаны в то время, когда объект Query еще не имел хороших возможностей для объединения по атрибутам отношений, не существовало функций типа PropComparator.of_type(), а сама конструкция aliased() не существовала.

Использование DISTINCT с дополнительными столбцами, но выбор только сущности

Синопсис

Query будет автоматически добавлять столбцы в ORDER BY при использовании distinct. Следующий запрос будет выбирать из всех столбцов User, а также «address.email_address», но возвращать только объекты User:

# 1.xx code

result = (
    session.query(User)
    .join(User.addresses)
    .distinct()
    .order_by(Address.email_address)
    .all()
)

В версии 2.0 столбец «email_address» не будет автоматически добавлен в предложение columns, и приведенный выше запрос будет неудачным, так как реляционные базы данных не позволят вам сделать ORDER BY «address.email_address» при использовании DISTINCT, если он также не указан в предложении columns.

Миграция на 2.0

В версии 2.0 столбец должен быть добавлен явно. Чтобы решить проблему возврата только основного объекта сущности, а не дополнительного столбца, используйте метод Result.columns():

# 1.4 / 2.0 code

stmt = (
    select(User, Address.email_address)
    .join(User.addresses)
    .distinct()
    .order_by(Address.email_address)
)

result = session.execute(stmt).columns(User).all()

Дискуссия

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

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

Выборка из самого запроса как подзапрос, например, «from_self()».

Синопсис

Метод Query.from_self() будет удален из Query:

# from_self is removed
q = (
    session.query(User, Address.email_address)
    .join(User.addresses)
    .from_self(User)
    .order_by(Address.email_address)
)

Миграция на 2.0

Конструкцию aliased() можно использовать для создания ORM-запросов к сущности, которая находится в терминах любого произвольного selectable. В версии 1.4 она была усовершенствована для того, чтобы ее можно было использовать несколько раз в одном и том же подзапросе для разных сущностей. Это можно использовать в 1.x style с Query, как показано ниже; обратите внимание, что поскольку конечный запрос хочет запросить в терминах сущностей User и Address, создаются две отдельные конструкции aliased():

from sqlalchemy.orm import aliased

subq = session.query(User, Address.email_address).join(User.addresses).subquery()

ua = aliased(User, subq)

aa = aliased(Address, subq)

q = session.query(ua, aa).order_by(aa.email_address)

Такую же форму можно использовать в 2.0 style:

from sqlalchemy.orm import aliased

subq = select(User, Address.email_address).join(User.addresses).subquery()

ua = aliased(User, subq)

aa = aliased(Address, subq)

stmt = select(ua, aa).order_by(aa.email_address)

result = session.execute(stmt)

Дискуссия

Метод Query.from_self() - это очень сложный метод, который редко используется. Цель этого метода - преобразовать Query в подзапрос, а затем вернуть новый Query, который выбирает из этого подзапроса. Сложность этого метода заключается в том, что в возвращаемом запросе применяется автоматический перевод сущностей и столбцов ORM, которые должны быть указаны в SELECT в терминах подзапроса, а также в том, что он позволяет изменять сущности и столбцы, из которых производится SELECT.

Поскольку Query.from_self() содержит большое количество неявного перевода в создаваемом SQL, хотя он и позволяет выполнить определенный шаблон очень лаконично, в реальном мире этот метод используется нечасто, поскольку он не прост для понимания.

Новый подход использует конструкцию aliased(), чтобы внутренним компонентам ORM не нужно было гадать, какие сущности и столбцы должны быть адаптированы и каким образом; в приведенном выше примере объекты ua и aa, оба из которых являются экземплярами AliasedClass, предоставляют внутренним компонентам однозначный маркер того, куда должен быть направлен подзапрос, а также какой столбец сущности или отношение рассматривается для данного компонента запроса.

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

# 1.4 / 2.0 code

subq = select(User, Address).join(User.addresses).subquery()

ua = aliased(User, subq)
aa = aliased(Address, subq)

stmt = select(ua, aa).order_by(aa.email_address)
result = session.execute(stmt)

Вышеприведенный запрос дезамбигирует колонку .id из User и Address, где Address.id отображается и отслеживается как id_1:

SELECT anon_1.id AS anon_1_id, anon_1.id_1 AS anon_1_id_1,
       anon_1.user_id AS anon_1_user_id,
       anon_1.email_address AS anon_1_email_address
FROM (
  SELECT "user".id AS id, address.id AS id_1,
  address.user_id AS user_id, address.email_address AS email_address
  FROM "user" JOIN address ON "user".id = address.user_id
) AS anon_1 ORDER BY anon_1.email_address

#5221

Выбор сущностей из альтернативных селектов; Query.select_entity_from()

Синопсис

Метод Query.select_entity_from() будет удален в версии 2.0:

subquery = session.query(User).filter(User.id == 5).subquery()

user = session.query(User).select_entity_from(subquery).first()

Миграция на 2.0

Как и в случае, описанном в Выборка из самого запроса как подзапрос, например, «from_self()»., объект aliased() предоставляет единственное место, где могут быть выполнены операции типа «select entity from a subquery». Используя 1.x style:

from sqlalchemy.orm import aliased

subquery = session.query(User).filter(User.name.like("%somename%")).subquery()

ua = aliased(User, subquery)

user = session.query(ua).order_by(ua.id).first()

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

from sqlalchemy.orm import aliased

subquery = select(User).where(User.name.like("%somename%")).subquery()

ua = aliased(User, subquery)

# note that LIMIT 1 is not automatically supplied, if needed
user = session.execute(select(ua).order_by(ua.id).limit(1)).scalars().first()

Дискуссия

Здесь в основном те же моменты, что и в Выборка из самого запроса как подзапрос, например, «from_self()».. Метод Query.select_from_entity() - это еще один способ указать запросу загрузить строки для определенной сущности, сопоставленной с ORM, из альтернативного selectable, что подразумевает применение ORM автоматического псевдонима к этой сущности везде, где она будет использоваться в запросе в дальнейшем, например, в предложении WHERE или ORDER BY. Эта очень сложная функция редко используется таким образом, поэтому, как и в случае с Query.from_self(), гораздо проще следить за тем, что происходит при использовании явного объекта aliased(), как с точки зрения пользователя, так и с точки зрения того, как внутренние компоненты SQLAlchemy ORM должны его обрабатывать.

Строки ORM по умолчанию не являются уникальными

Синопсис

Строки ORM, возвращаемые командой session.execute(stmt), больше не являются автоматически «уникальными». Обычно это будет приятным изменением, за исключением случаев, когда стратегия загрузчика «joined eager loading» используется с коллекциями:

# In the legacy API, many rows each have the same User primary key, but
# only one User per primary key is returned
users = session.query(User).options(joinedload(User.addresses))

# In the new API, uniquing is available but not implicitly
# enabled
result = session.execute(select(User).options(joinedload(User.addresses)))

# this actually will raise an error to let the user know that
# uniquing should be applied
rows = result.all()

Переход на 2.0

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

# 1.4 / 2.0 code

stmt = select(User).options(joinedload(User.addresses))

# statement will raise if unique() is not used, due to joinedload()
# of a collection.  in all other cases, unique() is not needed.
# By stating unique() explicitly, confusion over discrepancies between
# number of objects/ rows returned vs. "SELECT COUNT(*)" is resolved
rows = session.execute(stmt).unique().all()

Дискуссия

Ситуация здесь немного необычная, поскольку SQLAlchemy требует вызова метода, который на самом деле может быть выполнен автоматически. Причина требования вызова метода заключается в том, что разработчик «соглашается» на использование метода Result.unique(), чтобы не запутаться, когда прямой подсчет строк не противоречит подсчету записей в фактическом наборе результатов, что уже много лет является источником недоумения пользователей и сообщений об ошибках. То, что уникализация не происходит ни в одном другом случае по умолчанию, повысит производительность, а также улучшит ясность в тех случаях, когда автоматическая уникализация приводила к запутанным результатам.

В той степени, в которой неудобно вызывать Result.unique() при использовании объединенных коллекций с ускоренной загрузкой, в современной SQLAlchemy стратегия selectinload() представляет собой ориентированный на коллекции механизм ускоренной загрузки, который в большинстве аспектов превосходит joinedload() и должен быть предпочтительным.

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

Синопсис

Стратегия загрузчика отношений lazy="dynamic", рассмотренная в Динамические загрузчики отношений, использует объект Query, который является наследием версии 2.0.

Миграция на 2.0

Этот шаблон все еще корректируется для SQLAlchemy 2.0, и ожидается, что будут введены новые API. В настоящее время есть два способа добиться выполнения запросов в стиле 2.0 с точки зрения определенных отношений:

  • Используйте атрибут Query.statement на существующем отношении lazy="dynamic". Мы можем использовать методы типа Session.scalars() с динамическим загрузчиком следующим образом:

    class User(Base):
        __tablename__ = "user"
    
        posts = relationship(Post, lazy="dynamic")
    
    
    jack = session.get(User, 5)
    
    # filter Jack's blog posts
    posts = session.scalars(jack.posts.statement.where(Post.headline == "this is a post"))
  • Используйте функцию with_parent() для прямого построения конструкции select():

    from sqlalchemy.orm import with_parent
    
    jack = session.get(User, 5)
    
    posts = session.scalars(
        select(Post)
        .where(with_parent(jack, User.posts))
        .where(Post.headline == "this is a post")
    )

Дискуссия

Первоначальная идея заключалась в том, что функции with_parent() должно быть достаточно, однако продолжение использования специальных атрибутов в самих отношениях остается привлекательным, и нет причин, по которым конструкция в стиле 2.0 не может работать и здесь. Вероятно, появится название новой стратегии загрузчика, которая устанавливает API, аналогичный приведенному выше примеру, использующему атрибут .statement, например jack.posts.select().where(Post.headline == 'headline').

Режим автокоммита удален из сессии; добавлена поддержка автозапуска

Синопсис

Session больше не будет поддерживать режим «autocommit», то есть такой шаблон:

from sqlalchemy.orm import Session

sess = Session(engine, autocommit=True)

# no transaction begun, but emits SQL, won't be supported
obj = sess.query(Class).first()


# session flushes in a transaction that it begins and
# commits, won't be supported
sess.flush()

Миграция на 2.0

Основная причина использования Session в режиме «autocommit» заключается в том, что метод Session.begin() доступен, так что фреймворки и крючки событий могут контролировать, когда происходит это событие. В версии 1.4 метод Session теперь имеет функцию autobegin behavior, которая решает эту проблему; метод Session.begin() теперь может быть вызван:

from sqlalchemy.orm import Session

sess = Session(engine)

sess.begin()  # begin explicitly; if not called, will autobegin
# when database access is needed

sess.add(obj)

sess.commit()

Дискуссия

Режим «autocommit» - это еще один остаток от первых версий SQLAlchemy. Флаг остался в основном для поддержки разрешения явного использования Session.begin(), что теперь решено в 1.4, а также для разрешения использования «субтранзакций», которые также удалены в 2.0.

Устранено поведение «субтранзакции» сессии

Синопсис

Шаблон «субтранзакция», который часто использовался в режиме автокоммита, также устарел в версии 1.4. Эта схема позволяла использовать метод Session.begin(), когда транзакция уже началась, что приводило к конструкции, называемой «субтранзакция», которая, по сути, являлась блоком, предотвращающим выполнение метода Session.commit() от фактической фиксации.

Миграция на 2.0

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

import contextlib


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

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

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


# method_b also starts a transaction, but when
# called from method_a participates in the ongoing
# transaction.
def method_b(session):
    with transaction(session):
        session.add(SomeObject("bat", "lala"))


Session = sessionmaker(engine)

# create a Session and call method_a
with Session() as session:
    method_a(session)

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

def method_a(session):
    method_b(session)


def method_b(session):
    session.add(SomeObject("bat", "lala"))


Session = sessionmaker(engine)

# create a Session and call method_a
with Session() as session:
    with session.begin():
        method_a(session)

Дискуссия

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

Миграция 2.0 - расширение ORM и изменения рецептов

Рецепт кэша Dogpile и горизонтальный шардинг используют новый API сессий

Поскольку объект Query переходит в наследство, эти два рецепта, которые ранее полагались на подклассификацию объекта Query, теперь используют крючок SessionEvents.do_orm_execute(). Пример см. в разделе Повторное выполнение заявлений.

Расширение запеченных запросов заменено встроенным кэшированием

Расширение запеченных запросов вытеснено встроенной системой кэширования и больше не используется внутренними компонентами ORM.

Полную информацию о новой системе кэширования смотрите в Кэширование компиляции SQL.

Поддержка Asyncio

SQLAlchemy 1.4 включает поддержку asyncio как для Core, так и для ORM. Новый API использует исключительно шаблоны «будущего», отмеченные выше. Смотрите Asynchronous IO Support for Core and ORM для справки.

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