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

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

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

Введение

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

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

Общие сведения

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

В версии 1.3 все устаревшие модели поведения и API, включая те, которые уже много лет числятся как «устаревшие», выдают предупреждения DeprecationWarning. Это относится и к использованию параметров Session.weak_identity_map и классов MapperExtension. Несмотря на то, что в документации отмечены все изъятия, часто в них не используется соответствующая директива реструктурированного текста и не указывается, в какой версии они были изъяты. Вопрос о том, действительно ли та или иная функция API выдает предупреждение об устаревании, не был последовательным. Общее отношение было таково, что большинство или все эти устаревшие функции рассматривались как долгосрочное наследие, и их удаление не планировалось.

Изменения включают в себя то, что все документированные устаревшие функции теперь используются в документации в виде соответствующей директивы реструктурированного текста с номером версии, фраза о том, что данная функция или сценарий использования будут удалены в будущем релизе, становится явной (например, больше никаких устаревших сценариев использования навсегда), и что использование любой такой функции или сценария использования обязательно приведет к появлению DeprecationWarning, что в Python 3, а также при использовании современных инструментов тестирования, таких как Pytest, теперь более явно отражается в стандартном потоке ошибок. Цель состоит в том, чтобы эти давно устаревшие функции, появившиеся еще в версиях 0.7 или 0.6, начали удаляться полностью, а не оставались в качестве «старых». Кроме того, начиная с версии 1.3, добавляется несколько новых серьезных устаревших функций. Поскольку SQLAlchemy уже 14 лет используется тысячами разработчиков в реальных условиях, можно выделить единый поток примеров использования, которые хорошо сочетаются друг с другом, и отсечь функции и шаблоны, которые работают против этого единого способа работы.

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

О наиболее существенных ухудшениях, добавляемых в 1.3, см. ниже в соответствующих разделах.

#4393

Новые возможности и усовершенствования - ORM

Связь с AliasedClass заменяет необходимость в непервичных отображениях

Непервичный отображатель» - это Mapper, созданный в стиле Императивное картирование, который выступает в качестве дополнительного отображателя по отношению к уже отображенному классу с другим типом селекта. Непервичный отображатель берет свое начало в сериях 0.1, 0.2 SQLAlchemy, где предполагалось, что объект Mapper будет основным интерфейсом построения запросов, еще до появления объекта Query.

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

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

По мере того как этот вариант использования становился все более популярным, стали очевидны его недостатки, включая то, что непервичный отображатель трудно настроить на selectable, добавляющий новые столбцы, что отображатель не наследует отношения исходного отображения, что отношения, явно настроенные на непервичном отображателе, плохо работают с опциями загрузчика, и что непервичный отображатель также не предоставляет полнофункционального пространства имен атрибутов на основе столбцов, которые можно использовать в запросах (что, опять же, в старые времена 0. 1 - 0.4 можно было использовать объекты Table непосредственно с помощью ORM).

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

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

j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)

B_viacd = mapper(
    B,
    j,
    non_primary=True,
    primary_key=[j.c.b_id],
    properties={
        "id": j.c.b_id,  # so that 'id' looks the same as before
        "c_id": j.c.c_id,  # needed for disambiguation
        "d_c_id": j.c.d_c_id,  # needed for disambiguation
        "b_id": [j.c.b_id, j.c.d_b_id],
        "d_id": j.c.d_id,
    },
)

A.b = relationship(B_viacd, primaryjoin=A.b_id == B_viacd.c.b_id)

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

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

j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)

B_viacd = aliased(B, j, flat=True)

A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id)

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

#4423

Загрузка selectin больше не использует JOIN для простых «один-ко-многим

Функция загрузки «selectin», добавленная в 1.2, представила новый чрезвычайно производительный способ ускоренной загрузки коллекций, во многих случаях значительно более быстрый, чем загрузка «subquery», поскольку он не требует повторения исходного запроса SELECT, а использует простое предложение IN. Однако при загрузке «selectin» все равно требуется выполнить JOIN между родительской и связанной таблицами, поскольку для сопоставления строк необходимы значения первичного ключа родительской таблицы. В 1.3 добавлена новая оптимизация, которая позволяет обойтись без этого JOIN в наиболее распространенном случае простой загрузки «один ко многим», когда связанная строка уже содержит первичный ключ родительской строки, выраженный в столбцах внешнего ключа. Это снова обеспечивает значительное повышение производительности, поскольку ORM теперь может загружать большое количество коллекций в одном запросе без использования JOIN или подзапросов.

Дано отображение:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    bs = relationship("B", lazy="selectin")


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

В версии 1.2 при загрузке «селектином» загрузка из A в B выглядит как:

SELECT a.id AS a_id FROM a
SELECT a_1.id AS a_1_id, b.id AS b_id, b.a_id AS b_a_id
FROM a AS a_1 JOIN b ON a_1.id = b.a_id
WHERE a_1.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY a_1.id
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

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

SELECT a.id AS a_id FROM a
SELECT b.a_id AS b_a_id, b.id AS b_id FROM b
WHERE b.a_id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY b.a_id
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

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

#4340

Улучшение поведения выражений запросов типа «многие к одному

При построении запроса, сравнивающего отношение «многие-к-одному» со значением объекта, например:

u1 = session.query(User).get(5)

query = session.query(Address).filter(Address.user == u1)

Приведенное выше выражение Address.user == u1, которое в конечном итоге компилируется в SQL-выражение, обычно основанное на столбцах первичного ключа объекта User, как "address.user_id = 5", использует отложенный вызов, чтобы получить значение 5 внутри связанного выражения как можно позже. Это сделано для того, чтобы удовлетворить требованиям как случая, когда выражение Address.user == u1 может быть адресовано объекту User, который еще не промыт и полагается на сгенерированное сервером значение первичного ключа, так и для того, чтобы выражение всегда возвращало правильный результат, даже если значение первичного ключа u1 было изменено с момента создания выражения.

Однако побочным эффектом такого поведения является то, что если к моменту оценки выражения u1 окажется истекшим, то это приведет к появлению дополнительного оператора SELECT, а в случае, если u1 также оторвался от Session, то это вызовет ошибку:

