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

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

В данном документе описаны изменения между версиями SQLAlchemy 0.6, последний выпуск которой состоялся 5 мая 2012 года, и SQLAlchemy 0.7, находящейся на техническом обслуживании с октября 2012 года.

Дата документа: 27 июля 2011 г.

Введение

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

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

Второй, еще более узкий класс несовместимых изменений также документирован. К этому классу изменений относятся те возможности и поведения, которые были устаревшими, по крайней мере, с версии 0.5 и с момента их устаревания вызывали предупреждения. Эти изменения затронут только те приложения, которые все еще используют API в стиле 0.4 или ранней версии 0.5. По мере развития проекта в релизах уровня 0.x таких изменений становится все меньше и меньше, что является следствием того, что в нашем API все меньше функций, которые не являются идеальными для тех случаев использования, которые они должны были решить.

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

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

Новая система событий

SQLAlchemy начиналась с класса MapperExtension, который предоставлял крючки в цикл персистентности мапперов. По мере того, как SQLAlchemy быстро становилась все более компонентной, отводя картографам более целенаправленную конфигурационную роль, появилось множество классов «расширений», «слушателей» и «прокси» для решения различных задач по перехвату активности. Отчасти это было вызвано расхождением видов деятельности; объекты ConnectionProxy хотели предоставить систему перезаписи утверждений и параметров; AttributeExtension предоставляли систему замены входящих значений, а объекты DDL имели события, которые могли быть отключены от диалектно-чувствительных вызываемых таблиц.

В версии 0.7 практически все эти плагины реализованы заново, с использованием нового, унифицированного подхода, который сохраняет все функциональные возможности различных систем, обеспечивает большую гибкость и меньшее количество шаблонов, более высокую производительность и избавляет от необходимости изучать радикально различные API для каждой событийной подсистемы. Существовавшие ранее классы MapperExtension, SessionExtension, AttributeExtension, ConnectionProxy, PoolListener, а также метод DDLElement.execute_at устарели и теперь реализованы в терминах новой системы - эти API остаются полностью функциональными и, как ожидается, сохранятся в обозримом будущем.

Новый подход использует именованные события и определяемые пользователем callables для связывания действий с событиями. Внешний вид API был разработан на основе таких разнообразных источников, как JQuery, Blinker и Hibernate, а также неоднократно дорабатывался в ходе конференций с десятками пользователей в Twitter, который, как оказалось, имеет гораздо больший процент откликов, чем список рассылки для подобных вопросов.

В нем также реализована открытая система спецификации целей, позволяющая связывать события с классами API, например, для всех объектов Session или Engine, с конкретными экземплярами классов API, например, для конкретного Pool или Mapper, а также со связанными объектами, например, с определенным пользователем классом, который отображается, или с таким специфическим атрибутом для экземпляров определенного подкласса отображаемого родительского класса. Отдельные подсистемы слушателей могут применять обертки к входящим пользовательским функциям слушателей, которые изменяют способ их вызова - событие mapper может получать либо экземпляр объекта, с которым производится операция, либо его базовый объект InstanceState. Событие атрибута может выбрать, нести ли ответственность за возврат нового значения.

На новом событийном API построено несколько систем, включая новый API «изменяемых атрибутов», а также составных атрибутов. Повышенное внимание к событиям также привело к появлению ряда новых событий, включая операции истечения срока действия и обновления атрибутов, загрузки/выгрузки pickle, завершения операций построения mapper.

См.также

События

#1902

Гибридные атрибуты, реализует/заменяет synonym(), comparable_property()

Пример с «производными атрибутами» теперь превращен в официальное расширение. Типичным случаем использования synonym() является предоставление доступа дескриптора к сопоставленному столбцу; случаем использования comparable_property() является возможность возврата PropComparator из любого дескриптора. На практике подход «derived» проще в использовании, более расширяем, реализуется в нескольких десятках строк чистого Python, практически без импорта, и не требует, чтобы ядро ORM даже знало о нем. Теперь эта возможность известна как расширение «Hybrid Attributes».

synonym() и comparable_property() по-прежнему являются частью ORM, хотя их реализация вынесена за пределы ORM и построена на подходе, аналогичном гибридному расширению, так что основные модули ORM mapper/query/property не имеют о них никакого представления.

#1903

