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

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

В данном документе описаны изменения между версиями SQLAlchemy 0.5, выпущенной 16 января 2010 года, и SQLAlchemy 0.6, выпущенной 5 мая 2012 года.

Дата документа: 6 июня 2010 г.

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

Поддержка платформы

  • cPython версии 2.4 и выше всей серии 2.xx

  • Jython 2.5.1 - использование входящего в состав Jython интерфейса zxJDBC DBAPI.

  • cPython 3.x - информацию о сборке для python3 см. в [source:sqlalchemy/trunk/README.py3k].

Новая диалектная система

Модули диалекта теперь разбиты на отдельные подкомпоненты в рамках одного бэкенда базы данных. Реализации диалектов теперь находятся в пакете sqlalchemy.dialects. Пакет sqlalchemy.databases по-прежнему существует как временное ядро, обеспечивающее некоторый уровень обратной совместимости для простого импорта.

Для каждой поддерживаемой базы данных внутри sqlalchemy.dialects существует подпакет, содержащий несколько файлов. Каждый пакет содержит модуль base.py, определяющий конкретный диалект SQL, используемый данной базой данных. Кроме того, пакет содержит один или несколько «драйверных» модулей, каждый из которых соответствует определенному DBAPI - эти файлы имеют имя, соответствующее самому DBAPI, например, pysqlite, cx_oracle или pyodbc. Классы, используемые диалектами SQLAlchemy, сначала объявляются в модуле base.py, определяя все поведенческие характеристики, задаваемые базой данных. К ним относятся отображения возможностей, такие как «поддерживает последовательности», «поддерживает возврат» и т.д., определения типов и правила компиляции SQL. Каждый модуль «драйвера», в свою очередь, при необходимости предоставляет подклассы этих классов, которые переопределяют поведение по умолчанию, чтобы учесть дополнительные возможности, поведение и причуды данного DBAPI. Для DBAPI, поддерживающих несколько бэкендов (pyodbc, zxJDBC, mxODBC), модуль диалекта будет использовать миксины из пакета sqlalchemy.connectors, которые обеспечивают функциональность, общую для данного DBAPI для всех бэкендов, в основном это касается аргументов connect. Это означает, что подключение с использованием pyodbc, zxJDBC или mxODBC (если оно реализовано) будет одинаковым для всех поддерживаемых бэкендов.

Формат URL, используемый в create_engine(), был усовершенствован для работы с любым количеством DBAPI для конкретного бэкенда по схеме, напоминающей JDBC. Прежний формат по-прежнему работает и позволяет выбрать реализацию DBAPI «по умолчанию», как, например, в приведенном ниже URL PostgreSQL, использующем psycopg2:

create_engine("postgresql://scott:tiger@localhost/test")

Однако чтобы указать конкретный бэкенд DBAPI, например pg8000, добавьте его в раздел «protocol» URL, используя знак плюс «+»:

create_engine("postgresql+pg8000://scott:tiger@localhost/test")

Важные диалектные ссылки:

  • Документация по аргументам подключения: https://www.sqlalchemy.org/docs/06/dbengine.html#create- engine-url-arguments.

  • Справочная документация по отдельным диалектам: https://ww w.sqlalchemy.org/docs/06/reference/dialects/index.html

  • Советы и рекомендации на DatabaseNotes.

Другие заметки, касающиеся диалектов:

  • В SQLAlchemy 0.6 система типов претерпела значительные изменения. Это повлияло на все диалекты в части соглашений об именовании, поведения и реализации. См. раздел «Типы» ниже.

  • Объект ResultProxy теперь обеспечивает двукратное улучшение скорости в некоторых случаях благодаря некоторым рефакторингам.

  • RowProxy, т.е. индивидуальный объект строки результата, теперь является непосредственно pickleable.

  • точка входа setuptools, используемая для поиска внешних диалектов, теперь называется sqlalchemy.dialects. Внешний диалект, написанный для 0.4 или 0.5, в любом случае придется модифицировать для работы с 0.6, так что это изменение не добавит никаких дополнительных сложностей.

  • Диалекты теперь получают событие initialize() при первом подключении для определения свойств соединения.

  • Функции и операторы, генерируемые компилятором, теперь используют (почти) обычные функции диспетчеризации вида «visit_<opname>» и «visit_<funcname>_fn» для обеспечения пользовательской обработки. Это заменяет необходимость копирования словарей «функций» и «операторов» в подклассах компилятора прямыми методами visitor, а также позволяет подклассам компилятора полностью контролировать отрисовку, поскольку в них передается полный объект _Function или _BinaryExpression.

Импорт диалектов

Изменилась структура импорта диалектов. Теперь каждый диалект экспортирует свой базовый класс «диалект», а также полный набор типов SQL, поддерживаемых в данном диалекте, через sqlalchemy.dialects.<name>. Например, для импорта набора типов PG:

from sqlalchemy.dialects.postgresql import (
    INTEGER,
    BIGINT,
    SMALLINT,
    VARCHAR,
    MACADDR,
    DATE,
    BYTEA,
)