u1 = session.query(User).get(5)

query = session.query(Address).filter(Address.user == u1)

session.expire(u1)
session.expunge(u1)

query.all()  # <-- would raise DetachedInstanceError

Истечение / удаление объекта может происходить неявно, когда совершается Session и экземпляр u1 выпадает из области видимости, поскольку выражение Address.user == u1 не ссылается жестко на сам объект, а только на его InstanceState.

Исправление заключается в том, чтобы позволить выражению Address.user == u1 оценивать значение 5 на основе попытки получить или загрузить значение обычным образом во время компиляции выражения, как это происходит сейчас, но если объект отсоединен и срок его действия истек, то оно извлекается из нового механизма InstanceState, который запоминает последнее известное значение для конкретного атрибута в этом состоянии, когда срок действия атрибута истек. Этот механизм включается для конкретного атрибута InstanceState только тогда, когда это необходимо функции выражения для экономии производительности/накладных расходов памяти.

Первоначально пытались использовать более простые подходы, такие как немедленная оценка выражения с различными вариантами попытки загрузить значение позже, если оно отсутствует, однако сложным является случай, когда значение атрибута столбца (как правило, естественного первичного ключа) изменяется. Для того чтобы выражение типа Address.user == u1 всегда возвращало правильный ответ для текущего состояния u1, оно будет возвращать текущее значение, хранящееся в базе данных, для постоянного объекта, при необходимости разворачиваясь через SELECT-запрос, а для удаленного объекта будет возвращать последнее известное значение, независимо от того, когда объект был истекшим, используя новую функцию в InstanceState, которая отслеживает последнее известное значение атрибута столбца, когда атрибут должен быть истекшим.

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

#4359

Замена «многие к одному» не будет подниматься для «raiseload» или отделяться для «старого» объекта

В случае, когда ленивая загрузка будет выполняться по отношению «многие-к-одному» с целью загрузки «старого» значения, если в отношении не указан флаг relationship.active_history, то утверждение для отсоединенного объекта не будет вызвано:

a1 = session.query(Address).filter_by(id=5).one()

session.expunge(a1)

a1.user = some_user

Выше при замене атрибута .user на отсоединенном объекте a1 возникала ошибка DetachedInstanceError, поскольку атрибут пытался получить предыдущее значение .user из карты идентификации. Изменение заключается в том, что теперь операция выполняется без загрузки старого значения.

Аналогичное изменение внесено и в стратегию загрузчика lazy="raise":

class Address(Base):
    # ...

    user = relationship("User", ..., lazy="raise")

Ранее ассоциация a1.user вызывала исключение «raiseload» в результате того, что атрибут пытался получить предыдущее значение. Теперь это утверждение пропускается в случае загрузки «старого» значения.

#4353

«del» реализован для атрибутов ORM

Операция Python del не совсем подходила для сопоставленных атрибутов, как скалярных столбцов, так и объектных ссылок. Была добавлена поддержка корректной работы этой операции, где операция del примерно эквивалентна установке атрибута в значение None:

some_object = session.query(SomeObject).get(5)

del some_object.some_attribute  # from a SQL perspective, works like "= None"

#4354

информационный словарь добавлен в InstanceState

В класс InstanceState добавлен словарь .info - объект, получаемый при вызове inspect() на сопоставленном объекте. Это позволяет в пользовательских рецептах добавлять дополнительную информацию об объекте, которая будет храниться в памяти в течение всего жизненного цикла объекта:

from sqlalchemy import inspect

u1 = User(id=7, name="ed")

inspect(u1).info["user_info"] = "7|ed"

#4257

Расширение Horizontal Sharding поддерживает методы массового обновления и удаления

Объект расширения ShardedQuery поддерживает методы массового обновления/удаления Query.update() и Query.delete(). При их вызове происходит обращение к вызываемому объекту query_chooser, чтобы выполнить обновление/удаление на нескольких хранилищах по заданным критериям.

#4196

Усовершенствование доверенности ассоциации

Расширение Association Proxy в этом цикле получило множество улучшений, хотя и не по каким-то конкретным причинам.

Ассоциативный прокси имеет новый флаг cascade_scalar_deletes

Дано отображение в виде:

class A(Base):
    __tablename__ = "test_a"
    id = Column(Integer, primary_key=True)
    ab = relationship("AB", backref="a", uselist=False)
    b = association_proxy(
        "ab", "b", creator=lambda b: AB(b=b), cascade_scalar_deletes=True
    )


class B(Base):
    __tablename__ = "test_b"
    id = Column(Integer, primary_key=True)
    ab = relationship("AB", backref="b", cascade="all, delete-orphan")


class AB(Base):
    __tablename__ = "test_ab"
    a_id = Column(Integer, ForeignKey(A.id), primary_key=True)
    b_id = Column(Integer, ForeignKey(B.id), primary_key=True)

Присвоение A.b породит объект AB:

a.b = B()

Ассоциация A.b является скалярной и включает новый флаг AssociationProxy.cascade_scalar_deletes. Если он установлен, то установка A.b в None приведет к удалению и A.ab. Поведение по умолчанию оставляет a.ab на месте:

a.b = None
assert a.ab is None

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

Кроме того, del теперь работает для скаляров аналогично установке в None:

del a.b
assert a.ab is None

#4308

AssociationProxy хранит состояние, специфичное для каждого класса.

Объект AssociationProxy принимает множество решений в зависимости от родительского сопоставленного класса, с которым он связан. Хотя объект AssociationProxy исторически начинался как относительно простой «геттер», с самого начала стало очевидно, что ему также необходимо принимать решения о том, на какой атрибут он ссылается, например, скаляр или коллекция, сопоставленный объект или простое значение, и т.п. Для этого необходимо проинспектировать отображаемый атрибут или другой дескриптор или атрибут, на который он ссылается, как на ссылку из его родительского класса. Однако в механике дескрипторов Python дескриптор узнает о своем «родительском» классе только тогда, когда к нему обращаются в контексте этого класса, например, вызывая MyClass.some_descriptor, который вызывает метод __get__(), передающий класс. Таким образом, объект AssociationProxy будет хранить состояние, характерное для данного класса, но только после вызова этого метода; попытка проверить это состояние заранее, без предварительного обращения к AssociationProxy в качестве дескриптора, приведет к ошибке. Кроме того, предполагается, что первый класс, который увидит __get__(), будет единственным родительским классом, о котором ему необходимо знать. И это несмотря на то, что если у конкретного класса есть наследуемые подклассы, то ассоциативный прокси действительно работает от имени более чем одного родительского класса, даже если он не был явно повторно использован. Хотя даже с учетом этого недостатка ассоциативный прокси мог бы довольно далеко продвинуться со своим текущим поведением, он все же оставляет недостатки в некоторых случаях, а также сложную проблему