Повышение скорости

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

  • Теперь процесс flush будет объединять операторы INSERT в пакеты, передаваемые в cursor.executemany() для строк, в которых уже присутствует первичный ключ. В частности, это обычно относится к «дочерней» таблице в конфигурации наследования объединенных таблиц, что означает, что количество вызовов cursor.execute для большой массовой вставки объектов объединенных таблиц может быть сокращено вдвое, что позволяет использовать собственные оптимизации DBAPI для тех операторов, которые передаются в cursor.executemany() (например, повторное использование подготовленного оператора).

  • Значительно упрощен кодовый путь, вызываемый при обращении к уже загруженной ссылке «многие-к-одному» на связанный объект. Проверка карты идентичности выполняется напрямую, без необходимости генерировать новый объект Query, что является дорогостоящим в условиях обращения к тысячам in-memory many-to-one. Также для большинства ленивых загрузок атрибутов больше не используется построение объектов-загрузчиков на каждый вызов.

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

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

  • Внутреннее устройство ExecutionContext, объекта, соответствующего выполнению оператора, было включено и упрощено.

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

  • Коллекция «связующих процессоров» для конкретного экземпляра оператора Compiled также кэшируется на объекте Compiled, используя дополнительные преимущества «компилированного кэша», используемого процессом flush, для повторного использования одной и той же компилированной формы операторов INSERT, UPDATE, DELETE.

Демонстрация сокращения количества вызовов, включая пример эталонного сценария, находится по адресу https://techspot.zzzeek.org/2010/12/12/a-tale-of-three- profiles/

Композиты заново

Функция «composite», как и synonym() и comparable_property(), была переписана для использования более легковесной реализации, основанной на дескрипторах и событиях, а не на встраивании во внутренние компоненты ORM. Это позволило избавиться от некоторой задержки во внутреннем устройстве mapper/unit of work, а также упростить работу с композитом. Композитный атрибут больше не скрывает базовые столбцы, на которые он опирается, теперь они остаются обычными атрибутами. Композиты также могут выступать в качестве прокси для атрибутов relationship() и Column().

Основное изменение композитов, не совместимое с обратным ходом, заключается в том, что они больше не используют систему mutable=True для обнаружения мутаций на месте. Пожалуйста, используйте расширение Mutation Tracking для установления событий in-place изменений в существующих композитах.

#2008 #2024

Более лаконичная форма query.join(target, onclause)

Теперь по умолчанию используется метод выдачи query.join() на цель с явным onclause:

query.join(SomeClass, SomeClass.id == ParentClass.some_id)

В 0.6 такое использование считалось ошибкой, поскольку join() принимает несколько аргументов, соответствующих нескольким JOIN-клаузам - двухаргументная форма должна была находиться в кортеже, чтобы различать одноаргументные и двухаргументные цели присоединения. В середине 0.6 мы добавили обнаружение и сообщение об ошибке для этого специфического стиля вызова, так как он был очень распространен. В 0.7, поскольку мы все равно обнаруживаем именно этот шаблон, а необходимость вводить кортеж без всякой причины крайне раздражает, метод без кортежа стал «нормальным» способом. Случай использования «multiple JOIN» крайне редок по сравнению со случаем использования single join, а multiple joins в наши дни более наглядно представлено несколькими вызовами join().

Форма кортежа будет сохранена для обратной совместимости.

Заметим, что все остальные формы query.join() остаются неизменными:

query.join(MyClass.somerelation)
query.join("somerelation")
query.join(MyTarget)
# ... etc

Querying with Joins

#1923

Расширение события мутации, заменяющее «mutable=True».

Новое расширение Отслеживание мутаций предоставляет механизм, с помощью которого пользовательские типы данных могут передавать события изменения родительскому или родительским классам. Расширение включает в себя подход для скалярных значений базы данных, например, управляемых PickleType, postgresql.ARRAY или другими пользовательскими классами MutableType, а также подход для «композитов» ORM, сконфигурированных с помощью composite().

Операторы NULLS FIRST / NULLS LAST

Они реализованы в виде расширения операторов asc() и desc(), которые называются nullsfirst() и nullslast().

См.также

nullsfirst()

nullslast()

#723

select.distinct(), query.distinct() принимает *args для PostgreSQL DISTINCT ON

Это уже было доступно при передаче списка выражений в ключевой аргумент distinct в select(), метод distinct() в select() и Query теперь принимает позиционные аргументы, которые при использовании бэкенда PostgreSQL отображаются как DISTINCT ON.

distinct()

Query.distinct()

#1069

Index() может быть помещен в строку внутри Table, __table_args__

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

Table(
    "mytable",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("name", String(50), nullable=False),
    Index("idx_name", "name"),
)

Основное обоснование здесь - преимущества декларативного __table_args__, особенно при использовании с миксинами:

class HasNameMixin(object):
    name = Column("name", String(50), nullable=False)

    @declared_attr
    def __table_args__(cls):
        return (Index("name"), {})


class User(HasNameMixin, Base):
    __tablename__ = "user"
    id = Column("id", Integer, primary_key=True)

