SQLAlchemy 2.0 - руководство по миграции

Примечание для читателей

Документы по переходу на SQLAlchemy 2.0 разделены на два документа - один из которых описывает основные изменения в API от серии 1.x к 2.x, а другой - новые возможности и поведение по сравнению с SQLAlchemy 1.4:

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

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

В данном документе описаны изменения между SQLAlchemy версии 1.4 и 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. Кроме того, в нем предполагается стандартизировать некоторые новые возможности, доказавшие свою эффективность.

Путь миграции 1.4 -> 2.0

Наиболее заметные архитектурные особенности и изменения в API, которые считаются «SQLAlchemy 2.0», на самом деле были выпущены как полностью доступные в серии 1.4, чтобы обеспечить чистый путь обновления от серии 1.x к серии 2.x, а также служить бета-платформой для самих функций. Эти изменения включают:

Приведенные выше ссылки ведут к описанию этих новых парадигм, представленных в SQLAlchemy 1.4. в документе Что нового в SQLAlchemy 1.4?.

Для SQLAlchemy 2.0 все возможности и поведение API, которые были отмечены как deprecated for 2.0, теперь окончательно доработаны; в частности, к основным API, которые больше не присутствуют, относятся:

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

Полное описание шагов этого пути миграции приведено далее в этом документе в разделе Обзор миграции 1.x -> 2.x.

Обзор миграции 1.x -> 2.x

Переход на 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. Примите во внимание, что могут быть дополнительные изменения в API и поведении, которые могут вести себя по-разному при работе с SQLAlchemy 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 изменения, отнесенные к этой области, выглядят следующим образом:

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

  • Оператор SELECT больше не считается неявным предложением FROM - это изменение может повлиять на код, который каким-то образом полагался на поведение, которое в конструкции Select было в основном непригодным, где создавались безымянные подзапросы, которые обычно были запутанными и нерабочими. Такие подзапросы в любом случае будут отвергнуты большинством баз данных, поскольку имя обычно требуется, за исключением SQLite, однако возможно, что некоторым приложениям придется скорректировать некоторые запросы, которые непреднамеренно полагались на это.

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

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

Полный обзор изменений SQLAlchemy 1.4 приведен в документе Что нового в SQLAlchemy 1.4?.

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

На создание SQLAlchemy 2.0 в первую очередь вдохновил тот факт, что отказ от поддержки Python 2 произойдет в 2020 году. Отказ от поддержки Python 2.7 занимает у SQLAlchemy больше времени, чем у других крупных проектов. Тем не менее, для использования 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. В настройках тестового набора установите ряд фильтров предупреждений, которые будут выбирать для определенных подгрупп предупреждений либо возбуждение исключения, либо игнорирование (или запись в журнал). Одновременно можно работать только с одной подгруппой предупреждений. Ниже показана настройка фильтра предупреждений для приложения, в котором для прохождения всех тестов необходимо изменить вызовы .execute() на уровне Core, а все остальные предупреждения в стиле 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+psycopg2:///")

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 Шаг шестой - добавление __allow_unmapped__ в явно типизированные модели ORM

В SQLAlchemy 2.0 появилась новая поддержка интерпретации во время выполнения аннотаций типизации PEP 484 для ORM-моделей. Требование к этим аннотациям состоит в том, что они должны использовать общий контейнер Mapped. Аннотации, не использующие Mapped, которые ссылаются на такие конструкции, как relationship(), будут вызывать ошибки в Python, так как они предполагают неправильную конфигурацию.

Приложения SQLAlchemy, использующие Mypy plugin с явными аннотациями, которые не используют Mapped в своих аннотациях, подвержены этим ошибкам, как это происходит в приведенном ниже примере:

Base = declarative_base()


class Foo(Base):
    __tablename__ = "foo"

    id: int = Column(Integer, primary_key=True)

    # will raise
    bars: List["Bar"] = relationship("Bar", back_populates="foo")


class Bar(Base):
    __tablename__ = "bar"

    id: int = Column(Integer, primary_key=True)
    foo_id = Column(ForeignKey("foo.id"))

    # will raise
    foo: Foo = relationship(Foo, back_populates="bars", cascade="all")

Выше декларации Foo.bars и Bar.foo relationship() вызовут ошибку при конструировании класса, поскольку в них не используется Mapped (напротив, аннотации, использующие Column, игнорируются 2.0, поскольку они могут быть распознаны как унаследованный стиль конфигурации). Для того чтобы все аннотации, не использующие Mapped, проходили без ошибок, в классе или его подклассах можно использовать атрибут __allow_unmapped__, который приведет к тому, что аннотации в этих случаях будут полностью игнорироваться новой декларативной системой.