Теперь эти проблемы решены тем, что AssociationProxy больше не изменяет свое внутреннее состояние при вызове __get__(); вместо этого для каждого класса генерируется новый объект AssociationProxyInstance, который обрабатывает все состояние, характерное для конкретного сопоставленного родительского класса (если родительский класс не сопоставлен, то AssociationProxyInstance не генерируется). Концепция единственного «класса-владельца» для ассоциативного прокси, которая, тем не менее, была улучшена в 1.1, по сути, была заменена подходом, при котором AP теперь может одинаково относиться к любому количеству «классов-владельцев».

Для удобства приложений, которые хотят проверить это состояние на наличие AssociationProxy без обязательного вызова __get__(), добавлен новый метод AssociationProxy.for_class(), который обеспечивает прямой доступ к специфическому для класса AssociationProxyInstance, продемонстрированному как:

class User(Base):
    # ...

    keywords = association_proxy("kws", "keyword")


proxy_state = inspect(User).all_orm_descriptors["keywords"].for_class(User)

После того как у нас есть объект AssociationProxyInstance, в приведенном выше примере хранящийся в переменной proxy_state, мы можем посмотреть атрибуты, характерные для прокси User.keywords, например target_class:

>>> proxy_state.target_class
Keyword

#3423

AssociationProxy теперь предоставляет стандартные операторы столбцов для целей, ориентированных на столбцы

Дается AssociationProxy, где целью является столбец базы данных, и не является ссылкой на объект или другой ассоциативный прокси:

class User(Base):
    # ...

    elements = relationship("Element")

    # column-based association proxy
    values = association_proxy("elements", "value")


class Element(Base):
    # ...

    value = Column(String)

Прокси ассоциации User.values ссылается на колонку Element.value. Теперь доступны стандартные операции со столбцами, такие как like:

>>> print(s.query(User).filter(User.values.like("%foo%")))
{printsql}SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value LIKE :value_1)

equals:

>>> print(s.query(User).filter(User.values == "foo"))
{printsql}SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value = :value_1)

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

>>> print(s.query(User).filter(User.values == None))
{printsql}SELECT "user".id AS user_id
FROM "user"
WHERE (EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value IS NULL)) OR NOT (EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id))

Обратите внимание, что оператор ColumnOperators.contains() фактически является оператором сравнения строк; это изменение в поведении, поскольку ранее прокси ассоциации использовал .contains только как оператор локализации списка. При сравнении, ориентированном на столбец, он теперь ведет себя как «like»:

>>> print(s.query(User).filter(User.values.contains("foo")))
{printsql}SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND (element.value LIKE '%' || :value_1 || '%'))

Для проверки коллекции User.values на простую принадлежность значению "foo" следует использовать оператор equals (например, User.values == 'foo'), который работает и в предыдущих версиях.

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

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    user_elements = relationship("UserElement")

    # object-based association proxy
    elements = association_proxy("user_elements", "element")


class UserElement(Base):
    __tablename__ = "user_element"

    id = Column(Integer, primary_key=True)
    user_id = Column(ForeignKey("user.id"))
    element_id = Column(ForeignKey("element.id"))
    element = relationship("Element")


class Element(Base):
    __tablename__ = "element"

    id = Column(Integer, primary_key=True)
    value = Column(String)

Метод .contains() выдает то же выражение, что и раньше, проверяя список User.elements на наличие объекта Element:

>>> print(s.query(User).filter(User.elements.contains(Element(id=1))))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM user_element
WHERE "user".id = user_element.user_id AND :param_1 = user_element.element_id)

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

#4351

Прокси ассоциации теперь строго ссылаются на родительский объект

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

В качестве примера можно привести отображение с ассоциацией proxy:

class A(Base):
    __tablename__ = "a"

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


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


a1 = A(bs=[B(data="b1"), B(data="b2")])

b_data = a1.b_data

Ранее, если a1 удалялись за пределы области видимости:

del a1

Попытка выполнить итерацию коллекции b_data после удаления из области видимости a1 приведет к ошибке "stale association proxy, parent object has gone out of scope". Это связано с тем, что для создания представления ассоциативному прокси необходимо получить доступ к реальной коллекции a1.bs, а до этого изменения он поддерживал только слабую ссылку на a1. В частности, пользователи часто сталкивались с этой ошибкой при выполнении инлайн-операций типа:

collection = session.query(A).filter_by(id=1).first().b_data

Выше, поскольку объект A будет собран до того, как коллекция b_data будет реально использована.

Изменение заключается в том, что коллекция b_data теперь поддерживает сильную ссылку на объект a1, так что он остается присутствующим:

assert b_data == ["b1", "b2"]

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

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

#4268

Реализована массовая замена для множеств и сущностей с помощью AssociationProxy

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

class A(Base):
    __tablename__ = "test_a"

    id = Column(Integer, primary_key=True)
    b_rel = relationship(
        "B",
        collection_class=set,
        cascade="all, delete-orphan",
    )
    b = association_proxy("b_rel", "value", creator=lambda x: B(value=x))


class B(Base):
    __tablename__ = "test_b"
    __table_args__ = (UniqueConstraint("a_id", "value"),)

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("test_a.id"), nullable=False)
    value = Column(String)


# ...

s = Session(e)
a = A(b={"x", "y", "z"})
s.add(a)
s.commit()

# re-assign where one B should be deleted, one B added, two
# B's maintained
a.b = {"x", "z", "q"}