Indexes

Оконная функция SQL Конструкция

Оконная функция» предоставляет оператору информацию о результирующем наборе по мере его формирования. Это позволяет использовать критерии по таким параметрам, как «номер строки», «ранг» и т.д. Известно, что их поддерживают, по крайней мере, PostgreSQL, SQL Server и Oracle, возможно, и другие.

Лучшее знакомство с оконными функциями находится на сайте PostgreSQL, где оконные функции поддерживаются с версии 8.4:

https://www.postgresql.org/docs/current/static/tutorial-window.html

SQLAlchemy предоставляет простую конструкцию, которая обычно вызывается через существующее предложение функции с помощью метода over(), принимающего аргументы в виде ключевых слов order_by и partition_by. Ниже мы воспроизведем первый пример из учебника PG:

from sqlalchemy.sql import table, column, select, func

empsalary = table("empsalary", column("depname"), column("empno"), column("salary"))

s = select(
    [
        empsalary,
        func.avg(empsalary.c.salary)
        .over(partition_by=empsalary.c.depname)
        .label("avg"),
    ]
)

print(s)

SQL:

SELECT empsalary.depname, empsalary.empno, empsalary.salary,
avg(empsalary.salary) OVER (PARTITION BY empsalary.depname) AS avg
FROM empsalary

sqlalchemy.sql.expression.over

#1844

execution_options() на Connection принимает аргумент «isolation_level»

При этом устанавливается уровень изоляции транзакции для одного Connection, пока этот Connection не будет закрыт и его базовый DBAPI-ресурс не будет возвращен в пул соединений, после чего уровень изоляции будет возвращен к значению по умолчанию. Уровень изоляции по умолчанию задается с помощью аргумента isolation_level в create_engine().

Поддержка изоляции транзакций в настоящее время поддерживается только в бэкендах PostgreSQL и SQLite.

execution_options()

#2001

TypeDecorator работает с целочисленными столбцами первичного ключа

Со столбцом первичного ключа можно использовать TypeDecorator, который расширяет поведение Integer. Функция «автоинкремента» в Column теперь будет распознавать, что базовый столбец базы данных все еще является целым числом, так что механизмы lastrowid продолжают работать. Сам TypeDecorator будет иметь процессор обработки значений результатов, применяемый к вновь создаваемым первичным ключам, в том числе и к тем, которые получены с помощью аксессора DBAPI cursor.lastrowid.

#2005 #2006

TypeDecorator присутствует в пространстве импорта «sqlalchemy»

Больше не нужно импортировать это из sqlalchemy.types, теперь это зеркально отражено в sqlalchemy.

Новые диалекты

Добавлены диалекты:

  • драйвер MySQLdb для базы данных Drizzle:

    Drizzle

  • поддержка pymysql DBAPI:

    pymsql Notes

  • psycopg2 теперь работает с Python 3

Поведенческие изменения (обратная совместимость)

Сборка расширений C по умолчанию

Это относится к версии 0.7b4. Сборка эксетов будет производиться при обнаружении cPython 2.xx. Если сборка не удалась, например, при установке windows, это условие будет перехвачено, и установка без C продолжится. Эксклюзивы на языке C не будут собираться, если используется Python 3 или PyPy.

Query.count() упрощен, должен работать практически всегда

Очень старое гадание, которое происходило внутри Query.count(), было модернизировано для использования .from_self(). То есть, query.count() теперь эквивалентно:

query.from_self(func.count(literal_column("1"))).scalar()

Ранее внутренняя логика пыталась переписать строку columns самого запроса, а при обнаружении условия «подзапроса», например, запроса на основе столбцов, в котором могут присутствовать агрегаты, или запроса с DISTINCT, происходил сложный процесс переписывания строки columns. Такая логика не срабатывала в сложных условиях, особенно в условиях наследования объединенных таблиц, и давно устарела благодаря более полному вызову .from_self().

SQL, выдаваемый командой query.count(), теперь всегда имеет вид:

SELECT count(1) AS count_1 FROM (
    SELECT user.id AS user_id, user.name AS user_name from user
) AS anon_1

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

#2093

Чтобы выдать не подзапросную форму count()

Пользователи MySQL уже сообщили, что движок MyISAM, как это не удивительно, полностью падает при таком простом изменении. Заметим, что для простого count(), оптимизирующего работу с БД, которые не могут обрабатывать простые подзапросы, следует использовать func.count():

from sqlalchemy import func

session.query(func.count(MyClass.id)).scalar()

или для count(*):

from sqlalchemy import func, literal_column

session.query(func.count(literal_column("*"))).select_from(MyClass).scalar()

В формулах LIMIT/OFFSET теперь используются параметры привязки