Выше, INTEGER фактически является обычным типом INTEGER из sqlalchemy.types, но диалект PG делает его доступным так же, как и те типы, которые специфичны для PG, такие как BYTEA и MACADDR.

Изменения в языке выражения

Важная ошибка в языке выражения

В языке выражений есть одно довольно существенное изменение, которое может повлиять на некоторые приложения. Булевое значение булевых выражений Python, т.е. ==, != и им подобных, теперь точно оценивается относительно двух сравниваемых объектов клаузы.

Как известно, сравнение ClauseElement с любым другим объектом возвращает другой ClauseElement:

>>> from sqlalchemy.sql import column
>>> column("foo") == 5
<sqlalchemy.sql.expression._BinaryExpression object at 0x1252490>

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

>>> str(column("foo") == 5)
'foo = :foo_1'

Но что произойдет, если мы скажем это?

>>> if column("foo") == 5:
...     print("yes")

В предыдущих версиях SQLAlchemy возвращаемое значение _BinaryExpression представляло собой обычный объект Python, который оценивался как True. Теперь он оценивает, должно ли реальное ClauseElement иметь такое же хэш-значение, как и сравниваемое. Значение:

>>> bool(column("foo") == 5)
False
>>> bool(column("foo") == column("foo"))
False
>>> c = column("foo")
>>> bool(c == c)
True
>>>

Это означает код, подобный следующему:

if expression:
    print("the expression is:", expression)

Если expression является бинарным предложением, то оценка не производится. Поскольку указанный паттерн никогда не должен использоваться, то теперь базовый ClauseElement вызывает исключение, если вызывается в булевом контексте:

>>> bool(c)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  ...
    raise TypeError("Boolean value of this clause is not defined")
TypeError: Boolean value of this clause is not defined

Код, который хочет проверить наличие выражения ClauseElement, должен вместо этого сказать:

if expression is not None:
    print("the expression is:", expression)

Следует помнить, что это относится и к объектам Table и Column.

Это изменение объясняется двумя причинами:

  • Сравнения вида if c1 == c2:  <do something> фактически можно записать теперь так

  • Поддержка корректного хеширования объектов ClauseElement теперь работает на альтернативных платформах, а именно на Jython. До этого момента SQLAlchemy сильно зависела от специфического поведения cPython в этом отношении (и все еще имела с ним периодические проблемы).

Более строгое поведение «исполнителей»

В SQLAlchemy «executemany» соответствует вызову execute(), передающему коллекцию наборов связывающих параметров:

connection.execute(table.insert(), {"data": "row1"}, {"data": "row2"}, {"data": "row3"})

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

connection.execute(
    table.insert(),
    {"timestamp": today, "data": "row1"},
    {"timestamp": today, "data": "row2"},
    {"data": "row3"},
)

Потому что в третьей строке не указан столбец „timestamp“. Предыдущие версии SQLAlchemy просто вставляли NULL для этих отсутствующих столбцов. Однако если бы столбец timestamp в приведенном выше примере содержал значение или функцию по умолчанию на языке Python, он бы не использовался. Это объясняется тем, что операция «executemany» оптимизирована для максимальной производительности при огромном количестве наборов параметров и не пытается оценить значения по умолчанию на стороне Python для этих отсутствующих ключей. Поскольку умолчания часто реализуются либо как SQL-выражения, которые встраиваются в оператор INSERT, либо как выражения на стороне сервера, которые опять же срабатывают на основе структуры строки INSERT, которая по определению не может срабатывать условно на основе каждого набора параметров, было бы непоследовательно, чтобы умолчания на стороне Python вели себя иначе, чем умолчания на стороне SQL/сервера. (Умолчания, основанные на SQL-выражениях, начиная с версии 0.5 встраиваются в строку, опять же для минимизации влияния огромного количества наборов параметров).

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

UNION и другие «составные» конструкции последовательно заключаются в скобки

