Что нового в 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-only, а также к миру с аннотацией типов, и для достижения этой цели существуют намеренные планы по серьезной переработке 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, большинство случаев использования не основного отображения исчезло. Это было хорошо, поскольку SQLAlchemy также полностью отказалась от «классических» отображений примерно в серии 0.5 в пользу декларативной системы.

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

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

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

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

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 загрузки «selectin» загрузка из 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

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

from sqlalchemy import inspect

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

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

#4257

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

Объект расширения 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 исторически начинался как относительно простой «getter», рано стало очевидно, что ему также необходимо принимать решения о том, на какой тип атрибута он ссылается, например, скаляр или коллекция, сопоставленный объект или простое значение, и тому подобное. Для этого ему необходимо проверить сопоставленный атрибут или другой дескриптор или атрибут, на который он ссылается, как на ссылку из его родительского класса. Однако в механике дескрипторов 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%")))
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"))
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))
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")))
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

Реализована массовая замена для множеств, dicts с помощью 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 «swap». Учитывая стандартную установку «один ко многим/многие к одному»:

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 будет удалена во время flush. Эту же проблему можно продемонстрировать с помощью простых дубликатов:

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

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

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

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=[],
...     )
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=[],
...     )
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

Новая стратегия «последний-первый-выход» для QueuePool

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

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

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

Предупреждения, которые были впервые добавлены в версии 1.0 и описаны в Warnings emitted when coercing full SQL fragments into 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(<params>)
result = table.select().execute()

table.update().execute(<params>)

engine.commit()

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

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

    conn.execute(table.insert(), <params>)
    result = conn.execute(table.select())

    conn.execute(table.update(), <params>)

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

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

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

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

    conn.execute(table.update(), <params>)

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

#4393

параметры convert_unicode устарели

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

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

#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

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

Диалекты 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()),
    ],
)

См.также

mysql_insert_on_duplicate_key_update

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

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

Добавлен новый тип данных JSON, который реализует функции доступа к членам json от имени базового типа данных 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
)

См.также

sqlite_on_conflict_ddl

#4360

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

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

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

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

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

На стороне чтения в строки результатов 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,
)

См.также

mssql_pyodbc_fastexecutemany

#4158

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

SQL Server начиная с SQL Server 2012 теперь поддерживает последовательности с настоящим синтаксисом CREATE SEQUENCE. В #4235 SQLAlchemy добавит поддержку для них, используя Sequence точно так же, как и для любого другого диалекта. Однако в настоящее время ситуация такова, что Sequence был перепрофилирован на SQL Server специально для того, чтобы влиять на параметры «start» и «increment» для спецификации IDENTITY на столбце первичного ключа. Для того чтобы сделать переход к обычным последовательностям доступным, использование Sequence будет выдавать предупреждение об устаревании в серии 1.3. Для того чтобы повлиять на «начало» и «приращение», используйте новые параметры 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),
)

См.также

mssql_identity

#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

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