Клаузы LIMIT и OFFSET или их эквиваленты (например, TOP, ROW NUMBER OVER и т.д.) используют параметры привязки для фактических значений для всех поддерживающих их бэкендов (большинство, кроме Sybase). Это позволяет повысить производительность оптимизатора запросов, поскольку текстовая строка для нескольких операторов с различными LIMIT/OFFSET теперь идентична.

#805

Улучшение ведения журнала

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

#1926

Упрощенное назначение polymorphic_on

Заселение атрибута polymorphic_on, отображаемого в столбец, при использовании сценария наследования теперь происходит при создании объекта, т.е. при вызове его метода __init__ с помощью события init. После этого атрибут ведет себя так же, как и любой другой атрибут, отображаемый на столбец. Ранее для заполнения этого столбца при флеше срабатывала специальная логика, что не позволяло пользовательскому коду изменять его поведение. Новый подход улучшает эту ситуацию тремя способами: 1) полиморфная идентичность теперь присутствует в объекте сразу после его создания; 2) полиморфная идентичность может быть изменена пользовательским кодом без каких-либо отличий в поведении от любого другого атрибута, отображаемого в столбце; 3) внутреннее устройство отображения во время flush упрощено и больше не требует специальных проверок для этого столбца.

#1895

contains_eager() выстраивает цепочки по нескольким путям (т.е. «all()»)

Вызовы `contains_eager()`` modifier now will chain itself for a longer path without the need to emit individual ``contains_eager()`. Вместо:

session.query(A).options(contains_eager(A.b), contains_eager(A.b, B.c))

можно сказать:

session.query(A).options(contains_eager(A.b, B.c))

#2032

Смыв детей-сирот, не имеющих родителей, разрешен

У нас давно существует поведение, которое проверяет наличие так называемых «сирот» во время промывки, т.е. объект, связанный с relationship(), в котором указан каскад «удаление-сирота», был недавно добавлен в сессию для INSERT, и не было установлено родительское отношение. Эта проверка была добавлена несколько лет назад, чтобы приспособить некоторые тестовые примеры, которые проверяли поведение «сирот» на согласованность. В современном SQLA эта проверка на стороне Python больше не нужна. Эквивалентное поведение «проверки сироты» достигается за счет того, что ссылка внешнего ключа на родительскую строку объекта становится NOT NULL, при этом база данных выполняет свою работу по установлению согласованности данных точно так же, как SQLA позволяет выполнять большинство других операций. Если родительский внешний ключ объекта не нулевой, то строка может быть вставлена. Поведение «сироты» проявляется в том случае, когда объект был сохранен с определенным родителем, а затем отсоединяется от него, что приводит к выполнению для него оператора DELETE.

#1912

Предупреждения, выдаваемые, когда члены коллекции, скалярные референты не являются частью flush

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

Каскад save-update вступает в силу, когда объекты добавляются в Session или когда объекты впервые ассоциируются с родителем, так что объект и все, что с ним связано, обычно находятся в одном Session. Однако если каскад save-update отключен для конкретного relationship(), то такого поведения не происходит, и процесс flush не пытается его исправить, оставаясь последовательным в соответствии с настроенным поведением каскада. Ранее при обнаружении таких объектов во время промывки они молча пропускались. Новое поведение заключается в том, что выдается предупреждение, чтобы предупредить о ситуации, которая чаще всего является источником неожиданного поведения.

#1973

Установка больше не устанавливает плагин Nose

С момента перехода на nose мы использовали плагин, устанавливаемый через setuptools, чтобы скрипт nosetests автоматически запускал код плагина SQLA, необходимый для полноценного окружения наших тестов. В середине 0.6 мы поняли, что шаблон импорта здесь означает, что плагин «покрытия» Nose сломается, поскольку «покрытие» требует, чтобы он был запущен до импорта модулей, подлежащих покрытию; поэтому в середине 0.6 мы усугубили ситуацию, добавив в сборку отдельный пакет sqlalchemy-nose для решения этой проблемы.

В 0.7 мы отказались от попыток заставить nosetests работать автоматически, поскольку модуль SQLAlchemy привел бы к появлению большого количества конфигурационных опций nose для всех случаев использования nosetests, а не только для самих юнит-тестов SQLAlchemy, а дополнительная установка sqlalchemy-nose была еще более неудачной идеей, создающей лишний пакет в окружении Python. Сценарий sqla_nose.py в 0.7 теперь является единственным способом запуска тестов с помощью nose.

#1949

Не``Table``-производные конструкции могут быть отображены

Конструкция, которая вообще не противоречит никаким Table, например, функция, может быть отображена.

from sqlalchemy import select, func
from sqlalchemy.orm import mapper