Примечание

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

Приведенный ниже пример иллюстрирует применение __allow_unmapped__ к классу Declarative Base, где оно будет действовать для всех классов, происходящих от Base:

# qualify the base with __allow_unmapped__.  Can also be
# applied to classes directly if preferred
class Base:
    __allow_unmapped__ = True


Base = declarative_base(cls=Base)


# existing mapping proceeds, Declarative will ignore any annotations
# which don't include ``Mapped[]``
class Foo(Base):
    __tablename__ = "foo"

    id: int = Column(Integer, primary_key=True)

    bars: List["Bar"] = relationship("Bar", back_populates="foo")


class Bar(Base):
    __tablename__ = "bar"

    id: int = Column(Integer, primary_key=True)
    foo_id = Column(ForeignKey("foo.id"))

    foo: Foo = relationship(Foo, back_populates="bars", cascade="all")

Изменено в версии 2.0.0beta3: - improved the __allow_unmapped__ attribute support to allow for 1.4-style explicit annotated relationships that don’t use Mapped to remain usable.

Седьмой этап миграции на 2.0 - тестирование по версии SQLAlchemy 2.0

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

В руководстве по адресу Что нового в SQLAlchemy 2.0? представлен обзор новых возможностей и поведения SQLAlchemy 2.0, которые выходят за рамки базового набора изменений в API 1.4->2.0.

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

Функция «Autocommit» на уровне библиотек (но не драйверов) удалена из 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 на «неявном начале» и «явной фиксации» транзакций. Спустя 15 лет мы видим, что это было ошибкой, поскольку множество паттернов SQLAlchemy, пытающихся «скрыть» наличие транзакции, делают API более сложным, работающим непоследовательно и крайне запутанным для пользователей, особенно для тех, кто впервые сталкивается с реляционными базами данных и ACID-транзакциями в целом. В SQLAlchemy 2.0 будут отменены все попытки неявной фиксации транзакций, а паттерны использования всегда будут требовать от пользователя каким-либо образом обозначать «начало» и «конец» транзакции, подобно тому, как чтение или запись в файл в Python имеет «начало» и «конец».

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

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

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

Для использования «commit as you go, or rollback instead», что напоминает то, как 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»). «Автобегин» - это новый паттерн в SQLAlchemy 1.4, который используется как в Connection, так и в ORM-объекте Session; автобегин позволяет явно вызывать метод Connection.begin() при первом получении объекта для схем, желающих обозначить начало транзакции, но если метод не вызывается, то это происходит неявно при первом выполнении работы над объектом.

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

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

Истинное поведение «autocommit» в настоящее время широко доступно в большинстве реализаций DBAPI и поддерживается SQLAlchemy с помощью параметра Connection.execution_options.isolation_level, о чем говорится в Установка уровней изоляции транзакций, включая DBAPI Autocommit. Истинный autocommit рассматривается как «уровень изоляции», поэтому структура кода приложения при использовании autocommit не меняется; менеджер контекста Connection.begin(), а также методы типа Connection.commit() по-прежнему могут использоваться, они просто не работают на уровне драйвера базы данных при включенном autocommit на уровне 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 = connection.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 возвращает объект result с непотребленными результатами курсора. Этот результат курсора обязательно связан с соединением 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)

:class:`_engine.Connection`t

# 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() sqlalchemy.engine.Connection() T

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() Connection.execute() T

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() T

Connection.execute() Session.execute() text() text() text() T

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

Синопсис.

all new Result object Row V

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()``A

Обсуждение.

KeyedTuple Query Row Row create_engine.future Engine create_engine.future LegacyRow __contains__() Row A

Row row.some_column row._mapping row["some_column"] T

``mappings()``I

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"])

:class:`.Row`T

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() case() T

# 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() O

# 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"
)

Обсуждение.

select() select() S

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

# 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() I

# 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))
)

Обсуждение.

:func:`_sql.select`T

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

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

Синопсис.

sqlalchemy.ext.declarative sqlalchemy.orm declarative_base() declared_attr() declarative_base() registry T

Миграция на 2.0

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

from sqlalchemy.ext import declarative_base, declared_attr

T

from sqlalchemy.orm import declarative_base, declared_attr

Обсуждение.

sqlalchemy.ext.declarative sqlalchemy.orm Декларативность теперь интегрирована в ORM с новыми возможностями A

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