# only 'q' was added, so only one new B object.  previously
# all three would have been re-created leading to flush conflicts
# against the deleted ones.
assert len(s.new) == 1

#2642

Проверка обратных ссылок «многие-к-одному» на наличие дубликатов коллекций при операции удаления

Когда ORM-сопоставленная коллекция, существующая в виде последовательности Python, обычно Python list, как это принято по умолчанию для relationship(), содержала дубликаты, и объект удалялся из одной из позиций, но не из другой (других), обратная ссылка «многие-к-одному» устанавливала его атрибут в None, хотя со стороны «один-ко-многим» объект по-прежнему был представлен как существующий. Несмотря на то, что в реляционной модели коллекции «один ко многим» не могут иметь дубликатов, ORM-ориентированная relationship(), использующая коллекцию последовательностей, может иметь дубликаты внутри себя в памяти, с тем ограничением, что это состояние дубликатов не может быть ни сохранено, ни извлечено из базы данных. В частности, временное присутствие дубликата в списке является неотъемлемой частью операции «подмены» в Python. Если взять стандартную схему «один ко многим/многие к одному»:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    bs = relationship("B", backref="a")


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

Если мы имеем объект A с двумя членами B и выполняем обмен:

a1 = A(bs=[B(), B()])

a1.bs[0], a1.bs[1] = a1.bs[1], a1.bs[0]

При выполнении описанной выше операции перехват стандартных методов Python __setitem__ __delitem__ приводит к промежуточному состоянию, когда второй объект B() присутствует в коллекции дважды. Когда объект B() удаляется из одной из позиций, обратная ссылка B.a устанавливает ссылку на None, что приводит к удалению связи между объектами A и B во время промывки. Аналогичная проблема может быть продемонстрирована и на примере простых дубликатов:

>>> a1 = A()
>>> b1 = B()
>>> a1.bs.append(b1)
>>> a1.bs.append(b1)  # append the same b1 object twice
>>> del a1.bs[1]
>>> a1.bs  # collection is unaffected so far...
[<__main__.B object at 0x7f047af5fb70>]
>>> b1.a  # however b1.a is None
>>>
>>> session.add(a1)
>>> session.commit()  # so upon flush + expire....
>>> a1.bs  # the value is gone
[]

Исправление гарантирует, что в момент срабатывания обратной ссылки, то есть до того, как коллекция будет мутирована, коллекция будет проверена на наличие ровно одного или нуля экземпляров целевого элемента перед снятием установки со стороны many-to-one, используя линейный поиск, который на данный момент использует list.search и list.__contains__.

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

#1103

Ключевые изменения в поведении - ORM

Query.join() более явно обрабатывает неоднозначность в определении «левой» стороны

Исторически сложилось так, что при запросе типа:

u_alias = aliased(User)
session.query(User, u_alias).join(Address)

с учетом стандартных отображений учебника, запрос будет иметь формулу FROM:

SELECT ...
FROM users AS users_1, users JOIN addresses ON users.id = addresses.user_id

То есть, JOIN неявно будет относиться к первой совпавшей сущности. Новое поведение заключается в том, что исключение запрашивает разрешение этой двусмысленности:

sqlalchemy.exc.InvalidRequestError: Can't determine which FROM clause to
join from, there are multiple FROMS which can join to this entity.
Try adding an explicit ON clause to help resolve the ambiguity.

Решение состоит в том, чтобы предоставить предложение ON, либо в виде выражения:

# join to User
session.query(User, u_alias).join(Address, Address.user_id == User.id)

# join to u_alias
session.query(User, u_alias).join(Address, Address.user_id == u_alias.id)

Или использовать атрибут отношения, если он имеется:

# join to User
session.query(User, u_alias).join(Address, User.addresses)

# join to u_alias
session.query(User, u_alias).join(Address, u_alias.addresses)

Изменения заключаются в том, что теперь join может корректно ссылаться на предложение FROM, которое не является первым элементом в списке, если в остальном join не является однозначным:

session.query(func.current_timestamp(), User).join(Address)

До внесения этого усовершенствования приведенная выше формулировка запроса приводила к появлению ошибок:

sqlalchemy.exc.InvalidRequestError: Don't know how to join from
CURRENT_TIMESTAMP; please use select_from() to establish the
left entity/selectable of this join

Теперь запрос работает нормально:

SELECT CURRENT_TIMESTAMP AS current_timestamp_1, users.id AS users_id,
users.name AS users_name, users.fullname AS users_fullname,
users.password AS users_password
FROM users JOIN addresses ON users.id = addresses.user_id

В целом изменения прямо соответствуют философии Python «явное лучше неявного».

#4365

Предложение FOR UPDATE отображается как внутри объединенного подзапроса eager load, так и вне его

Данное изменение относится именно к использованию стратегии загрузки joinedload() в сочетании с запросом с ограничением строк, например, с использованием Query.first() или Query.limit(), а также с использованием метода Query.with_for_update().

Заданный запрос выглядит так:

session.query(A).options(joinedload(A.b)).limit(5)

Объект Query выдает SELECT следующего вида, когда объединенная ускоренная загрузка сочетается с LIMIT:

SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
    SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id

Это сделано для того, чтобы ограничение количества строк происходило для первичной сущности, не затрагивая при этом объединенную загрузку связанных элементов. Когда приведенный выше запрос комбинируется с «SELECT…FOR UPDATE», поведение было таким:

SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
    SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id FOR UPDATE

Однако MySQL, в отличие от PostgreSQL и других баз данных, благодаря https://bugs.mysql.com/bug.php?id=90693 не блокирует строки внутри подзапроса. Поэтому приведенный выше запрос теперь выглядит так:

SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
    SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5 FOR UPDATE
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id FOR UPDATE

В диалекте Oracle внутреннее «FOR UPDATE» не выводится, поскольку Oracle не поддерживает этот синтаксис и диалект пропускает любое «FOR UPDATE», обращенное к подзапросу; в любом случае оно не нужно, поскольку Oracle, как и PostgreSQL, корректно блокирует все элементы возвращаемой строки.