class Subset(object):
    pass


selectable = select(["x", "y", "z"]).select_from(func.some_db_function()).alias()
mapper(Subset, selectable, primary_key=[selectable.c.x])

#1876

aliased() принимает элементы FromClause

Это удобный помощник, позволяющий в случае передачи обычной FromClause, такой как select, Table или join в конструкцию orm.aliased(), перейти к методу .alias() этой конструкции, а не создавать AliasedClass уровня ORM.

#2018

Session.connection(), Session.execute() принимают „bind“

Это сделано для того, чтобы позволить операциям execute/connection участвовать в открытой транзакции движка в явном виде. Это также позволяет пользовательским подклассам Session, реализующим собственный метод get_bind() и аргументы, одинаково использовать эти аргументы как в методе execute(), так и в методе connection().

Session.connection Session.execute

#1996

Автономные параметры связывания в столбцах клаузул автоматически маркируются.

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

SQLite - относительные пути к файлам нормализуются с помощью os.path.abspath()

Таким образом, скрипт, изменяющий текущий каталог, будет продолжать ориентироваться на это же место при последующих соединениях с SQLite.

#2036

MS-SQL - String/Unicode/VARCHAR/NVARCHAR/VARBINARY выдает «max» при отсутствии длины

В бэкенде MS-SQL типы String/Unicode и их аналоги VARCHAR/ NVARCHAR, а также VARBINARY (#1833) выдают «max» в качестве длины, если длина не указана. Это делает его более совместимым с типом VARCHAR в PostgreSQL, который аналогично является неограниченным, если длина не указана. В SQL Server длина этих типов по умолчанию равна „1“, если длина не указана.

Изменения в поведении (обратная несовместимость)

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

PickleType и мутабельность ARRAY по умолчанию отключены

Это изменение относится к поведению ORM по умолчанию при отображении столбцов, имеющих тип данных PickleType или postgresql.ARRAY. Теперь флаг mutable по умолчанию устанавливается в значение False. Если существующее приложение использует эти типы и зависит от обнаружения мутаций in-place, то для восстановления поведения в версии 0.6 объект типа должен быть построен с флагом mutable=True:

Table(
    "mytable",
    metadata,
    # ....
    Column("pickled_data", PickleType(mutable=True)),
)

Флаг mutable=True постепенно отменяется в пользу нового расширения Mutation Tracking. Это расширение предоставляет механизм, с помощью которого пользовательские типы данных могут передавать события изменения обратно родительскому или родительским типам.

Предыдущий подход, предполагающий использование mutable=True, не предусматривает событий изменения - вместо этого ORM должен при каждом вызове flush() просканировать все мутабельные значения, присутствующие в сессии, и сравнить их с исходным значением на предмет изменений, что является очень трудоемким мероприятием. Это осталось с самых первых дней существования SQLAlchemy, когда flush() не был автоматическим, а система отслеживания истории не была столь совершенной, как сейчас.

Существующие приложения, использующие PickleType, postgresql.ARRAY или другие подклассы MutableType и требующие обнаружения мутаций на месте, должны перейти на новую систему отслеживания мутаций, поскольку mutable=True в будущем, скорее всего, будет устаревшей.

#1980

Для определения мутабельности composite() требуется расширение Mutation Tracking Extension

Так называемые «составные» атрибуты, сконфигурированные с помощью техники, описанной в Composite Column Types, были реализованы таким образом, что внутренние компоненты ORM больше не знают о них (что позволяет сократить и сделать более эффективными кодовые пути в критических разделах). Хотя композитные типы, как правило, должны рассматриваться как неизменяемые объекты значений, это никогда не соблюдалось. Для приложений, использующих композитные типы с возможностью изменения, расширение Mutation Tracking предлагает базовый класс, который устанавливает механизм, позволяющий пользовательским композитным типам посылать сообщения о событиях изменения родителям или родителям каждого объекта.

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

SQLite - диалект SQLite теперь использует NullPool для файловых баз данных

Это изменение 99,999% обратно совместимо, если только вы не используете временные таблицы в соединениях с пулом соединений.

Файловое соединение SQLite работает молниеносно, а использование NullPool означает, что каждый вызов Engine.connect создает новое соединение pysqlite.

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

SingletonThreadPool по-прежнему является движком по умолчанию, если используется база данных :memory:.

Обратите внимание, что это изменение разрушает временные таблицы, используемые между фиксациями сессии, из-за особенностей работы SQLite с временными таблицами. Если требуется создать временные таблицы, выходящие за рамки одного соединения с пулом, см. примечание на https://www.sqlalchemy.org/docs/dialects/sqlite.html#using- temporary-tables-with-sqlite.

#1921

Session.merge() проверяет идентификаторы версий для версионных картографов

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

При объединении данных в версионное состояние атрибут version id можно оставить неопределенным, и проверка версии не будет производиться.

Эта проверка была подтверждена изучением того, что делает Hibernate - и функция merge(), и функция версионирования были изначально заимствованы из Hibernate.

#2027

Имена меток кортежей в Query Improved

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

Даны два сопоставленных класса Foo и Bar, каждый из которых имеет столбец spam:

qa = session.query(Foo.spam)
qb = session.query(Bar.spam)

qu = qa.union(qb)

Имя, присваиваемое единственному столбцу, получаемому в результате qu, будет spam. Ранее это было что-то вроде foo_spam из-за того, что union объединял вещи, что несовместимо с именем spam в случае не объединенного запроса.

#1942

Атрибуты сопоставленных столбцов сначала ссылаются на наиболее специфичный столбец

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

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

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)