Синопсис.

sqlalchemy.orm.mapper() registry.map_imperatively() registry T

Миграция на 2.0

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

from sqlalchemy.orm import mapper


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

:class:`_orm.registry`T

from sqlalchemy.orm import registry

mapper_reg = registry()

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

registry relationship() T

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() B

Table hybrid declarative decorator I

:ref:`Python dataclasses <orm_declarative_dataclasses>`A

См.также

:ref:`orm_mapping_classes_toplevel`<

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

Session.execute() select() Session.query() Session.query() T

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

Обзор основных паттернов ORM-запросов.

:term:`1.x style`<

:term:`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 with 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 with 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 with 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 with 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"
  )
)

Операции INSERT, UPDATE и DELETE с поддержкой ORM

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

# or

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

Session.scalar()

ORM Query Unified with Core Select

Синопсис.

Query BakedQuery ShardedQuery select() Session.execute() Query Session.execute() Result T

Примеры унаследованного кода показаны ниже:

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

Query Query select() Session.execute() B

Select() Query Select.filter() Select.filter_by() Select.join() Select.outerjoin() Select.options() Query Query.populate_existing() T

Result ResultProxy Query Result.one() Result.all() Result.first() Result.one_or_none() R

Result Query Result.scalars() Result.unique() T

select() <no title> D

:func:`_sql.select`B

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()
)

Обсуждение.

select() Query T

Query Mapper Table Query SelectResults .where() SelectResults .filter() select() select() больше не принимает переменные аргументы конструктора, столбцы передаются позиционно I

Query Query select Query select select() select() Select.where() Select.order_by() A

select() Query Select I

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

Синопсис.

Query.get() Session.get() T

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

Миграция на 2.0

Session Session.get() I

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

Обсуждение.

Query select() Query.get() Session Session refresh merge T

Session.load() Session S

ORM Query - Joining / loading on relationships uses attributes, not strings

Синопсис.

Query.join() joinedload() T

# 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))

:term:`2.0-style`T

# 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

:meth:`_orm.Query.join`U

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

2.0-style Select Select.join() Select.join_from() F

# 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)

Обсуждение.

:meth:`_sql.Select.join`R

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

Синопсис.

aliased=True Query.join() from_joinpoint T

# 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 T

``from_joinpoint``M

aliased=True from_joinpoint Query PropComparator.of_type() aliased() T

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

Синопсис.

:class:`_query.Query`<

# 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

:meth:`_result.Result.columns`I

# 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()

Обсуждение.

:class:`_orm.Query`T

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

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

Синопсис.

Query.from_self() Query T

# 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() 1.x style Query User Address aliased() T

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)

:term:`2.0 style`T

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 T

:meth:`_query.Query.from_self`B

aliased() ua aa AliasedClass T

User Address S

# 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 T

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()

Синопсис.

:meth:`_orm.Query.select_entity_from`T

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

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

Миграция на 2.0

Выборка из самого запроса в качестве подзапроса, например, «from_self()». aliased() 1.x style A

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()

:term:`2.0 style`U

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() Query.from_self() aliased() T

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

Синопсис.

``session.execute(stmt)``O

# 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

:meth:`_engine.Result.unique`W

# 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()

Обсуждение.

:meth:`_engine.Result.unique`T

Result.unique() selectinload() joinedload() T

«Динамические» загрузчики отношений вытеснены «только записью»

Синопсис.

lazy="dynamic" Динамические загрузчики отношений Query T

lazy="write_only" WriteOnlyCollection ORM-enabled Bulk DML A

``lazy=»dynamic»``A

Миграция на 2.0

``lazy=»dynamic»``T

AppenderQuery WriteOnlyCollection T

``lazy=»dynamic»``S

  • Query.statement lazy="dynamic" Session.scalars() M

    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() U

    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")
    )

Обсуждение.

:func:`_orm.with_parent`T

.select() .insert() .update() .delete() .add() .add_all() .remove() Новая стратегия отношений «Только запись» заменяет «динамическую» T

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

Синопсис.

:class:`_orm.Session`T

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 Session.begin() Session autobegin behavior Session.begin() T

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()

Обсуждение.

:meth:`_orm.Session.begin`T

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

Синопсис.

Session.begin() Session.commit() T

Миграция на 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() Повторное выполнение заявлений A

Расширение Baked Query Extension заменено встроенным кэшированием

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

:ref:`sql_caching`S

Поддержка Asyncio

:ref:`change_3414`S

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