При использовании модификатора Query.with_for_update.of, как правило, в PostgreSQL, внешнее «FOR UPDATE» опускается, и OF теперь отображается внутри; ранее цель OF не преобразовывалась для корректного размещения подзапроса. Таким образом, дано:

session.query(A).options(joinedload(A.b)).with_for_update(of=A).limit(5)

Теперь запрос будет выглядеть так:

SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
    SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5 FOR UPDATE OF a
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id

Приведенная форма должна быть полезна и для PostgreSQL, поскольку PostgreSQL не позволяет выводить предложение FOR UPDATE после цели LEFT OUTER JOIN.

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

#4246

passive_deletes=“all“ оставит FK неизменным для объектов, удаленных из коллекции

Опция relationship.passive_deletes принимает значение "all", указывающее на то, что атрибуты внешнего ключа не должны быть изменены при очистке объекта, даже если коллекция / ссылка отношения была удалена. Ранее это не происходило для отношений «один-ко-многим» или «один-к-одному» в следующей ситуации:

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    addresses = relationship("Address", passive_deletes="all")


class Address(Base):
    __tablename__ = "addresses"
    id = Column(Integer, primary_key=True)
    email = Column(String)

    user_id = Column(Integer, ForeignKey("users.id"))
    user = relationship("User")


u1 = session.query(User).first()
address = u1.addresses[0]
u1.addresses.remove(address)
session.commit()

# would fail and be set to None
assert address.user_id == u1.id

Теперь исправление включает в себя то, что address.user_id остается неизменным в соответствии с passive_deletes="all". Подобные вещи полезны для построения пользовательских схем «таблиц версий» и т.п., в которых строки архивируются, а не удаляются.

#3844

Новые возможности и усовершенствования - Ядро

Новые многоколоночные маркеры именования, усечение длинных имен

Для случая, когда в соглашении об именовании MetaData необходимо провести однозначную идентификацию между многостолбцовыми ограничениями и использовать все столбцы в сгенерированном имени ограничения, добавлена новая серия лексем соглашения об именовании, включая column_0N_name, column_0_N_name, column_0N_key, column_0_N_key, referred_column_0N_name, referred_column_0_N_name и др, которые отображают имя столбца (или ключа, или метки) для всех столбцов в ограничении, объединенных либо без разделителя, либо с разделителем в виде знака подчеркивания. Ниже мы определяем соглашение, согласно которому ограничения UniqueConstraint будут называться именем, объединяющим имена всех столбцов:

metadata_obj = MetaData(
    naming_convention={"uq": "uq_%(table_name)s_%(column_0_N_name)s"}
)

table = Table(
    "info",
    metadata_obj,
    Column("a", Integer),
    Column("b", Integer),
    Column("c", Integer),
    UniqueConstraint("a", "b", "c"),
)

CREATE TABLE для приведенной выше таблицы будет иметь вид:

CREATE TABLE info (
    a INTEGER,
    b INTEGER,
    c INTEGER,
    CONSTRAINT uq_info_a_b_c UNIQUE (a, b, c)
)

Кроме того, теперь к именам, генерируемым соглашениями об именовании, применяется логика усечения длинных имен, в частности, для многоколоночных меток, которые могут давать очень длинные имена. Эта логика, аналогичная той, что используется для усечения длинных имен меток в операторе SELECT, заменяет лишние символы, превышающие ограничение на длину идентификатора для целевой базы данных, детерминированным 4-символьным хэшем. Например, в СУБД PostgreSQL, где длина идентификаторов не может превышать 63 символов, длинное имя ограничения обычно генерируется из приведенного ниже определения таблицы:

long_names = Table(
    "long_names",
    metadata_obj,
    Column("information_channel_code", Integer, key="a"),
    Column("billing_convention_name", Integer, key="b"),
    Column("product_identifier", Integer, key="c"),
    UniqueConstraint("a", "b", "c"),
)

Логика усечения гарантирует, что для ограничения UNIQUE не будет создано слишком длинное имя:

CREATE TABLE long_names (
    information_channel_code INTEGER,
    billing_convention_name INTEGER,
    product_identifier INTEGER,
    CONSTRAINT uq_long_names_information_channel_code_billing_conventi_a79e
    UNIQUE (information_channel_code, billing_convention_name, product_identifier)
)

Приведенный выше суффикс a79e основан на md5-хэше длинного имени и будет генерировать одно и то же значение каждый раз для получения согласованных имен для данной схемы.

Заметим, что логика усечения также поднимает IdentifierError, когда имя ограничения явно слишком велико для данного диалекта. Такое поведение уже давно характерно для объекта Index, но теперь оно применяется и к другим видам ограничений:

from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import Table
from sqlalchemy import UniqueConstraint
from sqlalchemy.dialects import postgresql
from sqlalchemy.schema import AddConstraint

m = MetaData()
t = Table("t", m, Column("x", Integer))
uq = UniqueConstraint(
    t.c.x,
    name="this_is_too_long_of_a_name_for_any_database_backend_even_postgresql",
)

print(AddConstraint(uq).compile(dialect=postgresql.dialect()))

будет выведено:

sqlalchemy.exc.IdentifierError: Identifier
'this_is_too_long_of_a_name_for_any_database_backend_even_postgresql'
exceeds maximum length of 63 characters

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

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

uq = UniqueConstraint(
    t.c.x,
    name=conv("this_is_too_long_of_a_name_for_any_database_backend_even_postgresql"),
)

При этом снова будет выведен детерминированно усеченный SQL, как в примере:

ALTER TABLE t ADD CONSTRAINT this_is_too_long_of_a_name_for_any_database_backend_eve_ac05 UNIQUE (x)

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

Это изменение также устраняет две другие проблемы. Одна из них заключается в том, что маркер column_0_key был недоступен, хотя этот маркер был задокументирован, другая - в том, что маркер referred_column_0_name случайно приводил к отображению .key, а не .name столбца, если эти два значения были разными.

#3989

Интерпретация двоичных сравнений для функций SQL

Это усовершенствование реализовано на уровне Core, однако применимо в первую очередь к ORM.