class Child(Parent):
    __tablename__ = "child"
    id = Column(Integer, ForeignKey("parent.id"), primary_key=True)

Выше атрибут Child.id ссылается как на колонку child.id, так и на parent.id - это связано с названием атрибута. Если бы в классе он был назван по-другому, например Child.child_id, то он сопоставлялся бы с child.id, а Child.id был бы тем же атрибутом, что и Parent.id.

Когда атрибут id ссылается на parent.id и child.id, он сохраняет их в упорядоченном списке. Тогда такое выражение, как Child.id, при выводе ссылается только на один из этих столбцов. До версии 0.6 таким столбцом был parent.id. В 0.7 это менее удивительное выражение child.id.

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

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

class Child(Parent):
    __tablename__ = "child"
    id = Column(Integer, ForeignKey("parent.id"), primary_key=True)
    some_related = relationship(
        "SomeRelated", primaryjoin="Child.id==SomeRelated.child_id"
    )


class SomeRelated(Base):
    __tablename__ = "some_related"
    id = Column(Integer, primary_key=True)
    child_id = Column(Integer, ForeignKey("child.id"))

До версии 0.7 выражение Child.id ссылалось на Parent.id, и необходимо было сопоставлять child.id с отдельным атрибутом.

Это также означает, что запрос, подобный этому, изменяет его поведение:

session.query(Parent).filter(Child.id > 7)

В версии 0.6 это будет выглядеть так:

SELECT parent.id AS parent_id
FROM parent
WHERE parent.id > :id_1

в 0,7, получаем:

SELECT parent.id AS parent_id
FROM parent, child
WHERE child.id > :id_1

который, как вы заметили, является картезианским произведением - такое поведение теперь эквивалентно поведению любого другого атрибута, локального для Child. Метод with_polymorphic() или аналогичная стратегия явного объединения базовых объектов Table используется для вывода запроса ко всем объектам Parent с критериями Child, аналогично тому, как это сделано в 0.5 и 0.6:

print(s.query(Parent).with_polymorphic([Child]).filter(Child.id > 7))

Причем как на рендерах 0.6, так и на рендерах 0.7:

SELECT parent.id AS parent_id, child.id AS child_id
FROM parent LEFT OUTER JOIN child ON parent.id = child.id
WHERE child.id > :id_1

Другим следствием этого изменения является то, что при объединенном наследовании двух таблиц загрузка будет производиться из значений дочерней таблицы, а не родительской. Необычный случай: запрос к «Родительской» с использованием with_polymorphic="*" выдает запрос к «Родительской» с LEFT OUTER JOIN к «Детской». Строка находится в «Parent», видит, что полиморфная идентичность соответствует «Child», но предположим, что фактическая строка в «child» была удалена. В результате этого повреждения строка приходит со всеми столбцами, соответствующими «child», установленными в NULL - теперь заполняется именно это значение, а не то, которое было в родительской таблице.

#1892

Сопоставление с объединениями с двумя или более одноименными столбцами требует явного объявления

Это в некоторой степени связано с предыдущим изменением в #1892. При отображении в join одноименные столбцы должны быть явно связаны с отображаемыми атрибутами, т.е. так, как описано в Mapping a Class Against Multiple Tables.

Если даны две таблицы foo и bar, каждая из которых имеет столбец первичного ключа id, то при выполнении следующей операции возникает ошибка:

foobar = foo.join(bar, foo.c.id == bar.c.foo_id)
mapper(FooBar, foobar)

Это происходит потому, что mapper() отказывается угадывать, какой столбец является первичным представлением FooBar.id - это foo.c.id или это bar.c.id ? Атрибут должен быть явным:

foobar = foo.join(bar, foo.c.id == bar.c.foo_id)
mapper(FooBar, foobar, properties={"id": [foo.c.id, bar.c.id]})