Было удалено правило, призванное помочь SQLite, согласно которому первый составной элемент внутри другого составного элемента (например, union() внутри except_()) не заключался в скобки. Это непоследовательно и приводит к неправильным результатам в PostgreSQL, где действуют правила приоритета в отношении INTERSECTION, и это, в общем-то, удивительно. При использовании сложных композитов в SQLite теперь необходимо превращать первый элемент в подзапрос (что также совместимо на PG). Новый пример приведен в учебнике по SQL-выражениям в конце раздела [https://www.sqlalchemy.org/docs/06/sqlexpression.html #unions-and-other-set-operations]. Дополнительную информацию см. в #1665 и r6690.

Расширения языка C для получения результатов

Элемент ResultProxy и связанные с ним элементы, включая наиболее распространенные функции «обработки строк», такие как преобразование юникода, числовые/булевые преобразования и разбор даты, были реализованы в виде дополнительных расширений на языке Си для повышения производительности. Это начало пути SQLAlchemy на «темную сторону», где мы надеемся продолжить повышение производительности за счет реализации критических секций на языке C. Расширения могут быть построены путем указания --with-cextensions, т.е. python setup.py --with- cextensions install.

Расширения наиболее сильно влияют на выборку результатов при прямом доступе ResultProxy, т.е. тех, которые возвращаются с помощью engine.execute(), connection.execute() или session.execute(). Внутри результатов, возвращаемых объектом ORM Query, на выборку результатов приходится не такой большой процент накладных расходов, поэтому производительность ORM повышается более скромно, и в основном в области выборки больших наборов результатов. Прирост производительности сильно зависит от используемого dbapi и от синтаксиса, используемого для доступа к столбцам каждой строки (например, row['name'] намного быстрее, чем row.name). Текущие расширения не влияют на скорость вставки/обновления/удаления, а также не улучшают задержку выполнения SQL, т.е. приложение, которое тратит большую часть времени на выполнение множества операторов с очень маленькими наборами результатов, не увидит значительного улучшения.

Производительность в 0.6 по сравнению с 0.5 повысилась независимо от расширений. Краткий обзор того, как выглядит подключение и получение 50 000 строк с помощью SQLite, используя в основном прямой доступ к SQLite, ResultProxy и простой отображаемый объект ORM:

sqlite select/native: 0.260s

0.6 / C extension

sqlalchemy.sql select: 0.360s
sqlalchemy.orm fetch: 2.500s

0.6 / Pure Python

sqlalchemy.sql select: 0.600s
sqlalchemy.orm fetch: 3.000s

0.5 / Pure Python

sqlalchemy.sql select: 0.790s
sqlalchemy.orm fetch: 4.030s

Выше ORM извлекает строки на 33% быстрее, чем 0.5, за счет повышения производительности in-python. С помощью расширений на языке C мы получаем еще 20%. При этом скорость обработки строк ResultProxy улучшается на 67% по сравнению с отсутствием расширения C. По данным других тестов, в некоторых сценариях, например, с большим количеством преобразований строк, прирост скорости достигает 200%.

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

Пакет sqlalchemy.schema получил давно необходимое внимание. Наиболее заметным изменением является новая расширенная система DDL. В SQLAlchemy, начиная с версии 0.5, можно было создавать пользовательские DDL-строки и связывать их с таблицами или объектами метаданных:

from sqlalchemy.schema import DDL

DDL("CREATE TRIGGER users_trigger ...").execute_at("after-create", metadata)

Теперь под одной системой доступен весь набор DDL-конструкций, включая конструкции CREATE TABLE, ADD CONSTRAINT и т.д:

from sqlalchemy.schema import Constraint, AddConstraint

AddContraint(CheckConstraint("value > 5")).execute_at("after-create", mytable)

Кроме того, все объекты DDL теперь являются обычными объектами ClauseElement, как и любой другой объект выражения SQLAlchemy:

from sqlalchemy.schema import CreateTable

create = CreateTable(mytable)

# dumps the CREATE TABLE as a string
print(create)

# executes the CREATE TABLE statement
engine.execute(create)

а с помощью расширения sqlalchemy.ext.compiler можно сделать свой собственный:

from sqlalchemy.schema import DDLElement
from sqlalchemy.ext.compiler import compiles


class AlterColumn(DDLElement):
    def __init__(self, column, cmd):
        self.column = column
        self.cmd = cmd


@compiles(AlterColumn)
def visit_alter_column(element, compiler, **kw):
    return "ALTER TABLE %s ALTER COLUMN %s %s ..." % (
        element.column.table.name,
        element.column.name,
        element.cmd,
    )


engine.execute(AlterColumn(table.c.mycolumn, "SET DEFAULT 'test'"))

Утраченные/удаленные элементы схемы

Пакет схем также был значительно оптимизирован. Многие опции и методы, которые были устаревшими в версии 0.5, были удалены. Также были удалены другие малоизвестные аксессоры и методы.

  • ключевой аргумент «owner» удален из Table. Используйте «schema» для представления любых пространств имен, которые должны быть добавлены к имени таблицы.

  • Устранены устаревшие MetaData.connect() и ThreadLocalMetaData.connect() - для связывания метаданных передавайте атрибут «bind».

  • Устранен устаревший метод metadata.table_iterator() (используйте sorted_tables)

  • аргумент «метаданные» удаляется из DefaultGenerator и подклассов, но остается локально присутствующим в Sequence, который является самостоятельной конструкцией в DDL.

  • deprecated PassiveDefault - используйте DefaultClause.

  • Убрана публичная мутабельность из объектов Index и Constraint:

    • ForeignKeyConstraint.append_element()

    • Index.append_column()

    • UniqueConstraint.append_column()

    • PrimaryKeyConstraint.add()

    • PrimaryKeyConstraint.remove()

Они должны быть построены декларативно (т.е. в одной конструкции).

  • Другие удаленные вещи:

    • Table.key (не знаю, для чего это нужно)

    • Column.bind (получить через column.table.bind)

    • Column.metadata (получить через column.table.metadata)

    • Column.sequence (использовать column.default)

Другие изменения в поведении

  • UniqueConstraint, Index, PrimaryKeyConstraint принимают в качестве аргументов списки имен столбцов или объектов столбцов.

  • Флаг use_alter на ForeignKey теперь является опцией быстрого доступа к операциям, которые могут быть сконструированы вручную с использованием системы событий DDL(). Побочным эффектом этого рефактора является то, что объекты ForeignKeyConstraint с флагом use_alter=True не будут выдаваться на SQLite, который не поддерживает ALTER для внешних ключей. На поведение SQLite это никак не влияет, поскольку SQLite фактически не соблюдает ограничения FOREIGN KEY.

  • Table.primary_key не является назначаемым - используйте table.append_constraint(PrimaryKeyConstraint(...))

  • Определение Column с ForeignKey и без типа, например Column(name, ForeignKey(sometable.c.somecol)), раньше получало тип ссылаемого столбца. Сейчас поддержка такого автоматического вывода типа является частичной и может работать не во всех случаях.

Открыта запись в журнал

За счет нескольких дополнительных вызовов методов можно установить уровни журнала INFO и DEBUG после создания движка, пула или маппера, и журналирование начнется. Метод isEnabledFor(INFO) теперь вызывается per-Connection и isEnabledFor(DEBUG) per-ResultProxy, если он уже включен на родительском соединении. Логирование пула отправляется в log.info() и log.debug() без проверки - обратите внимание, что проверка/выход из пула обычно происходит один раз за транзакцию.

Reflection/Inspector API

Система отражения, позволяющая отражать столбцы таблицы через Table('sometable', metadata, autoload=True), была раскрыта в собственный мелкоструктурный API, который позволяет напрямую просматривать такие элементы базы данных, как таблицы, столбцы, ограничения, индексы и т.д. В этом API возвращаемые значения выражаются в виде простых списков строк, словарей и объектов TypeEngine. Внутреннее устройство autoload=True теперь строится на основе этой системы, так что трансляция исходной информации базы данных в конструкции sqlalchemy.schema централизована, а контракт отдельных диалектов значительно упрощен, что значительно уменьшает количество ошибок и несоответствий в различных бэкендах.

Для использования инспектора:

from sqlalchemy.engine.reflection import Inspector

insp = Inspector.from_engine(my_engine)

print(insp.get_schema_names())

метод from_engine() в некоторых случаях предоставляет специфичный для бэкенда инспектор с дополнительными возможностями, как, например, у PostgreSQL, который предоставляет метод get_table_oid():

my_engine = create_engine("postgresql://...")
pg_insp = Inspector.from_engine(my_engine)

print(pg_insp.get_table_oid("my_table"))

Поддержка возврата

Конструкции insert(), update() и delete() теперь поддерживают метод returning(), который соответствует предложению SQL RETURNING, поддерживаемому в PostgreSQL, Oracle, MS-SQL и Firebird. Для других бэкендов он пока не поддерживается.

Если задать список выражений столбцов аналогично конструкции select(), то значения этих столбцов будут возвращены в виде регулярного набора результатов:

result = connection.execute(
    table.insert().values(data="some data").returning(table.c.id, table.c.timestamp)
)
row = result.first()
print("ID:", row["id"], "Timestamp:", row["timestamp"])

Реализация RETURNING в четырех поддерживаемых бэкендах сильно различается: в случае Oracle требуется сложное использование параметров OUT, которые перенаправляются в «шуточный» набор результатов, а в случае MS-SQL используется неудобный синтаксис SQL. На использование RETURNING накладываются ограничения:

  • он не работает для любого стиля выполнения «executemany()». Это ограничение всех поддерживаемых DBAPI.

  • Некоторые бэкенды, например Oracle, поддерживают RETURNING, возвращающий только один ряд - это относится и к операторам UPDATE и DELETE, то есть конструкция update() или delete() должна соответствовать только одному ряду, иначе возникнет ошибка (у Oracle, а не у SQLAlchemy).

RETURNING также автоматически используется SQLAlchemy, когда он доступен и когда иное не указано в явном вызове returning(), для получения значений вновь сгенерированных первичных ключей для однорядных операторов INSERT. Это означает, что для операторов вставки, где требуется значение первичного ключа, больше не нужно выполнять предварительный запрос «SELECT nextval(sequence)». Правда, неявная функция RETURNING несет в себе больше накладных расходов на метод, чем старая система «select nextval()», которая использовала быстрый и грязный cursor.execute() для получения значения последовательности, а в случае Oracle требовала дополнительного связывания параметров out. Поэтому, если накладные расходы на метод/протокол оказываются дороже дополнительных обращений к базе данных, функцию можно отключить, указав implicit_returning=False в качестве create_engine().

Изменения в системе типов

Новая архитектура

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

  • Отделить обработку параметров привязки и значений строк результатов, что обычно является требованием DBAPI, от SQL-спецификации самого типа, что является требованием базы данных. Это соответствует общему рефактору диалекта, который отделяет поведение SQL в базе данных от DBAPI.

  • Установить четкий и последовательный контракт для генерации DDL из объекта TypeEngine и для построения объектов TypeEngine на основе отражения столбцов.

Основные изменения включают:

  • Полностью переработано построение типов в диалектах. Диалекты теперь определяют общедоступные типы исключительно как имена UPPERCASE, а внутренние типы реализации - с помощью идентификаторов с подчеркиванием (т.е. являются частными). Система, с помощью которой типы выражаются в SQL и DDL, перенесена в систему компилятора. Это привело к тому, что в большинстве диалектов стало гораздо меньше объектов типов. Подробный документ по этой архитектуре для авторов диалектов находится в [source:/lib/sqlalc hemy/dialects/type_migration_guidelines.txt].

  • Отражение типов теперь возвращает точный тип UPPERCASE в файле types.py, или тип UPPERCASE в самом диалекте, если тип не является стандартным типом SQL. Это означает, что отражение теперь возвращает более точную информацию об отражаемых типах.

  • Определяемые пользователем типы, которые являются подклассом TypeEngine и хотят предоставлять get_col_spec(), теперь должны быть подклассом UserDefinedType.

  • Метод result_processor() всех классов типов теперь принимает дополнительный аргумент coltype. Это объект типа DBAPI, присоединенный к cursor.description, и его следует использовать в тех случаях, когда это необходимо для принятия более точных решений о том, какой тип вызываемого объекта обработки результатов должен быть возвращен. В идеале функции обработки результатов никогда не должны использовать isinstance(), который является дорогостоящим вызовом на этом уровне.

Родной режим Unicode

Поскольку все больше DBAPI поддерживают непосредственное возвращение объектов Python unicode, базовый диалект теперь при первом подключении выполняет проверку, которая устанавливает, возвращает ли DBAPI объект Python unicode для базового выбора значения VARCHAR. Если да, то тип String и все его подклассы (т.е. Text, Unicode и т.д.) будут пропускать шаг проверки/преобразования «юникода» при получении строк результатов. Это дает значительный прирост производительности для больших наборов результатов. В настоящее время известно, что «режим юникода» работает с:

  • sqlite3 / pysqlite

  • psycopg2 - SQLA 0.6 теперь по умолчанию использует расширение типа «UNICODE» для каждого объекта соединения psycopg2

  • pg8000

  • cx_oracle (мы используем выходной процессор - хорошая возможность !)

Другие типы при необходимости могут отключать обработку юникода, как, например, тип NVARCHAR при использовании с MS-SQL.

В частности, при переносе приложения, основанного на DBAPI, который ранее возвращал неюникодные строки, режим «native unicode» имеет явно иное поведение по умолчанию - столбцы, объявленные как String или VARCHAR, теперь по умолчанию возвращают юникод, тогда как раньше они возвращали строки. Это может привести к поломке кода, ожидающего неюникодных строк. Режим psycopg2 «native unicode» может быть отключен путем передачи use_native_unicode=False в create_engine().

Более общим решением для строковых столбцов, в которых явно не требуется объект unicode, является использование TypeDecorator, который преобразует unicode обратно в utf-8, или в то, что требуется:

class UTF8Encoded(TypeDecorator):
    """Unicode type which coerces to utf-8."""

    impl = sa.VARCHAR

    def process_result_value(self, value, dialect):
        if isinstance(value, unicode):
            value = value.encode("utf-8")
        return value

Обратите внимание, что флаг assert_unicode теперь устарел. SQLAlchemy позволяет используемому DBAPI и backend-базе данных обрабатывать параметры в Unicode, если они доступны, и не добавляет операционных затрат на проверку входящего типа; современные системы, такие как sqlite и PostgreSQL, в случае передачи некорректных данных выдадут ошибку кодировки на своей стороне. В тех случаях, когда SQLAlchemy необходимо перевести параметр связывания из Python Unicode в кодированную строку, или когда тип Unicode используется явно, выдается предупреждение, если объект является байтовой строкой. Это предупреждение может быть подавлено или преобразовано в исключение с помощью фильтра предупреждений Python, документированного по адресу: https://docs.python.org/library/warnings.html.

Generic Enum Type

Теперь у нас есть Enum в модуле types. Это строковый тип, которому задается набор «меток», ограничивающих возможные значения, присваиваемые этим меткам. По умолчанию этот тип генерирует VARCHAR, используя размер самой большой метки, и накладывает ограничение CHECK на таблицу в операторе CREATE TABLE. При использовании MySQL тип по умолчанию использует тип MySQL ENUM, а при использовании PostgreSQL тип будет генерировать определенный пользователем тип CREATE TYPE <mytype> AS ENUM. Для создания типа с использованием PostgreSQL необходимо указать в конструкторе параметр name. Тип также принимает параметр native_enum=False, который будет выдавать стратегию VARCHAR/CHECK для всех баз данных. Обратите внимание, что в настоящее время типы PostgreSQL ENUM не работают с pg8000 или zxjdbc.

Отражение возвращает диалектно-специфические типы

Теперь Reflection возвращает из базы данных наиболее конкретный тип. То есть, если создать таблицу с использованием String, а затем отразить ее обратно, то отраженный столбец, скорее всего, будет VARCHAR. Для диалектов, поддерживающих более конкретную форму типа, это то, что вы получите. Так, тип Text на Oracle будет иметь вид oracle.CLOB, LargeBinary может быть mysql.MEDIUMBLOB и т.д. Очевидным преимуществом здесь является то, что отражение сохраняет как можно больше информации из базы данных.

Некоторые приложения, работающие с метаданными таблиц, могут захотеть сравнивать типы в отраженных и/или неотраженных таблицах. Для этого на TypeEngine имеется полуприватный аксессор _type_affinity и связанный с ним помощник сравнения _compare_type_affinity. Этот аксессор возвращает «общий» класс types, которому соответствует тип:

>>> String(50)._compare_type_affinity(postgresql.VARCHAR(50))
True
>>> Integer()._compare_type_affinity(mysql.REAL)
False

Различные изменения в API

По-прежнему используется обычная система «общих» типов, т.е. String, Float, DateTime. Здесь есть несколько изменений:

  • Типы больше не делают никаких предположений относительно параметров по умолчанию. В частности, Numeric, Float, а также подклассы NUMERIC, FLOAT, DECIMAL не генерируют никакой длины или масштаба, если это не указано. Сюда же по-прежнему относятся спорные типы String и VARCHAR (хотя диалект MySQL будет упреждающе поднимать вопрос о выводе VARCHAR без длины). Никакие значения по умолчанию не принимаются, и если они используются в операторе CREATE TABLE, то будет выдана ошибка, если базовая база данных не позволяет использовать не удлиненные версии этих типов.

  • тип Binary переименован в LargeBinary, для BLOB/BYTEA/подобных типов. Для BINARY и VARBINARY они присутствуют непосредственно как types.BINARY, types.VARBINARY, а также в диалектах MySQL и MS-SQL.

  • PickleType теперь использует == для сравнения значений при mutable=True, если к типу не указан аргумент «comparator» с функцией сравнения. Если вы собираете пользовательский объект, то вам следует реализовать метод __eq__(), чтобы сравнение по значениям было точным.

  • Аргументы по умолчанию «precision» и «scale» для Numeric и Float были удалены и теперь имеют значение None. По умолчанию NUMERIC и FLOAT будут отображаться без числовых аргументов, если эти значения не указаны.

  • Типы DATE, TIME и DATETIME в SQLite теперь могут принимать необязательные аргументы «storage_format» и «regexp». «storage_format» может использоваться для хранения этих типов в пользовательском строковом формате. «regexp» позволяет использовать пользовательское регулярное выражение для сопоставления строковых значений из базы данных.

  • __legacy_microseconds__ на типах SQLite Time и DateTime больше не поддерживается. Вместо этого следует использовать новый аргумент «storage_format».

  • Типы DateTime на SQLite теперь по умолчанию используют более строгое регулярное выражение для сопоставления строк из базы данных. Используйте новый аргумент «regexp», если вы используете данные, хранящиеся в устаревшем формате.

Изменения в ORM

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

Новая единица измерения

Внутренние компоненты единицы работы, в первую очередь topological.py и unitofwork.py, были полностью переписаны и значительно упрощены. Это не должно повлиять на использование, так как все существующее поведение во время flush было сохранено в точности (или, по крайней мере, в той степени, в которой оно было реализовано в нашем тестовом наборе и в нескольких производственных средах, которые активно тестировали его). При выполнении flush() теперь используется на 20-30% меньше вызовов методов, а также должно потребляться меньше памяти. Замысел и поток исходного кода теперь достаточно легко проследить, а архитектура flush на данный момент является достаточно открытой, что создает пространство для потенциальных новых областей развития. Процесс промывки больше не зависит от рекурсии, поэтому можно выполнять промывку планов произвольного размера и сложности. Кроме того, процесс «сохранения» mapper’а, выполняющий операции INSERT и UPDATE, теперь кэширует «скомпилированную» форму этих двух операций, что позволяет значительно сократить количество вызовов при очень больших объемах прошивки.

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

Изменения в query.update() и query.delete()

  • Опция „expire“ в query.update() была переименована в „fetch“, что соответствует опции query.delete()

  • По умолчанию для стратегии синхронизации query.update() и query.delete() установлено значение „evaluate“.

  • стратегия „synchronize“ для update() и delete() приводит к ошибке. Неявного возврата на «fetch» не существует. Неудачная оценка основана на структуре критериев, поэтому успех/неудача детерминированы структурой кода.

relation() официально назван relationship()

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

Расширенная загрузка подзапросов

Добавлена новая разновидность ускоренной загрузки, называемая «подзапросом». Это загрузка, при которой сразу после первого SQL-запроса выполняется второй, который загружает полные коллекции для всех родительских запросов первого запроса, соединяясь с родительским запросом по возрастанию с помощью INNER JOIN. Загрузка подзапросов используется аналогично текущей загрузке joined-eager, с использованием `subqueryload()`` and ``subqueryload_all()`` options as well as the ``lazy='subquery'`` setting on ``relationship()`. Загрузка с помощью подзапросов обычно гораздо эффективнее при загрузке больших коллекций, поскольку в ней безоговорочно используется INNER JOIN, а также не происходит повторной загрузки родительских строк.

`eagerload()``, ``eagerload_all()`` is now ``joinedload()``, ``joinedload_all()`

Чтобы освободить место для новой функции загрузки подзапросов, существующие `eagerload()``/``eagerload_all()`` options are now superseded by ``joinedload()`` and ``joinedload_all()``. The old names will hang around for the foreseeable future just like ``relation()`.

`lazy=False|None|True|'dynamic'`` now accepts ``lazy='noload'|'joined'|'subquery'|'select'|'dynamic'`

В продолжение открытой темы о стратегиях загрузчиков, стандартные ключевые слова для `lazy`` option on ``relationship()`` are now ``select`` for lazy loading (via a SELECT issued on attribute access), ``joined`` for joined-eager loading, ``subquery`` for subquery-eager loading, ``noload`` for no loading should occur, and ``dynamic`` for a «dynamic» relationship. The old ``True``, ``False``, ``None` аргументы по-прежнему принимаются с тем же поведением, что и раньше.

innerjoin=True on relation, joinedload

Скалярам и коллекциям, загружаемым с нетерпением, теперь можно предписать использовать INNER JOIN вместо OUTER JOIN. На PostgreSQL это дает ускорение в 300-600% для некоторых запросов. Установите этот флаг для любого many-to-one, работающего с внешним ключом NOT NULL, и аналогично для любой коллекции, в которой гарантированно существуют связанные элементы.

На уровне картографа:

mapper(Child, child)
mapper(
    Parent,
    parent,
    properties={"child": relationship(Child, lazy="joined", innerjoin=True)},
)

На уровне времени запроса:

session.query(Parent).options(joinedload(Parent.child, innerjoin=True)).all()

Флаг innerjoin=True на уровне relationship() будет действовать и для любой опции joinedload(), которая не переопределяет его значение.

Усовершенствования «многие к одному

  • Отношения «многие-к-одному» теперь вызывают ленивую загрузку в меньшем количестве случаев, в том числе в большинстве случаев не будут получать «старое» значение при замене его новым.

  • отношение «многие-к-одному» к подклассу объединенной таблицы теперь использует get() для простой загрузки (известное как условие «use_get»), т.е. Related->``Sub(Base)``, без необходимости переопределения условия primaryjoin в терминах базовой таблицы. [ticket:1186]

  • Указание внешнего ключа с декларативным столбцом, т.е. ForeignKey(MyRelatedClass.id), не нарушает условия «use_get» [ticket:1492]

  • В relationship(), joinedload() и joinedload_all() появилась опция «innerjoin». Укажите True или False для того, чтобы определить, как будет построено нетерпеливое соединение - INNER или OUTER. По умолчанию, как всегда, используется False. Опции отображателя будут переопределять ту настройку, которая задана в relationship(). Как правило, этот параметр следует устанавливать для отношений типа «многие к одному», а не для отношений с нулевым внешним ключом, чтобы повысить производительность соединения. [ticket:1544]

  • поведение объединенной ускоренной загрузки, при котором основной запрос оборачивается в подзапрос при наличии LIMIT/OFFSET, теперь делает исключение для случая, когда все ускоренные загрузки являются объединениями «многие к одному». В таких случаях нетерпеливые соединения выполняются непосредственно к родительской таблице вместе с ограничением/смещением без дополнительных затрат на подзапрос, поскольку соединение «многие к одному» не добавляет строк к результату.

    Например, в 0,5 этот запрос:

    session.query(Address).options(eagerload(Address.user)).limit(10)

    выдаст SQL типа:

    SELECT * FROM
      (SELECT * FROM addresses LIMIT 10) AS anon_1
      LEFT OUTER JOIN users AS users_1 ON users_1.id = anon_1.addresses_user_id

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

    В 0.6 эта логика более чувствительна и может определить, что все eager-загрузчики представляют собой many-to-one, и в этом случае eager-соединения не влияют на количество строк:

    SELECT * FROM addresses LEFT OUTER JOIN users AS users_1 ON users_1.id = addresses.user_id LIMIT 10

Изменяемые первичные ключи с наследованием объединенных таблиц

Конфигурация наследования объединенных таблиц, в которой дочерняя таблица имеет PK, являющийся внешним ключом к родительскому PK, теперь может обновляться в базах данных с поддержкой CASCADE, таких как PostgreSQL. У параметра mapper() теперь есть опция passive_updates=True, которая указывает, что этот внешний ключ обновляется автоматически. Если вы используете некаскадную базу данных, например SQLite или MySQL/MyISAM, установите этот флаг в значение False. В будущем будет сделана попытка сделать этот флаг автоматически настраиваемым в зависимости от используемого диалекта/стиля таблицы.

Кэширование мензурок

Новый многообещающий пример интеграции Beaker представлен в examples/beaker_caching. Это простой рецепт, который применяет кэш Beaker в механизме генерации результатов Query. Параметры кэша предоставляются через query.options(), что позволяет полностью контролировать содержимое кэша. В SQLAlchemy 0.6 метод Session.merge() был усовершенствован для поддержки этого и подобных рецептов, а также для обеспечения значительно более высокой производительности в большинстве сценариев.

Другие изменения

  • объект «кортеж строк», возвращаемый командой Query при выборе нескольких столбцов/сущностей, теперь является picklable, а также имеет более высокую производительность.

  • query.join() был переработан для обеспечения более последовательного поведения и большей гибкости (включает [ticket:1537])

  • query.select_from() принимает несколько предложений для получения нескольких записей, разделенных запятыми, в предложении FROM. Это полезно при выборе из нескольких клаузул join().

  • флаг «dont_load=True» для Session.merge() устарел и теперь имеет значение «load=False».

  • добавлена вспомогательная функция «make_transient()», которая превращает постоянный/отсоединенный экземпляр в переходный (т.е. удаляет ключ_экземпляра и удаляет из любой сессии) [ticket:1052].

  • флаг allow_null_pks в mapper() устарел и был переименован в allow_partial_pks. По умолчанию он включен. Это означает, что строка, имеющая ненулевое значение для любого из столбцов первичного ключа, будет считаться идентичностью. Необходимость в этом сценарии обычно возникает только при сопоставлении с внешним объединением. При установке значения False PK, содержащий NULL, не будет считаться первичным ключом - в частности, это означает, что строка результата будет возвращаться как None (или не будет заполнена в коллекцию), а в новой версии 0.6 также означает, что session.merge() не будет выполнять обход базы данных для такого значения PK. [ticket:1680]

  • Механика «обратной ссылки» полностью объединена в более тонкую систему «back_populates» и происходит полностью внутри метода _generate_backref() в RelationProperty. Это упрощает процедуру инициализации RelationProperty и позволяет легче распространять настройки (например, из подклассов RelationProperty) в обратную ссылку. Внутренний BackRef() исчез, а backref() возвращает обычный кортеж, понятный RelationProperty.

  • атрибут keys в ResultProxy теперь является методом, поэтому ссылки на него (result.keys) должны быть заменены на вызовы методов (result.keys())

  • ResultProxy.last_inserted_ids теперь устарел, вместо него используйте ResultProxy.inserted_primary_key.

Утраченные/удаленные элементы ORM

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

  • Флаг „transactional“ в функции sessionmaker() и других удален. Используйте „autocommit=True“ для указания „transactional=False“.

  • Убран аргумент „polymorphic_fetch“ в функции mapper(). Загрузка может контролироваться с помощью опции „with_polymorphic“.

  • Аргумент „select_table“ в функции mapper() удален. Для этой функциональности используйте „with_polymorphic=(«*», <some selectable>)“.

  • Убран аргумент „proxy“ в функции synonym(). В версии 0.5 этот флаг ничего не делал, так как теперь «генерация прокси» происходит автоматически.

  • Передача одного списка элементов в функции joinedload(), joinedload_all(), contains_eager(), lazyload(), defer() и undefer() вместо нескольких позиционных *args является устаревшей.

  • Передача одного списка элементов в query.order_by(), query.group_by(), query.join() или query.outerjoin() вместо нескольких позиционных *args является устаревшей.

  • query.iterate_instances() удаляется. Используйте query.instances().

  • Query.query_from_parent() удаляется. Используйте функцию sqlalchemy.orm.with_parent() для получения «родительского» предложения, или, как вариант, query.with_parent().

  • query._from_self() удален, вместо него используйте query.from_self().

  • аргумент «comparator» в composite() удален. Используйте «comparator_factory».

  • RelationProperty._get_join() удаляется.

  • флаг „echo_uow“ на Session снят. Используйте протоколирование по имени «sqlalchemy.orm.unitofwork».

  • session.clear() удаляется. используйте session.expunge_all().

  • session.save(), session.update(), session.save_or_update() удалены. Используйте session.add() и session.add_all().

  • флаг «objects» в session.flush() остается устаревшим.

  • флаг «dont_load=True» в session.merge() устарел в пользу «load=False».

  • ScopedSession.mapper остается устаревшим. См. рецепт использования по адресу https://www.sqlalchemy.org/trac/wiki/Usag eRecipes/SessionAwareMapper.

  • Передача InstanceState (внутреннего объекта состояния SQLAlchemy) в attributes.init_collection() или attributes.get_history() является устаревшей. Эти функции являются публичными API и обычно ожидают обычный экземпляр объекта mapped.

  • параметр „engine“ в declarative_base() удаляется. Используйте аргумент с ключевым словом „bind“.

Расширения

SQLSoup

SQLSoup был модернизирован и обновлен с учетом общих возможностей 0.5/0.6, включая хорошо проработанную интеграцию с сессиями. Ознакомьтесь с новой документацией по адресу [https://www.sqlalc hemy.org/docs/06/reference/ext/sqlsoup.html].

Декларативный

DeclarativeMeta (метакласс по умолчанию для declarative_base) ранее позволял подклассам модифицировать dict_ для добавления атрибутов класса (например, столбцов). Теперь это не работает, конструктор DeclarativeMeta теперь игнорирует dict_. Вместо этого атрибуты класса должны присваиваться напрямую, например, cls.id=Column(...), или вместо подхода метаклассов следует использовать подход MixIn class.

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