Функция SQL, сравнивающая два элемента, теперь может быть использована как объект «сравнения», пригодный для использования в ORM relationship(), сначала создав функцию как обычно, используя фабрику func, а затем, когда функция завершена, вызвав модификатор FunctionElement.as_comparison(), получить BinaryExpression, имеющий «левую» и «правую» стороны:

class Venue(Base):
    __tablename__ = "venue"
    id = Column(Integer, primary_key=True)
    name = Column(String)

    descendants = relationship(
        "Venue",
        primaryjoin=func.instr(remote(foreign(name)), name + "/").as_comparison(1, 2)
        == 1,
        viewonly=True,
        order_by=name,
    )

Выше, relationship.primaryjoin отношения «потомки» будет выдавать выражение «слева» и «справа», основанное на первом и втором аргументах, переданных в instr(). Это позволяет таким функциям, как ORM lazyload, создавать SQL типа:

SELECT venue.id AS venue_id, venue.name AS venue_name
FROM venue
WHERE instr(venue.name, (? || ?)) = ? ORDER BY venue.name
('parent1', '/', 1)

и присоединяемая нагрузка, например:

v1 = (
    s.query(Venue)
    .filter_by(name="parent1")
    .options(joinedload(Venue.descendants))
    .one()
)

работать как:

SELECT venue.id AS venue_id, venue.name AS venue_name,
  venue_1.id AS venue_1_id, venue_1.name AS venue_1_name
FROM venue LEFT OUTER JOIN venue AS venue_1
  ON instr(venue_1.name, (venue.name || ?)) = ?
WHERE venue.name = ? ORDER BY venue_1.name
('/', 1, 'parent1')

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

#3831

Функция расширения IN теперь поддерживает пустые списки

Функция «расширения IN», появившаяся в версии 1.2 в Расширенные в последнее время наборы параметров IN позволяют использовать IN-выражения с кэшированными утверждениями, теперь поддерживает пустые списки, передаваемые оператору ColumnOperators.in_(). Реализация для пустого списка будет выдавать выражение «пустое множество», специфичное для целевого бэкенда, например, «SELECT CAST(NULL AS INTEGER) WHERE 1!=1» для PostgreSQL, «SELECT 1 FROM (SELECT 1) as _empty_set WHERE 1!=1» для MySQL:

>>> from sqlalchemy import create_engine
>>> from sqlalchemy import select, literal_column, bindparam
>>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
>>> with e.connect() as conn:
...     conn.execute(
...         select([literal_column("1")]).where(
...             literal_column("1").in_(bindparam("q", expanding=True))
...         ),
...         q=[],
...     )
{exexsql}SELECT 1 WHERE 1 IN (SELECT CAST(NULL AS INTEGER) WHERE 1!=1)

Эта возможность также работает для кортежно-ориентированных операторов IN, где «пустое IN» выражение будет расширено для поддержки элементов, заданных внутри кортежа, как, например, в PostgreSQL:

>>> from sqlalchemy import create_engine
>>> from sqlalchemy import select, literal_column, tuple_, bindparam
>>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
>>> with e.connect() as conn:
...     conn.execute(
...         select([literal_column("1")]).where(
...             tuple_(50, "somestring").in_(bindparam("q", expanding=True))
...         ),
...         q=[],
...     )
{exexsql}SELECT 1 WHERE (%(param_1)s, %(param_2)s)
IN (SELECT CAST(NULL AS INTEGER), CAST(NULL AS VARCHAR) WHERE 1!=1)

#4271

Методы TypeEngine bind_expression, column_expression работают с Variant, типами-спецификами

Методы TypeEngine.bind_expression() и TypeEngine.column_expression() теперь работают, если они присутствуют на «impl» конкретного типа данных, что позволяет использовать эти методы в диалектах, а также для случаев использования TypeDecorator и Variant.

Следующий пример иллюстрирует TypeDecorator, который применяет функции преобразования SQL-времени к LargeBinary. Для того чтобы этот тип работал в контексте Variant, компилятору необходимо проникнуть в «impl» вариантного выражения, чтобы найти эти методы:

from sqlalchemy import TypeDecorator, LargeBinary, func


class CompressedLargeBinary(TypeDecorator):
    impl = LargeBinary

    def bind_expression(self, bindvalue):
        return func.compress(bindvalue, type_=self)

    def column_expression(self, col):
        return func.uncompress(col, type_=self)


MyLargeBinary = LargeBinary().with_variant(CompressedLargeBinary(), "sqlite")

Приведенное выше выражение будет представлять собой функцию в рамках SQL при использовании только на SQLite:

from sqlalchemy import select, column
from sqlalchemy.dialects import sqlite

print(select([column("x", CompressedLargeBinary)]).compile(dialect=sqlite.dialect()))

будет оказывать:

SELECT uncompress(x) AS x

Изменение также включает в себя то, что диалекты могут реализовать TypeEngine.bind_expression() и TypeEngine.column_expression() на уровне диалекта в типах реализации, где они теперь будут использоваться; в частности, это будет использоваться для нового требования MySQL «двоичный префикс», а также для приведения десятичных значений привязки для MySQL.

#3981

Новая стратегия last-in-first-out для пула QueuePool

Пул соединений, обычно используемый create_engine(), известен как QueuePool. Этот пул использует объект, эквивалентный встроенному в Python классу Queue, для хранения соединений с базами данных, ожидающих своего использования. В Queue реализовано поведение «первым пришел - первым ушел», которое призвано обеспечить поочередное использование соединений с базами данных, постоянно находящихся в пуле. Однако потенциальным недостатком такого поведения является то, что при низком уровне использования пула повторное последовательное использование каждого соединения приводит к тому, что стратегия таймаута на стороне сервера, направленная на сокращение неиспользуемых соединений, не позволяет отключить эти соединения. Для решения этой задачи добавлен новый флаг create_engine.pool_use_lifo, который изменяет метод .get() на Queue, позволяя извлекать соединение из начала очереди, а не из ее конца, по сути превращая «очередь» в «стек» (рассматривался вариант добавления целого нового пула с названием StackPool, однако это оказалось слишком многословным).

Основные изменения - Основные

Полностью удалено принудительное приведение строковых SQL-фрагментов к text()