#1896

Mapper требует, чтобы колонка polymorphic_on присутствовала в отображаемом selectable

В версии 0.6 это было предупреждением, а в версии 0.7 стало ошибкой. Столбец, указанный для polymorphic_on, должен быть в отображаемом выбираемом. Это сделано для предотвращения некоторых случайных ошибок пользователей, таких как:

mapper(SomeClass, sometable, polymorphic_on=some_lookup_table.c.id)

где выше полиморфный_on должен находиться на столбце sometable, в данном случае, возможно, sometable.c.some_lookup_id. Существуют также некоторые сценарии «полиморфного объединения», в которых иногда возникают подобные ошибки.

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

#1875

В конструкциях DDL() теперь исключаются знаки процента

Ранее для тех DBAPI, которые принимают привязки pyformat или format (например, psycopg2, mysql-python), необходимо было экранировать знаки процента в строках DDL(), т.е. %% в зависимости от DBAPI, что было несовместимо с конструкциями text(), которые делали это автоматически. Теперь для DDL() выполняется такое же экранирование, как и для text().

#1897

Table.c / MetaData.tables немного доработаны, не допускают прямой мутации

Еще одна область, в которой некоторые пользователи делали навороты, которые на самом деле не работают так, как ожидалось, но все же оставляют крайне малую вероятность того, что какое-то приложение полагается на такое поведение. Конструкция, возвращаемая атрибутом .c на Table и атрибутом .tables на MetaData, явно не является мутабельной. «Мутабельная» версия конструкции теперь является приватной. Добавление столбцов в .c предполагает использование метода append_column() из Table, который гарантирует, что вещи будут связаны с родительским Table соответствующим образом; Аналогично, MetaData.tables имеет контракт с объектами Table, хранящимися в этом словаре, а также немного нового учета, заключающегося в отслеживании set() всех имен схем, который удовлетворяется только при использовании публичного конструктора Table, а также Table.tometadata().

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

#1893 #1917

server_default последовательно возвращает None для всех значений inserted_primary_key

Установлена согласованность при наличии server_default в столбце Integer PK. SQLA не осуществляет предварительную выборку таких столбцов, и они не возвращаются в cursor.lastrowid (DBAPI). Убедились, что все бэкенды последовательно возвращают None в result.inserted_primary_key для этих столбцов - некоторые бэкенды могли возвращать значение ранее. Использование значения server_default для столбца первичного ключа является крайне необычным. Если для генерации значений по умолчанию для первичного ключа используется специальная функция или SQL-выражение, их следует установить в качестве «значения по умолчанию» на стороне Python вместо server_default.

Что касается отражения для данного случая, то отражение int PK col с server_default устанавливает флаг «autoincrement» в False, за исключением случая PG SERIAL col, где мы обнаружили sequence default.

#2020 #2021

Псевдоним sqlalchemy.exceptions в sys.modules удален

В течение нескольких лет мы добавляли к sys.modules строку sqlalchemy.exceptions, чтобы работало утверждение типа «import sqlalchemy.exceptions». Основной модуль исключений уже давно называется exc, поэтому рекомендуемый импорт для этого модуля следующий:

from sqlalchemy import exc

Имя exceptions по-прежнему присутствует в «sqlalchemy» для приложений, которые могли бы сказать from sqlalchemy import exceptions, но они также должны начать использовать имя exc.

Изменения в рецептах временных параметров запросов

Хотя это и не относится к самой SQLAlchemy, стоит отметить, что в связи с переработкой ConnectionProxy в новую систему событий, она больше не подходит для рецепта «Тайминг всех запросов». Пожалуйста, настройте таймеры запросов на использование событий before_cursor_execute() и after_cursor_execute(), как показано в обновленном рецепте UsageRecipes/Profiling.

Утративший актуальность API

Конструктор по умолчанию для типов не будет принимать аргументы

Простые типы типа Integer, Date и т.д. в модуле core types не принимают аргументов. Конструктор по умолчанию, принимающий/игнорирующий catchall \*args, \**kwargs, восстановлен в версии 0.7b4/0.7.0, но выдает предупреждение об устаревании.

Если аргументы используются с основным типом, например Integer, то, возможно, вы хотели использовать тип, специфичный для диалекта, например sqlalchemy.dialects.mysql.INTEGER, который принимает аргумент «display_width».

compile_mappers() переименована в configure_mappers(), упрощены внутренние настройки конфигурации