Предупреждения, впервые добавленные в версии 1.0 и описанные в Предупреждения, выдаваемые при принудительном введении полных фрагментов SQL в text(), теперь преобразованы в исключения. Продолжает вызывать озабоченность автоматическое преобразование фрагментов строк, передаваемых в методы типа Query.filter() и Select.order_by(), в конструкции text(), несмотря на то, что при этом выдается предупреждение. В случаях Select.order_by(), Query.order_by(), Select.group_by() и Query.group_by() строковая метка или имя столбца по-прежнему преобразуется в соответствующую конструкцию выражения, однако при неудачном преобразовании выдается предупреждение CompileError, что предотвращает непосредственное отображение необработанного SQL-текста.

#4481

«Потоково-локальная» двигательная стратегия устарела

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

Первоначальная модель использования SQLAlchemy выглядела следующим образом:

engine.begin()

table.insert().execute(parameters)
result = table.select().execute()

table.update().execute(parameters)

engine.commit()

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

conn = engine.connect()
try:
    trans = conn.begin()

    conn.execute(table.insert(), parameters)
    result = conn.execute(table.select())

    conn.execute(table.update(), parameters)

    trans.commit()
except:
    trans.rollback()
    raise
finally:
    conn.close()

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

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

with engine.begin() as conn:
    conn.execute(table.insert(), parameters)
    result = conn.execute(table.select())

    conn.execute(table.update(), parameters)

На данный момент весь оставшийся код, который все еще полагается на стиль «threadlocal», будет поощряться этим устареванием к модернизации - эта возможность должна быть полностью удалена в следующей крупной серии SQLAlchemy, например, 1.4. Параметр пула соединений Pool.use_threadlocal также устарел, поскольку в большинстве случаев не имеет никакого значения, как и метод Engine.contextual_connect(), который обычно является синонимом метода Engine.connect(), за исключением случаев, когда используется механизм threadlocal.

#4393

Параметры convert_unicode deprecated

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

С появлением Python 3 DBAPI стали поддерживать Unicode более полно и, что особенно важно, по умолчанию. Однако условия, при которых тот или иной DBAPI будет или не будет возвращать Unicode-данные из результата, а также принимать значения Python Unicode в качестве параметров, оставались крайне сложными. Это стало началом устаревания флагов «convert_unicode», поскольку они перестали быть достаточным средством обеспечения того, чтобы кодирование/декодирование происходило только там, где это необходимо, а не там, где это не нужно. Вместо этого «convert_unicode» стал автоматически определяться диалектами. Частично это можно увидеть в SQL «SELECT „test plain returns“» и «SELECT „test_unicode_returns“», выдаваемых движком при первом подключении; диалект проверяет, возвращает ли текущий DBAPI с его текущими настройками и подключением к базе данных backend по умолчанию Unicode или нет.

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

#4393

Улучшения и изменения в диалекте - PostgreSQL

Добавлена базовая поддержка отражения для таблиц с разбиением PostgreSQL

SQLAlchemy может выводить последовательность «PARTITION BY» внутри оператора PostgreSQL CREATE TABLE, используя флаг postgresql_partition_by, добавленный в версии 1.2.6. Однако до сих пор тип 'p' не был частью используемых запросов отражения.

Дана схема, например:

dv = Table(
    "data_values",
    metadata_obj,
    Column("modulus", Integer, nullable=False),
    Column("data", String(30)),
    postgresql_partition_by="range(modulus)",
)

sa.event.listen(
    dv,
    "after_create",
    sa.DDL(
        "CREATE TABLE data_values_4_10 PARTITION OF data_values "
        "FOR VALUES FROM (4) TO (10)"
    ),
)

Имена двух таблиц 'data_values' и 'data_values_4_10' будут возвращаться из Inspector.get_table_names() и, кроме того, столбцы будут возвращаться из Inspector.get_columns('data_values') и Inspector.get_columns('data_values_4_10'). Это распространяется и на использование Table(..., autoload=True) с этими таблицами.

#4237

Улучшения и изменения в диалекте - MySQL

ping на уровне протокола теперь используется для предварительного пинга

Диалекты MySQL, включая mysqlclient, python-mysql, PyMySQL и mysql-connector-python, теперь используют метод connection.ping() для функции предварительного пинга пула, описанной в Работа с разъединениями - пессимистично. Это гораздо более легкий пинг, чем предыдущий метод выдачи «SELECT 1» при подключении.

Управление упорядочиванием параметров в ON DUPLICATE KEY UPDATE

Порядок следования параметров UPDATE в предложении ON DUPLICATE KEY UPDATE теперь может быть явно упорядочен путем передачи списка из двух кортежей:

from sqlalchemy.dialects.mysql import insert

insert_stmt = insert(my_table).values(id="some_existing_id", data="inserted value")

on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update(
    [
        ("data", "some data"),
        ("updated_at", func.current_timestamp()),
    ],
)

Улучшения и изменения в диалекте - SQLite

Добавлена поддержка SQLite JSON

Добавлен новый тип данных JSON, который реализует функции доступа к членам json от имени базового типа данных SQLite JSON. Функции SQLite JSON_EXTRACT и JSON_QUOTE используются реализацией для обеспечения базовой поддержки JSON.

Обратите внимание, что имя самого типа данных, отображаемого в базе данных, имеет название «JSON». Это приведет к созданию типа данных SQLite с «числовой» принадлежностью, что обычно не должно вызывать проблем, за исключением случаев, когда значение JSON состоит из одного целого числа. Тем не менее, следуя примеру, приведенному в собственной документации SQLite по адресу https://www.sqlite.org/json1.html, имя JSON используется для удобства.

#3850

Добавлена поддержка SQLite ON CONFLICT в ограничениях

SQLite поддерживает нестандартное предложение ON CONFLICT, которое может быть указано для автономных ограничений, а также для некоторых ограничений в столбцах, таких как NOT NULL. Поддержка этих условий была добавлена с помощью ключевого слова sqlite_on_conflict, добавляемого к объектам типа UniqueConstraint, а также нескольких Column -специфических вариантов:

some_table = Table(
    "some_table",
    metadata_obj,
    Column("id", Integer, primary_key=True, sqlite_on_conflict_primary_key="FAIL"),
    Column("data", Integer),
    UniqueConstraint("id", "data", sqlite_on_conflict="IGNORE"),
)

В операторе CREATE TABLE приведенная выше таблица будет выглядеть так:

CREATE TABLE some_table (
    id INTEGER NOT NULL,
    data INTEGER,
    PRIMARY KEY (id) ON CONFLICT FAIL,
    UNIQUE (id, data) ON CONFLICT IGNORE
)

#4360

Улучшения и изменения в диалекте - Oracle

Национальные типы данных char отменены для общего юникода, снова включены с помощью опции

Типы данных Unicode и UnicodeText по умолчанию теперь соответствуют типам данных VARCHAR2 и CLOB на Oracle, а не NVARCHAR2 и NCLOB (так называемые «национальные» типы наборов символов). Это проявляется в том, как они отображаются в операторах CREATE TABLE, а также в том, что при использовании связанных параметров Unicode или UnicodeText объект типа не передается в setinputsizes(); cx_Oracle обрабатывает строковое значение нативно. Это изменение основано на рекомендации сопровождающего cx_Oracle о том, что «национальные» типы данных в Oracle в значительной степени устарели и не являются производительными. Кроме того, они мешают в некоторых ситуациях, например, когда применяются к спецификатору формата для функций типа trunc().

Единственный случай, когда NVARCHAR2 и связанные с ним типы могут понадобиться, - это база данных, не использующая набор символов, совместимый с Unicode. В этом случае флаг use_nchar_for_unicode может быть передан в create_engine() для повторного включения старого поведения.

Как всегда, при явном использовании типов данных NVARCHAR2 и NCLOB по-прежнему будут использоваться NVARCHAR2 и NCLOB, в том числе в DDL, а также при обработке связанных параметров с помощью cx_Oracle’s setinputsizes().

На стороне чтения в строках результатов CHAR/VARCHAR/CLOB добавлено автоматическое преобразование Unicode под Python 2, чтобы соответствовать поведению cx_Oracle под Python 3. Для того чтобы уменьшить падение производительности диалекта cx_Oracle при таком поведении под Python 2, под Python 2 используются очень производительные (при наличии C-расширений) нативные обработчики Unicode SQLAlchemy. Автоматическое согласование юникода может быть отключено установкой флага coerce_to_unicode в значение False. Теперь этот флаг по умолчанию равен True и применяется ко всем строковым данным, возвращаемым в наборе результатов, которые явно не относятся к типам данных Unicode или NVARCHAR2/NCHAR/NCLOB от Oracle.

#4242

Модернизированы аргументы подключения cx_Oracle, удалены устаревшие параметры

Внесен ряд изменений в параметры, принимаемые диалектом cx_oracle, а также в строку URL:

  • Удалены устаревшие параметры auto_setinputsizes, allow_twophase, exclude_setinputsizes.

  • Значение параметра threaded, которое по умолчанию всегда было равно True для диалекта SQLAlchemy, больше не генерируется по умолчанию. Сам объект SQLAlchemy Connection не считается потокобезопасным, поэтому нет необходимости передавать этот флаг.

  • Передавать threaded в сам create_engine() устарело. Чтобы установить значение threaded в True, передайте его либо в словарь create_engine.connect_args, либо используйте строку запроса, например, oracle+cx_oracle://...?threaded=true.

  • Все параметры, передаваемые в строке запроса URL, которые не используются в других случаях, теперь передаются в функцию cx_Oracle.connect(). Некоторые из них также принудительно преобразуются либо в константы cx_Oracle, либо в булевы значения, включая mode, purity, events и threaded.

  • Как и ранее, все аргументы cx_Oracle .connect() принимаются через словарь create_engine.connect_args, в документации была допущена неточность.

#4369

Улучшения и изменения в диалекте - SQL Server

Поддержка pyodbc fast_executemany

Недавно добавленный в Pyodbc режим «fast_executemany», доступный при использовании драйвера Microsoft ODBC, теперь является опцией для диалекта pyodbc / mssql. Передайте его через create_engine():

engine = create_engine(
    "mssql+pyodbc://scott:tiger@mssql2017:1433/test?driver=ODBC+Driver+13+for+SQL+Server",
    fast_executemany=True,
)

#4158

Новые параметры, влияющие на начало и приращение IDENTITY, использование Sequence устарело

SQL Server начиная с SQL Server 2012 теперь поддерживает последовательности с настоящим синтаксисом CREATE SEQUENCE. В #4235 SQLAlchemy добавит их поддержку с помощью Sequence точно так же, как и для любого другого диалекта. Однако в настоящее время ситуация такова, что Sequence был перепрофилирован на SQL Server специально для того, чтобы влиять на параметры «start» и «increment» для спецификации IDENTITY на столбце первичного ключа. Для перехода к использованию обычных последовательностей в серии 1.3 при использовании Sequence будет выдаваться предупреждение об устаревании. Для того чтобы повлиять на «начало» и «приращение», используйте новые параметры mssql_identity_start и mssql_identity_increment на Column:

test = Table(
    "test",
    metadata_obj,
    Column(
        "id",
        Integer,
        primary_key=True,
        mssql_identity_start=100,
        mssql_identity_increment=10,
    ),
    Column("name", String(20)),
)

Для того чтобы выдать IDENTITY по столбцу, не являющемуся первичным ключом, что является малораспространенным, но допустимым случаем использования SQL Server, используйте флаг Column.autoincrement, установив его в значение True по целевому столбцу, False по любому целочисленному столбцу первичного ключа:

test = Table(
    "test",
    metadata_obj,
    Column("id", Integer, primary_key=True, autoincrement=False),
    Column("number", Integer, autoincrement=True),
)

#4362

#4235

Изменено форматирование StatementError (новые строки и %s)

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

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

sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) A value is
required for bind parameter 'id' [SQL: 'select * from reviews\nwhere id = ?']
(Background on this error at: https://sqlalche.me/e/cd3x)

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

sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) A value is required for bind parameter 'id'
[SQL: select * from reviews
where id = ?]
(Background on this error at: https://sqlalche.me/e/cd3x)

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

#4500

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