Эта система постепенно превратилась из чего-то небольшого, реализованного локально для отдельного картографа и плохо названного, в нечто более глобальное на уровне «реестра» и плохо названное, поэтому мы исправили и то, и другое, полностью вынеся реализацию из Mapper и переименовав ее в configure_mappers(). Конечно, обычно приложению нет необходимости вызывать configure_mappers(), так как этот процесс происходит по мере необходимости, как только сопоставление требуется через доступ к атрибутам или запросам.

#1966

Основной слушатель/прокси вытеснен слушателями событий

На смену PoolListener, ConnectionProxy, DDLElement.execute_at приходят event.listen(), использующие цели диспетчеризации PoolEvents, EngineEvents, DDLEvents соответственно.

ORM-расширения, вытесненные слушателями событий

MapperExtension, AttributeExtension, SessionExtension заменяются на event.listen(), использующие цели диспетчеризации MapperEvents/InstanceEvents, AttributeEvents, SessionEvents, соответственно.

Передача строки в „distinct“ в select() для MySQL должна осуществляться через префиксы

Эта малоизвестная особенность позволяет использовать данный паттерн с бэкендом MySQL:

select([mytable], distinct="ALL", prefixes=["HIGH_PRIORITY"])

Для нестандартных или необычных префиксов следует использовать ключевое слово prefixes или метод prefix_with():

select([mytable]).prefix_with("HIGH_PRIORITY", "ALL")

useexisting заменен на extend_existing и keep_existing

Флаг useexisting для Table заменен новой парой флагов keep_existing и extend_existing. extend_existing эквивалентен useexisting - возвращается существующая таблица и добавляются дополнительные элементы конструктора. При использовании флага keep_existing возвращается существующая таблица, но дополнительные элементы конструктора не добавляются - эти элементы применяются только при новом создании таблицы.

Изменения в API с обратной несовместимостью

Callables, переданные в bindparam(), не оцениваются - влияет на пример с Beaker

#1950

Обратите внимание, что это касается примера с кэшированием Beaker, где работа функции _params_from_query() требовала небольшой корректировки. Если вы используете код из примера Beaker, то это изменение должно быть применено.

types.type_map теперь является приватным, types._type_map

Мы заметили, что некоторые пользователи обращаются к этому словарю внутри sqlalchemy.types в качестве быстрого способа ассоциирования типов Python с типами SQL. Мы не можем гарантировать содержимое или формат этого словаря, кроме того, ассоциирование типов Python по принципу «один к одному» имеет некоторые серые области, которые лучше решать в отдельных приложениях, поэтому мы подчеркнули этот атрибут.

#1870

alias``Переименовать ключевое слово arg автономной функции ``alias() в name

Таким образом, аргумент ключевого слова name совпадает с аргументом методов alias() на всех объектах FromClause, а также с аргументом name на Query.subquery().

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

Непубличные методы Pool подчеркнуты

Все методы Pool и подклассов, не предназначенные для публичного использования, переименованы с подчеркиванием. То, что они не были названы таким образом ранее, было ошибкой.

Методы пулинга теперь подчеркиваются или удаляются:

Pool.create_connection() -> Pool._create_connection()

Pool.do_get() -> Pool._do_get()

Pool.do_return_conn() -> Pool._do_return_conn()

Pool.do_return_invalid() -> удалено, не использовалось

Pool.return_conn() -> Pool._return_conn()

Pool.get() -> Pool._get(), публичный API - Pool.connect().

SingletonThreadPool.cleanup() -> _cleanup()

SingletonThreadPool.dispose_local() -> удалено, используется conn.invalidate()

#1982

Ранее устаревшие, теперь исключенные

Query.join(), Query.outerjoin(), eagerload(), eagerload_all() и другие больше не допускают в качестве аргументов списки атрибутов

Передача списка атрибутов или имен атрибутов в Query.join, eagerload() и им подобные была отменена начиная с версии 0.5:

# old way, deprecated since 0.5
session.query(Houses).join([Houses.rooms, Room.closets])
session.query(Houses).options(eagerload_all([Houses.rooms, Room.closets]))

Все эти методы принимают *args, начиная с версии 0.5:

# current way, in place since 0.5
session.query(Houses).join(Houses.rooms, Room.closets)
session.query(Houses).options(eagerload_all(Houses.rooms, Room.closets))

ScopedSession.mapper удаляется

Эта возможность предоставляла расширение mapper, которое связывало функциональность, основанную на классах, с определенным ScopedSession, в частности, обеспечивая поведение, при котором новые экземпляры объектов автоматически ассоциировались с этой сессией. Эта возможность была чрезмерно использована в учебниках и фреймворках, что привело к путанице пользователей из-за ее неявного поведения, и в версии 0.5.5 она была упразднена. Методы воспроизведения ее функциональности приведены в [wiki:UsageRecipes/SessionAwareMapper].

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