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

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

В данном документе описаны изменения между версией SQLAlchemy 0.8, которая находилась на техническом обслуживании по состоянию на май 2013 года, и версией SQLAlchemy 0.9, первый производственный релиз которой состоялся 30 декабря 2013 года.

Последнее обновление документа: 10 июня 2015 г.

Введение

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

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

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

Ориентирован на Python 2.6 и выше, теперь Python 3 без 2to3

Первым достижением релиза 0.9 является устранение зависимости от инструмента 2to3 для обеспечения совместимости с Python 3. Чтобы сделать это более понятным, в настоящее время используется самый младший релиз Python - 2.6, который обладает широкой степенью кросс-совместимости с Python 3. Все модули и модульные тесты SQLAlchemy теперь одинаково хорошо интерпретируются любым интерпретатором Python, начиная с версии 2.6, включая интерпретаторы 3.1 и 3.2.

#2671

Расширения языка C, поддерживаемые в Python 3

Расширения языка Си были перенесены для поддержки Python 3 и теперь собираются как в среде Python 2, так и в среде Python 3.

#2161

Поведенческие изменения - ORM

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

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

>>> session.query(Vertex.start, Vertex.end).filter(Vertex.start == Point(3, 4)).all()
[(Point(x=3, y=4), Point(x=5, y=6))]

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

>>> session.query(Vertex.start.clauses, Vertex.end.clauses).filter(
...     Vertex.start == Point(3, 4)
... ).all()
[(3, 4, 5, 6)]

#2824

Query.select_from() больше не применяет оговорку к соответствующим сущностям

Метод Query.select_from() был популярен в последних версиях как средство управления тем, что первым «выбирает» объект Query, обычно для управления тем, как будет отображаться JOIN.

Рассмотрим следующий пример с обычным отображением User:

select_stmt = select([User]).where(User.id == 7).alias()

q = (
    session.query(User)
    .join(select_stmt, User.id == select_stmt.c.id)
    .filter(User.name == "ed")
)

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

SELECT "user".id AS user_id, "user".name AS user_name
FROM "user" JOIN (SELECT "user".id AS id, "user".name AS name
FROM "user"
WHERE "user".id = :id_1) AS anon_1 ON "user".id = anon_1.id
WHERE "user".name = :name_1

Если бы мы хотели изменить порядок следования левого и правого элементов JOIN, то из документации можно было бы сделать вывод, что для этого можно использовать Query.select_from():

q = (
    session.query(User)
    .select_from(select_stmt)
    .join(User, User.id == select_stmt.c.id)
    .filter(User.name == "ed")
)

Однако в версии 0.8 и более ранних версиях приведенное выше использование Query.select_from() приведет к тому, что сущность select_stmt будет замещать сущность User, поскольку она выбирает из таблицы user, которая совместима с User:

-- SQLAlchemy 0.8 and earlier...
SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name
FROM (SELECT "user".id AS id, "user".name AS name
FROM "user"
WHERE "user".id = :id_1) AS anon_1 JOIN "user" ON anon_1.id = anon_1.id
WHERE anon_1.name = :name_1

Приведенное выше утверждение является беспорядочным, предложение ON ссылается на anon_1.id = anon_1.id, наше предложение WHERE также заменено на anon_1.

Такое поведение вполне намеренно, но имеет иной сценарий использования, чем тот, который стал популярным для Query.select_from(). Описанное выше поведение теперь доступно с помощью нового метода, известного как Query.select_entity_from(). Это менее распространенное поведение, которое в современной SQLAlchemy примерно эквивалентно выбору из настроенной конструкции aliased():

select_stmt = select([User]).where(User.id == 7)
user_from_stmt = aliased(User, select_stmt.alias())

q = session.query(user_from_stmt).filter(user_from_stmt.name == "ed")

Таким образом, в SQLAlchemy 0.9 наш запрос, выбирающий из select_stmt, выдает тот SQL, который мы ожидаем:

-- SQLAlchemy 0.9
SELECT "user".id AS user_id, "user".name AS user_name
FROM (SELECT "user".id AS id, "user".name AS name
FROM "user"
WHERE "user".id = :id_1) AS anon_1 JOIN "user" ON "user".id = id
WHERE "user".name = :name_1

Метод Query.select_entity_from() будет доступен в SQLAlchemy 0.8.2, поэтому приложения, которые полагаются на старое поведение, могут сначала перейти на этот метод, убедиться, что все тесты продолжают работать, а затем без проблем перейти на версию 0.9.

#2736

viewonly=True на relationship() предотвращает вступление в силу истории

Флаг viewonly на relationship() применяется для того, чтобы изменения целевого атрибута не оказывали никакого влияния в процессе промывки. Это достигается за счет того, что атрибут не учитывается в процессе смывки. Однако до сих пор изменения атрибута могли регистрировать родительский объект как «грязный» и вызывать потенциальный flush. Изменение заключается в том, что флаг viewonly теперь предотвращает установку истории и для целевого атрибута. Такие события атрибута, как обратные ссылки и события, определяемые пользователем, продолжают функционировать нормально.

Это изменение иллюстрируется следующим образом:

from sqlalchemy import Column, Integer, ForeignKey, create_engine
from sqlalchemy.orm import backref, relationship, Session
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import inspect

Base = declarative_base()


class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)


class B(Base):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))
    a = relationship("A", backref=backref("bs", viewonly=True))


e = create_engine("sqlite://")
Base.metadata.create_all(e)

a = A()
b = B()

sess = Session(e)
sess.add_all([a, b])
sess.commit()

b.a = a

assert b in sess.dirty

# before 0.9.0
# assert a in sess.dirty
# assert inspect(a).attrs.bs.history.has_changes()

# after 0.9.0
assert a not in sess.dirty
assert not inspect(a).attrs.bs.history.has_changes()

#2833

Улучшения и исправления SQL-выражения Association Proxy

Операторы == и !=, реализованные для прокси ассоциации, ссылающейся на скалярное значение скалярного отношения, теперь дают более полное SQL-выражение, предназначенное для учета наличия или отсутствия строки «ассоциации» при сравнении с None.

Рассмотрим такое отображение:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

    b_id = Column(Integer, ForeignKey("b.id"), primary_key=True)
    b = relationship("B")
    b_value = association_proxy("b", "value")


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    value = Column(String)

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

s.query(A).filter(A.b_value == None).all()

будет производить:

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE EXISTS (SELECT 1
FROM b
WHERE b.id = a.b_id AND b.value IS NULL)

В версии 0.9 он теперь выдает:

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE (EXISTS (SELECT 1
FROM b
WHERE b.id = a.b_id AND b.value IS NULL)) OR a.b_id IS NULL

Разница заключается в том, что проверяется не только b.value, но и то, ссылается ли a вообще на какой-либо ряд b. Это даст отличные от предыдущих версий результаты для системы, использующей такой тип сравнения, когда некоторые родительские строки не имеют ассоциативной строки.

Что еще более важно, корректное выражение выдается для A.b_value != None. В версии 0.8 для строк A, в которых не было b, возвращалось True:

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE NOT (EXISTS (SELECT 1
FROM b
WHERE b.id = a.b_id AND b.value IS NULL))

Теперь, в версии 0.9, проверка переработана таким образом, что она гарантирует наличие строки A.b_id, а также то, что B.value не является NULL:

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE EXISTS (SELECT 1
FROM b
WHERE b.id = a.b_id AND b.value IS NOT NULL)

Кроме того, оператор has() усовершенствован таким образом, что его можно вызывать только против скалярного значения столбца без критерия, и он будет выдавать критерии, проверяющие наличие или отсутствие ассоциативной строки:

s.query(A).filter(A.b_value.has()).all()

выход:

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE EXISTS (SELECT 1
FROM b
WHERE b.id = a.b_id)

Это эквивалентно A.b.has(), но позволяет выполнять запросы к b_value напрямую.

#2751

Association Proxy Missing Scalar returns None

Прокси ассоциации скалярного атрибута со скалярным атрибутом теперь будет возвращать None, если проксируемый объект не существует. Это согласуется с тем фактом, что в SQLAlchemy отсутствующее значение many-to-one возвращает None, поэтому и проксируемое значение должно быть таким же. Например:

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy

Base = declarative_base()


class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    b = relationship("B", uselist=False)

    bname = association_proxy("b", "name")


class B(Base):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))
    name = Column(String)


a1 = A()

# this is how m2o's always have worked
assert a1.b is None

# but prior to 0.9, this would raise AttributeError,
# now returns None just like the proxied value.
assert a1.bname is None

#2810

attributes.get_history() по умолчанию будет запрашивать из БД, если значение не присутствует

Исправление ошибки get_history() позволяет атрибуту, основанному на колонке, запрашивать у базы данных невыгруженное значение, если флаг passive оставлен по умолчанию PASSIVE_OFF. Ранее этот флаг не выполнялся. Кроме того, в дополнение к атрибуту AttributeState.load_history() добавлен новый метод AttributeState.history, который будет выдавать вызываемые файлы загрузчика для незагруженного атрибута.

Это небольшое изменение демонстрируется следующим образом:

from sqlalchemy import Column, Integer, String, create_engine, inspect
from sqlalchemy.orm import Session, attributes
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    data = Column(String)


e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)

sess = Session(e)

a1 = A(data="a1")
sess.add(a1)
sess.commit()  # a1 is now expired

# history doesn't emit loader callables
assert inspect(a1).attrs.data.history == (None, None, None)

# in 0.8, this would fail to load the unloaded state.
assert attributes.get_history(a1, "data") == (
    (),
    [
        "a1",
    ],
    (),
)

# load_history() is now equivalent to get_history() with
# passive=PASSIVE_OFF ^ INIT_OK
assert inspect(a1).attrs.data.load_history() == (
    (),
    [
        "a1",
    ],
    (),
)

#2787

Поведенческие изменения - Основная часть

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

Вплоть до версии 0.8 большинство объектов типов принимали произвольные аргументы в виде ключевых слов, которые молчаливо игнорировались:

from sqlalchemy import Date, Integer

# storage_format argument here has no effect on any backend;
# it needs to be on the SQLite-specific type
d = Date(storage_format="%(day)02d.%(month)02d.%(year)04d")

# display_width argument here has no effect on any backend;
# it needs to be on the MySQL-specific type
i = Integer(display_width=5)

Это была очень старая ошибка, о которой в серии 0.8 было добавлено предупреждение об обесценивании, но поскольку никто никогда не запускает Python с флагом «-W», она в основном так и не была замечена:

$ python -W always::DeprecationWarning ~/dev/sqlalchemy/test.py
/Users/classic/dev/sqlalchemy/test.py:5: SADeprecationWarning: Passing arguments to
type object constructor <class 'sqlalchemy.types.Date'> is deprecated
  d = Date(storage_format="%(day)02d.%(month)02d.%(year)04d")
/Users/classic/dev/sqlalchemy/test.py:9: SADeprecationWarning: Passing arguments to
type object constructor <class 'sqlalchemy.types.Integer'> is deprecated
  i = Integer(display_width=5)

Начиная с версии 0.9 конструктор «catch all» удален из TypeEngine, и эти бессмысленные аргументы больше не принимаются.

Для использования диалектных аргументов, таких как storage_format и display_width, правильно использовать соответствующие диалектные типы:

from sqlalchemy.dialects.sqlite import DATE
from sqlalchemy.dialects.mysql import INTEGER

d = DATE(storage_format="%(day)02d.%(month)02d.%(year)04d")

i = INTEGER(display_width=5)

А как быть в случае, когда нам нужен еще и диалектно-агностический тип? Мы используем метод TypeEngine.with_variant():

from sqlalchemy import Date, Integer
from sqlalchemy.dialects.sqlite import DATE
from sqlalchemy.dialects.mysql import INTEGER

d = Date().with_variant(
    DATE(storage_format="%(day)02d.%(month)02d.%(year)04d"), "sqlite"
)

i = Integer().with_variant(INTEGER(display_width=5), "mysql")

TypeEngine.with_variant() не является новым, он был добавлен в SQLAlchemy 0.7.2. Поэтому код, работающий на версии 0.8, может быть исправлен для использования этого подхода и протестирован перед обновлением до версии 0.9.

None больше не может использоваться в качестве конструктора «частичного И»

None больше не может быть использован в качестве «подпорки» для формирования условия AND по частям. Этот паттерн не был документирован, хотя в некоторых внутренних документах SQLAlchemy он использовался:

condition = None

for cond in conditions:
    condition = condition & cond

if condition is not None:
    stmt = stmt.where(condition)

Приведенная выше последовательность, когда conditions непуста, в версии 0.9 будет выдавать SELECT .. WHERE <condition> AND NULL. При этом None больше не игнорируется неявно, а согласуется с тем, как интерпретируется None в других контекстах, кроме конъюнкции.

Правильный код для 0.8 и 0.9 должен выглядеть так:

from sqlalchemy.sql import and_

if conditions:
    stmt = stmt.where(and_(*conditions))

Другой вариант, который работает на всех бэкендах в 0.9, но в 0.8 работает только на бэкендах, поддерживающих булевы константы:

from sqlalchemy.sql import true

condition = true()

for cond in conditions:
    condition = cond & condition

stmt = stmt.where(condition)

В версии 0.8 это приведет к тому, что оператор SELECT всегда будет содержать AND true в предложении WHERE, что не принимается бэкендами, не поддерживающими булевы константы (MySQL, MSSQL). В версии 0.9 константа true будет опускаться внутри конъюнкции and_().

Часть «пароль» create_engine() больше не рассматривает знак + как закодированный пробел

По какой-то причине к полю «пароль» URL была применена функция Python unquote_plus(), которая некорректно применяла правила кодирования, описанные в RFC 1738, поскольку экранировала пробелы как знаки плюс. Теперь строчная кодировка URL кодирует только «:», «@» или «/» и ничего больше, и применяется как к полю username, так и к полю password (ранее она применялась только к паролю). При разборе кодированные символы преобразуются, но знаки плюс и пробелы передаются как есть:

# password: "pass word + other:words"
dbtype://user:pass word + other%3Awords@host/dbname

# password: "apples/oranges"
dbtype://username:apples%2Foranges@hostspec/database

# password: "apples@oranges@@"
dbtype://username:apples%40oranges%40%40@hostspec/database

# password: '', username is "username@"
dbtype://username%40:@hostspec/database

#2873

Правила прецедента для COLLATE были изменены

Ранее использовалось выражение типа:

print((column("x") == "somevalue").collate("en_EN"))

даст выражение, подобное этому:

-- 0.8 behavior
(x = :x_1) COLLATE en_EN

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

-- 0.9 behavior
x = :x_1 COLLATE en_EN

Потенциально обратное несовместимое изменение возникает, если оператор ColumnOperators.collate() применяется к правому столбцу, как показано ниже:

print(column("x") == literal("somevalue").collate("en_EN"))

В версии 0.8 это дает результат:

x = :param_1 COLLATE en_EN

Однако в 0.9 теперь будет выдаваться более точная, но, возможно, не совсем то, что вам нужно, форма:

x = (:param_1 COLLATE en_EN)

Оператор ColumnOperators.collate() теперь более корректно работает и внутри выражения ORDER BY, так как операторам ASC и DESC присвоен особый приоритет, который вновь гарантирует отсутствие круглых скобок:

>>> # 0.8
>>> print(column("x").collate("en_EN").desc())
{printsql}(x COLLATE en_EN) DESC{stop}

>>> # 0.9
>>> print(column("x").collate("en_EN").desc())
{printsql}x COLLATE en_EN DESC{stop}

#2879

PostgreSQL CREATE TYPE <x> AS ENUM теперь применяет кавычки к значениям

Тип ENUM теперь будет применять экранирование к знакам одинарной кавычки внутри перечисляемых значений:

>>> from sqlalchemy.dialects import postgresql
>>> type = postgresql.ENUM("one", "two", "three's", name="myenum")
>>> from sqlalchemy.dialects.postgresql import base
>>> print(base.CreateEnumType(type).compile(dialect=postgresql.dialect()))
{printsql}CREATE TYPE myenum AS ENUM ('one','two','three''s')

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

#2878

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

API удаления событий

События, установленные с помощью listen() или listens_for(), теперь можно удалить с помощью новой функции remove(). Аргументы target, identifier и fn, передаваемые в remove(), должны точно совпадать с теми, которые были переданы для прослушивания, и событие будет удалено из всех мест, в которых оно было установлено:

@event.listens_for(MyClass, "before_insert", propagate=True)
def my_before_insert(mapper, connection, target):
    """listen for before_insert"""
    # ...


event.remove(MyClass, "before_insert", my_before_insert)

В приведенном примере установлен флаг propagate=True. Это означает, что my_before_insert() устанавливается в качестве слушателя для MyClass, а также для всех подклассов MyClass. Система отслеживает везде, где в результате этого вызова была размещена функция-слушатель my_before_insert(), и удаляет ее в результате вызова remove().

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

#2268

Новый API опций запросов; load_only() опция

Система опций загрузчика, таких как joinedload(), subqueryload(), lazyload(), defer() и т.д., основана на новой системе, известной как Load. Load обеспечивает «цепочку методов» (она же generative) к опциям загрузчика, так что вместо объединения длинных путей с помощью точек или нескольких имен атрибутов, для каждого пути задается явный стиль загрузчика.

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

Старый путь.

Чтобы задать определенный стиль загрузки по каждому звену многоэлементного пути, необходимо использовать опцию _all():

query(User).options(joinedload_all("orders.items.keywords"))

Новый способ.

Опции загрузчика теперь можно передавать по цепочке, поэтому один и тот же метод joinedload(x) применяется одинаково к каждому звену, без необходимости соблюдать прямую связь между joinedload() и joinedload_all():

query(User).options(joinedload("orders").joinedload("items").joinedload("keywords"))

Старый путь.

Установка опции на путь, основанный на подклассе, требует, чтобы все ссылки в пути были прописаны как атрибуты, связанные с классом, поскольку необходимо вызвать метод PropComparator.of_type():

session.query(Company).options(
    subqueryload_all(Company.employees.of_type(Engineer), Engineer.machines)
)

Новый способ.

Только те элементы пути, для которых действительно требуется PropComparator.of_type(), должны быть заданы как атрибут, привязанный к классу, строковые имена могут быть восстановлены после этого:

session.query(Company).options(
    subqueryload(Company.employees.of_type(Engineer)).subqueryload("machines")
)

Старый путь.

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

query(User).options(subqueryload("orders.items.keywords"))

Новый способ.

Теперь путь может быть прописан с использованием defaultload() для записей в пути, где существующий стиль загрузчика должен быть неизменным. Более многословно, но смысл понятнее:

query(User).options(defaultload("orders").defaultload("items").subqueryload("keywords"))

Точечный стиль все же может быть использован, особенно в случае пропуска нескольких элементов пути:

query(User).options(defaultload("orders.items").subqueryload("keywords"))

Старый путь.

Опция defer() в пути должна была быть прописана с указанием полного пути для каждого столбца:

query(User).options(defer("orders.description"), defer("orders.isopen"))

Новый способ.

Один объект Load, попавший на целевой путь, может иметь многократный вызов Load.defer():

query(User).options(defaultload("orders").defer("description").defer("isopen"))

Класс нагрузки

Класс Load может быть использован непосредственно для обеспечения «связанной» цели, особенно при наличии нескольких родительских сущностей:

from sqlalchemy.orm import Load

query(User, Address).options(Load(Address).joinedload("entries"))

Только нагрузка

Новая опция load_only() позволяет добиться загрузки в стиле «отложить все, кроме», загружая только заданные столбцы и откладывая остальные:

from sqlalchemy.orm import load_only

query(User).options(load_only("name", "fullname"))

# specify explicit parent entity
query(User, Address).options(Load(User).load_only("name", "fullname"))

# specify path
query(User).options(joinedload(User.addresses).load_only("email_address"))

Подстановочные знаки, специфичные для конкретного класса

С помощью символа Load можно установить загрузку для всех отношений (или, возможно, столбцов) данной сущности, не затрагивая другие:

# lazyload all User relationships
query(User).options(Load(User).lazyload("*"))

# undefer all User columns
query(User).options(Load(User).undefer("*"))

# lazyload all Address relationships
query(User).options(defaultload(User.addresses).lazyload("*"))

# undefer all Address columns
query(User).options(defaultload(User.addresses).undefer("*"))

#1418

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

Конструкция text() обзавелась новыми методами:

  • TextClause.bindparams() позволяет гибко задавать типы и значения связанных параметров:

    # setup values
    stmt = text(
        "SELECT id, name FROM user WHERE name=:name AND timestamp=:timestamp"
    ).bindparams(name="ed", timestamp=datetime(2012, 11, 10, 15, 12, 35))
    
    # setup types and/or values
    stmt = (
        text("SELECT id, name FROM user WHERE name=:name AND timestamp=:timestamp")
        .bindparams(bindparam("name", value="ed"), bindparam("timestamp", type_=DateTime()))
        .bindparam(timestamp=datetime(2012, 11, 10, 15, 12, 35))
    )
  • TextClause.columns() заменяет вариант typemap из text(), возвращая новую конструкцию TextAsFrom:

    # turn a text() into an alias(), with a .c. collection:
    stmt = text("SELECT id, name FROM user").columns(id=Integer, name=String)
    stmt = stmt.alias()
    
    stmt = select([addresses]).select_from(
        addresses.join(stmt), addresses.c.user_id == stmt.c.id
    )
    
    
    # or into a cte():
    stmt = text("SELECT id, name FROM user").columns(id=Integer, name=String)
    stmt = stmt.cte("x")
    
    stmt = select([addresses]).select_from(
        addresses.join(stmt), addresses.c.user_id == stmt.c.id
    )

#2877

INSERT из SELECT

После буквально нескольких лет бессмысленных проволочек была добавлена эта относительно небольшая синтаксическая возможность, которая также перенесена в 0.8.3, так что технически она не является «новой» в 0.9. Конструкция select() или другая совместимая конструкция может быть передана в новый метод Insert.from_select(), где она будет использована для рендеринга конструкции INSERT .. SELECT:

>>> from sqlalchemy.sql import table, column
>>> t1 = table("t1", column("a"), column("b"))
>>> t2 = table("t2", column("x"), column("y"))
>>> print(t1.insert().from_select(["a", "b"], t2.select().where(t2.c.y == 5)))
{printsql}INSERT INTO t1 (a, b) SELECT t2.x, t2.y
FROM t2
WHERE t2.y = :y_1

Конструкция достаточно умна, чтобы также учитывать объекты ORM, такие как классы и объекты Query:

s = Session()
q = s.query(User.id, User.name).filter_by(name="ed")
ins = insert(Address).from_select((Address.id, Address.email_address), q)

оказание услуг:

INSERT INTO addresses (id, email_address)
SELECT users.id AS users_id, users.name AS users_name
FROM users WHERE users.name = :name_1

#722

Новая поддержка FOR UPDATE на select(), Query()

Сделана попытка упростить спецификацию предложения FOR UPDATE в операторах SELECT, выполняемых в Core и ORM, а также добавлена поддержка FOR UPDATE OF SQL, поддерживаемого PostgreSQL и Oracle.

Используя ядро GenerativeSelect.with_for_update(), опции типа FOR SHARE и NOWAIT можно указывать по отдельности, а не связывать с произвольными кодами строк:

stmt = select([table]).with_for_update(read=True, nowait=True, of=table)

На языке Posgtresql приведенное выше утверждение может иметь вид:

SELECT table.a, table.b FROM table FOR SHARE OF table NOWAIT

Объект Query обзавелся аналогичным методом Query.with_for_update(), который ведет себя так же. Этот метод заменяет существующий метод Query.with_lockmode(), который транслировал клаузулы FOR UPDATE по другой системе. В настоящее время строковый аргумент «lockmode» по-прежнему принимается методом Session.refresh().

Точность преобразования строк с плавающей запятой настраивается для нативных типов с плавающей запятой

Преобразование, которое SQLAlchemy выполняет каждый раз, когда DBAPI возвращает тип с плавающей точкой Python, который должен быть преобразован в Python Decimal(), обязательно включает промежуточный шаг, который преобразует значение с плавающей точкой в строку. Масштаб, используемый для этого преобразования строки, ранее был жестко закодирован в 10, теперь же он настраивается. Настройка доступна как для типа Numeric, так и для типа Float, а также для всех потомков типа, специфичных для SQL и диалектов, с помощью параметра decimal_return_scale. Если тип поддерживает параметр .scale, как в случае с Numeric и некоторыми типами float, такими как DOUBLE, то значение .scale используется по умолчанию для .decimal_return_scale, если оно не указано иначе. Если и .scale, и .decimal_return_scale отсутствуют, то по умолчанию используется значение 10. Например:

from sqlalchemy.dialects.mysql import DOUBLE
import decimal

data = Table(
    "data",
    metadata,
    Column("double_value", mysql.DOUBLE(decimal_return_scale=12, asdecimal=True)),
)

conn.execute(
    data.insert(),
    double_value=45.768392065789,
)
result = conn.scalar(select([data.c.double_value]))

# previously, this would typically be Decimal("45.7683920658"),
# e.g. trimmed to 10 decimal places

# now we get 12, as requested, as MySQL can support this
# much precision for DOUBLE
assert result == decimal.Decimal("45.768392065789")

#2867

Пакеты столбцов для ORM-запросов

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

#2824

Подсчет версий на стороне сервера

Функция версионирования в ORM (теперь она также документирована по адресу Настройка счетчика версий) теперь может использовать как серверные схемы подсчета версий, например, создаваемые триггерами или столбцами системы БД, так и условные программные схемы, не входящие в саму функцию version_id_counter. При указании значения False в параметре version_id_generator ORM будет использовать уже установленный идентификатор версии или, в качестве альтернативы, извлекать идентификатор версии из каждой строки в момент выполнения INSERT или UPDATE. При использовании генерируемого сервером идентификатора версии настоятельно рекомендуется применять эту возможность только на бэкенде с сильной поддержкой RETURNING (PostgreSQL, SQL Server; Oracle также поддерживает RETURNING, но драйвер cx_oracle имеет лишь ограниченную поддержку), иначе дополнительные операторы SELECT будут значительно увеличивать производительность. Пример, приведенный в Счетчики версий на стороне сервера, иллюстрирует использование системного столбца PostgreSQL xmin для интеграции его с функцией версионности ORM.

#2793

include_backrefs=False опция для @validates

Функция validates() теперь принимает опцию include_backrefs=True, которая позволяет обойти срабатывание валидатора для случая, когда событие инициировано из backref:

from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship, validates
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = "a"

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

    @validates("bs")
    def validate_bs(self, key, item):
        print("A.bs validator")
        return item


class B(Base):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))

    @validates("a", include_backrefs=False)
    def validate_a(self, key, item):
        print("B.a validator")
        return item


a1 = A()
a1.bs.append(B())  # prints only "A.bs validator"

#1535

PostgreSQL JSON Type

В диалекте PostgreSQL появился тип JSON, дополняющий тип HSTORE.

См.также

JSON

#2581

Расширение Automap

В 0.9.1 добавлено новое расширение sqlalchemy.ext.automap. Это экспериментальное расширение, которое расширяет функциональность Declarative, а также класса DeferredReflection. По сути, расширение предоставляет базовый класс AutomapBase, который автоматически генерирует сопоставленные классы и отношения между ними на основе заданных метаданных таблицы.

Используемый MetaData обычно может быть получен с помощью рефлексии, но нет никаких требований, чтобы рефлексия использовалась. Самый простой вариант использования иллюстрирует, как sqlalchemy.ext.automap может предоставлять отображенные классы, включая отношения, на основе отраженной схемы:

from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
from sqlalchemy import create_engine

Base = automap_base()

# engine, suppose it has two tables 'user' and 'address' set up
engine = create_engine("sqlite:///mydatabase.db")

# reflect the tables
Base.prepare(engine, reflect=True)

# mapped classes are now created with names matching that of the table
# name.
User = Base.classes.user
Address = Base.classes.address

session = Session(engine)

# rudimentary relationships are produced
session.add(Address(email_address="foo@bar.com", user=User(name="foo")))
session.commit()

# collection-based relationships are by default named "<classname>_collection"
print(u1.address_collection)

Кроме того, класс AutomapBase является декларативной базой и поддерживает все возможности декларативной базы. Функция «automapping» может быть использована с существующей, явно объявленной схемой только для генерации отношений и недостающих классов. Схемы именования и процедуры создания отношений могут быть добавлены с помощью вызываемых функций.

Предполагается, что система AutomapBase обеспечивает быстрое и модернизированное решение проблемы, которую пытается решить и известная SQLSoup, - быструю и рудиментарную генерацию объектной модели из существующей базы данных «на лету». Решая проблему строго на уровне конфигурации маппера и полностью интегрируясь с существующими технологиями декларативных классов, AutomapBase стремится обеспечить хорошо интегрированный подход к проблеме оперативной автогенерации специальных маппингов.

См.также

Automap

Поведенческие улучшения

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

Многие выражения JOIN и LEFT OUTER JOIN больше не будут обернуты в (SELECT * FROM …) AS ANON_1

В течение многих лет в SQLAlchemy ORM не было возможности вложить JOIN в правую часть существующего JOIN (обычно LEFT OUTER JOIN, так как INNER JOIN всегда можно было сгладить):

SELECT a.*, b.*, c.* FROM a LEFT OUTER JOIN (b JOIN c ON b.id = c.id) ON a.id

Это было связано с тем, что SQLite до версии 3.7.16 не может разобрать утверждение приведенного формата:

SQLite version 3.7.15.2 2013-01-09 11:53:05
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> create table a(id integer);
sqlite> create table b(id integer);
sqlite> create table c(id integer);
sqlite> select a.id, b.id, c.id from a left outer join (b join c on b.id=c.id) on b.id=a.id;
Error: no such column: b.id

Другой способ обойти правосторонние скобки - это, конечно, правосторонние соединения; реализация этого способа была бы значительно сложнее и визуально неприятнее, но, к счастью, SQLite не поддерживает RIGHT OUTER JOIN :):

sqlite> select a.id, b.id, c.id from b join c on b.id=c.id
   ...> right outer join a on b.id=a.id;
Error: RIGHT and FULL OUTER JOINs are not currently supported

Еще в 2005 году было неясно, есть ли проблемы с этой формой у других баз данных, но сегодня, похоже, все протестированные базы данных, кроме SQLite, ее поддерживают (Oracle 8, очень старая база данных, вообще не поддерживает ключевое слово JOIN, но SQLAlchemy всегда имела простую схему переписывания для синтаксиса Oracle). Еще хуже то, что обычный обходной путь SQLAlchemy - применение SELECT - часто снижает производительность на таких платформах, как PostgreSQL и MySQL:

SELECT a.*, anon_1.* FROM a LEFT OUTER JOIN (
                SELECT b.id AS b_id, c.id AS c_id
                FROM b JOIN c ON b.id = c.id
            ) AS anon_1 ON a.id=anon_1.b_id

JOIN, подобный приведенному выше, часто встречается при работе со структурами наследования объединенных таблиц; при любом использовании Query.join() для соединения от некоторого родителя к подклассу объединенной таблицы или при аналогичном использовании joinedload() ORM SQLAlchemy всегда следит за тем, чтобы вложенный JOIN не был выведен, чтобы запрос не мог выполняться на SQLite. Несмотря на то, что ядро всегда поддерживало JOIN в более компактной форме, ORM приходилось избегать этого.

Дополнительная проблема может возникнуть при создании объединений по отношениям «многие-ко-многим», когда в предложении ON присутствуют специальные критерии. Рассмотрим соединение с нетерпеливой загрузкой, например, следующее:

session.query(Order).outerjoin(Order.items)

Если предположить, что множество от Order до Item, которое на самом деле ссылается на подкласс, например Subitem, то SQL для этого будет выглядеть следующим образом:

SELECT order.id, order.name
FROM order LEFT OUTER JOIN order_item ON order.id = order_item.order_id
LEFT OUTER JOIN item ON order_item.item_id = item.id AND item.type = 'subitem'

Что не так с приведенным выше запросом? В основном то, что он загрузит много строк order / order_item, в которых критерий item.type == 'subitem' не является истинным.

Начиная с версии SQLAlchemy 0.9, применяется совершенно новый подход. ORM больше не беспокоится о вложенности JOIN в правую часть вложенного JOIN, и теперь будет выводить их как можно чаще, возвращая при этом корректные результаты. При передаче SQL-запроса на компиляцию компилятор диалекта будет переписывать join в соответствии с целевым бэкендом, если известно, что этот бэкенд не поддерживает вложенный справа JOIN (в настоящее время это только SQLite - если у других бэкендов есть такая проблема, пожалуйста, сообщите нам!)

Поэтому регулярка query(Parent).join(Subclass) теперь обычно дает более простое выражение:

SELECT parent.id AS parent_id
FROM parent JOIN (
        base_table JOIN subclass_table
        ON base_table.id = subclass_table.id) ON parent.id = base_table.parent_id

При объединенных ускоренных загрузках типа query(Parent).options(joinedload(Parent.subclasses)) вместо обертывания в ANON_1 будут псевдонимы отдельных таблиц:

SELECT parent.*, base_table_1.*, subclass_table_1.* FROM parent
    LEFT OUTER JOIN (
        base_table AS base_table_1 JOIN subclass_table AS subclass_table_1
        ON base_table_1.id = subclass_table_1.id)
        ON parent.id = base_table_1.parent_id

Объединения типа «многие-ко-многим» и eagerloads правильно вложат «вторичную» и «правую» таблицы:

SELECT order.id, order.name
FROM order LEFT OUTER JOIN
(order_item JOIN item ON order_item.item_id = item.id AND item.type = 'subitem')
ON order_item.order_id = order.id

Все эти объединения, если они представлены с помощью оператора Select, в котором специально указано use_labels=True, что справедливо для всех запросов, выдаваемых ORM, являются кандидатами на «переписывание объединений», что представляет собой процесс переписывания всех этих право-вложенных объединений во вложенные операторы SELECT, сохраняя при этом идентичную маркировку, используемую в Select. Поэтому SQLite, единственная база данных, которая даже в 2013 году не будет поддерживать этот очень распространенный синтаксис SQL, сама взвалила на себя дополнительные сложности, переписав приведенные выше запросы в виде:

-- sqlite only!
SELECT parent.id AS parent_id
    FROM parent JOIN (
        SELECT base_table.id AS base_table_id,
                base_table.parent_id AS base_table_parent_id,
                subclass_table.id AS subclass_table_id
        FROM base_table JOIN subclass_table ON base_table.id = subclass_table.id
    ) AS anon_1 ON parent.id = anon_1.base_table_parent_id

-- sqlite only!
SELECT parent.id AS parent_id, anon_1.subclass_table_1_id AS subclass_table_1_id,
        anon_1.base_table_1_id AS base_table_1_id,
        anon_1.base_table_1_parent_id AS base_table_1_parent_id
FROM parent LEFT OUTER JOIN (
    SELECT base_table_1.id AS base_table_1_id,
        base_table_1.parent_id AS base_table_1_parent_id,
        subclass_table_1.id AS subclass_table_1_id
    FROM base_table AS base_table_1
    JOIN subclass_table AS subclass_table_1 ON base_table_1.id = subclass_table_1.id
) AS anon_1 ON parent.id = anon_1.base_table_1_parent_id

-- sqlite only!
SELECT "order".id AS order_id
FROM "order" LEFT OUTER JOIN (
        SELECT order_item_1.order_id AS order_item_1_order_id,
            order_item_1.item_id AS order_item_1_item_id,
            item.id AS item_id, item.type AS item_type
FROM order_item AS order_item_1
    JOIN item ON item.id = order_item_1.item_id AND item.type IN (?)
) AS anon_1 ON "order".id = anon_1.order_item_1_order_id

Примечание

Начиная с SQLAlchemy 1.1, обходные пути, присутствующие в данной функции для SQLite, автоматически отключаются при обнаружении SQLite версии 3.7.16 или выше, поскольку в SQLite восстановлена поддержка право-вложенных объединений.

Функции Join.alias(), aliased() и with_polymorphic() теперь поддерживают новый аргумент flat=True, который используется для построения псевдонимов присоединяемых табличных сущностей без встраивания в SELECT. По умолчанию этот флаг не включен, чтобы обеспечить обратную совместимость - но теперь «полиморфный» selectable может быть присоединен в качестве цели без создания подзапросов:

employee_alias = with_polymorphic(Person, [Engineer, Manager], flat=True)

session.query(Company).join(Company.employees.of_type(employee_alias)).filter(
    or_(Engineer.primary_language == "python", Manager.manager_name == "dilbert")
)

Генерирует (везде, кроме SQLite):

SELECT companies.company_id AS companies_company_id, companies.name AS companies_name
FROM companies JOIN (
    people AS people_1
    LEFT OUTER JOIN engineers AS engineers_1 ON people_1.person_id = engineers_1.person_id
    LEFT OUTER JOIN managers AS managers_1 ON people_1.person_id = managers_1.person_id
) ON companies.company_id = people_1.company_id
WHERE engineers.primary_language = %(primary_language_1)s
    OR managers.manager_name = %(manager_name_1)s

#2369 #2587

Правые вложенные внутренние объединения доступны в объединенных ускоренных загрузках

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

Обычно объединенная цепочка eager load выглядит следующим образом:

query(User).options(
    joinedload("orders", innerjoin=False).joinedload("items", innerjoin=True)
)

Не получалось внутреннего соединения; из-за LEFT OUTER JOIN из user->order, объединенная загрузка не могла использовать INNER join из order->items без изменения возвращаемых пользовательских строк, и вместо этого игнорировала директиву «chained» innerjoin=True. В версии 0.9.0 это должно было быть реализовано следующим образом:

FROM users LEFT OUTER JOIN orders ON <onclause> LEFT OUTER JOIN items ON <onclause>

сработает новая логика «вложенные справа соединения - это нормально», и мы получим:

FROM users LEFT OUTER JOIN (orders JOIN items ON <onclause>) ON <onclause>

Поскольку мы упустили этот момент, то во избежание дальнейших регрессий мы добавили вышеуказанную функциональность, указав строку "nested" в joinedload.innerjoin:

query(User).options(
    joinedload("orders", innerjoin=False).joinedload("items", innerjoin="nested")
)

Эта возможность появилась в версии 0.9.4.

#2976

ORM может эффективно получать только что сгенерированные значения по умолчанию INSERT/UPDATE, используя RETURNING

Уже давно в Mapper поддерживается недокументированный флаг, известный как eager_defaults=True. Действие этого флага заключается в том, что когда выполняется INSERT или UPDATE, и известно, что строка имеет сгенерированные сервером значения по умолчанию, за ней немедленно следует SELECT, чтобы «с нетерпением» загрузить эти новые значения. Обычно сгенерированные сервером столбцы помечаются в объекте как «просроченные», так что никаких накладных расходов не возникает, если только приложение не обращается к этим столбцам вскоре после сброса. Поэтому флаг eager_defaults не имеет особого смысла, так как он может только снизить производительность, и присутствует только для поддержки экзотических схем событий, когда пользователям требуется, чтобы значения по умолчанию были доступны сразу же в процессе flush.

В 0.9, в результате усовершенствования идентификатора версии, eager_defaults теперь может выдавать предложение RETURNING для этих значений, поэтому на бэкенде с сильной поддержкой RETURNING, в частности, в PostgreSQL, ORM может получать новые значения по умолчанию и SQL-выражения в строке с INSERT или UPDATE. eager_defaults, если включено, использует RETURNING автоматически, если целевой бэкэнд и Table поддерживают «неявное возвращение».

Подзапрос Eager Loading будет применять DISTINCT к самому внутреннему SELECT для некоторых запросов

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

То есть при загрузке подзапроса по принципу «многие-к-одному» из A->B:

SELECT b.id AS b_id, b.name AS b_name, anon_1.b_id AS a_b_id
FROM (SELECT DISTINCT a_b_id FROM a) AS anon_1
JOIN b ON b.id = anon_1.a_b_id

Поскольку a.b_id является нечетким внешним ключом, то для устранения избыточных a.b_id применяется DISTINCT. Это поведение может быть включено или выключено безусловно для конкретного relationship() с помощью флага distinct_target_key, устанавливающего значение True для безусловного включения, False для безусловного выключения и None для того, чтобы функция действовала, когда целевой SELECT выполняется против столбцов, не составляющих полный первичный ключ. В версии 0.9 по умолчанию используется значение None.

Эта опция также перенесена в 0.8, где опция distinct_target_key по умолчанию принимает значение False.

Хотя данная функция призвана повысить производительность за счет устранения дублирующихся строк, ключевое слово DISTINCT в SQL само по себе может оказывать негативное влияние на производительность. Если столбцы в SELECT не индексированы, то DISTINCT, скорее всего, выполнит ORDER BY для набора строк, что может быть дорогостоящим. Если ограничить эту возможность только внешними ключами, которые, как мы надеемся, в любом случае индексируются, то можно ожидать, что новые значения по умолчанию будут вполне разумными.

Кроме того, эта функция не устраняет все возможные сценарии дублирования строк: если в цепочке присоединений присутствует many-to-one, то дублирование строк все равно может иметь место.

#2836

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

Изменен механизм передачи «инициатора» событий атрибутов, т.е. объекта, связанного с началом события; вместо AttributeImpl передается новый объект Event, который ссылается на AttributeImpl, а также на «маркер операции», показывающий, является ли данная операция добавлением, удалением или заменой.

Система событий атрибутов больше не обращается к этому объекту-«инициатору» для того, чтобы остановить рекурсивную серию событий атрибутов. Вместо этого система предотвращения бесконечной рекурсии из-за взаимозависимых обработчиков обратных ссылок была перенесена на обработчики событий обратных ссылок ORM, которые теперь берут на себя роль гаранта того, что цепочка взаимозависимых событий (например, append to collection A.bs, set many-to-one attribute B.a in response) не перейдет в бесконечный рекурсивный поток. Смысл этого заключается в том, что система backref, получив больше деталей и контроля над распространением событий, может, наконец, разрешить выполнение операций более чем на один уровень вглубь; типичным сценарием является ситуация, когда добавление коллекции приводит к операции замены «многие на одного», которая, в свою очередь, должна привести к удалению элемента из предыдущей коллекции:

class Parent(Base):
    __tablename__ = "parent"

    id = Column(Integer, primary_key=True)
    children = relationship("Child", backref="parent")


class Child(Base):
    __tablename__ = "child"

    id = Column(Integer, primary_key=True)
    parent_id = Column(ForeignKey("parent.id"))


p1 = Parent()
p2 = Parent()
c1 = Child()

p1.children.append(c1)

assert c1.parent is p1  # backref event establishes c1.parent as p1

p2.children.append(c1)

assert c1.parent is p2  # backref event establishes c1.parent as p2
assert c1 not in p1.children  # second backref event removes c1 from p1.children

Выше, до этого изменения, объект c1 по-прежнему присутствовал в p1.children, даже если он одновременно присутствовал и в p2.children; обработчики обратных ссылок остановились бы на замене c1.parent на p2 вместо p1. В 0.9, используя более подробный объект Event, а также позволяя обработчикам обратных ссылок принимать более подробные решения относительно этих объектов, распространение может быть продолжено на удаление c1 из p1.children, сохраняя при этом проверку на то, что распространение не перейдет в бесконечный рекурсивный цикл.

Код конечного пользователя, который a. использует события AttributeEvents.set(), AttributeEvents.append() или AttributeEvents.remove() и b. инициирует дальнейшие операции модификации атрибутов в результате этих событий, возможно, придется модифицировать для предотвращения рекурсивных циклов, поскольку система атрибутов больше не препятствует бесконечному распространению цепочки событий в отсутствие обработчиков событий backref. Кроме того, код, зависящий от значения initiator, должен быть адаптирован к новому API, а также должен быть готов к тому, что значение initiator может измениться по сравнению с исходным значением в строке событий, инициированных backref, поскольку обработчики backref теперь могут подменять значение initiator для некоторых операций.

#2789

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

В TypeEngine TypeEngine.literal_processor(), а также в TypeDecorator.process_literal_param() для TypeDecorator добавляется новый метод, который берет на себя задачу визуализации так называемых «inline literal parameters» - параметров, которые обычно визуализируются как «связанные» значения, но вместо этого в силу конфигурации компилятора выводятся inline в SQL-оператор. Эта возможность используется при генерации DDL для таких конструкций, как CheckConstraint, а также в Alembic при использовании таких конструкций, как op.inline_literal(). Ранее простая проверка «isinstance» проверяла наличие нескольких основных типов, а «bind processor» использовался безусловно, что приводило к таким проблемам, как преждевременное кодирование строк в utf-8.

Пользовательские типы, написанные с использованием TypeDecorator, должны продолжать работать в сценариях «inline literal», поскольку TypeDecorator.process_literal_param() по умолчанию возвращается к TypeDecorator.process_bind_param(), так как эти методы обычно обрабатывают манипуляции с данными, а не то, как эти данные представляются базе данных. TypeDecorator.process_literal_param() может быть задан для получения строки, представляющей, как значение должно быть представлено в операторе DDL.

#2838

Идентификаторы схем теперь содержат собственную информацию о цитировании

Это изменение упрощает использование в ядре так называемых «кавычечных» флагов, таких как флаг quote, передаваемый в Table и Column. Теперь флаг интериоризируется в само строковое имя, которое теперь представляется как экземпляр quoted_name, подкласса строк. Теперь IdentifierPreparer в большинстве случаев полагается исключительно на предпочтения кавычек, сообщаемые объектом quoted_name, а не проверяет наличие явных флагов quote. Решение проблемы заключается в том, что различные чувствительные к регистру методы, такие как Engine.has_table(), а также аналогичные методы в диалектах теперь работают с явно кавычками, без необходимости усложнять или вносить обратно несовместимые изменения в эти API (многие из которых являются сторонними) с деталями флагов кавычек - в частности, более широкий диапазон идентификаторов теперь корректно работает с так называемыми «прописными» бэкендами, такими как Oracle, Firebird и DB2 (бэкенды, которые хранят и сообщают имена таблиц и столбцов, используя все прописные буквы для имен, не чувствительных к регистру).

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

#2812

Улучшено отображение булевых констант, констант NULL, конъюнкции

Новые возможности добавлены к константам true() и false(), в частности, в сочетании с функциями and_() и or_(), а также к поведению предложений WHERE/HAVING в сочетании с этими типами, булевыми типами в целом и константой null().

Начнем с таблицы следующего вида:

from sqlalchemy import Table, Boolean, Integer, Column, MetaData

t1 = Table("t", MetaData(), Column("x", Boolean()), Column("y", Integer))

Конструкция select теперь будет отображать булевский столбец как двоичное выражение на бэкендах, не поддерживающих поведение констант true/false:

>>> from sqlalchemy import select, and_, false, true
>>> from sqlalchemy.dialects import mysql, postgresql

>>> print(select([t1]).where(t1.c.x).compile(dialect=mysql.dialect()))
{printsql}SELECT t.x, t.y  FROM t WHERE t.x = 1

Конструкции and_() и or_() теперь будут проявлять квази «короткое замыкание», то есть усекать отображаемое выражение при наличии константы true() или false():

>>> print(
...     select([t1]).where(and_(t1.c.y > 5, false())).compile(dialect=postgresql.dialect())
... )
{printsql}SELECT t.x, t.y FROM t WHERE false

true() можно использовать в качестве основы для построения выражения:

>>> expr = true()
>>> expr = expr & (t1.c.y > 5)
>>> print(select([t1]).where(expr))
{printsql}SELECT t.x, t.y FROM t WHERE t.y > :y_1

Булевы константы true() и false() сами по себе отображаются как 0 = 1 и 1 = 1 для бэкенда, не имеющего булевых констант:

>>> print(select([t1]).where(and_(t1.c.y > 5, false())).compile(dialect=mysql.dialect()))
{printsql}SELECT t.x, t.y FROM t WHERE 0 = 1

Интерпретация None, хотя и не является особо валидным SQL, по крайней мере, теперь последовательна:

>>> print(select([t1.c.x]).where(None))
{printsql}SELECT t.x FROM t WHERE NULL{stop}

>>> print(select([t1.c.x]).where(None).where(None))
{printsql}SELECT t.x FROM t WHERE NULL AND NULL{stop}

>>> print(select([t1.c.x]).where(and_(None, None)))
{printsql}SELECT t.x FROM t WHERE NULL AND NULL{stop}

#2804

Конструкции меток теперь могут отображаться только в виде своего имени в ORDER BY

В случае, когда метка Label используется как в предложении columns, так и в предложении ORDER BY в SELECT, в предложении ORDER BY метка будет отображаться как просто ее имя, если базовый диалект поддерживает эту возможность.

Например, пример:

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

t = table("t", column("c1"), column("c2"))
expr = (func.foo(t.c.c1) + t.c.c2).label("expr")

stmt = select([expr]).order_by(expr)

print(stmt)

До версии 0.9 отображалось как:

SELECT foo(t.c1) + t.c2 AS expr
FROM t ORDER BY foo(t.c1) + t.c2

И теперь переводится как:

SELECT foo(t.c1) + t.c2 AS expr
FROM t ORDER BY expr

ORDER BY отображает метку только в том случае, если она не встроена в выражение внутри ORDER BY, кроме простого ASC или DESC.

Приведенный формат работает на всех протестированных базах данных, но может иметь проблемы совместимости с более старыми версиями баз данных (MySQL 4? Oracle 8? и т.д.). На основе сообщений пользователей мы можем добавить правила, которые будут отключать эту функцию на основе определения версии базы данных.

#1068

RowProxy теперь имеет поведение сортировки по кортежам

Объект RowProxy действует подобно кортежу, но до сих пор не сортировался как кортеж, если их список был отсортирован с помощью sorted(). Теперь метод __eq__() сравнивает обе стороны как кортеж, а также добавлен метод __lt__():

users.insert().execute(
    dict(user_id=1, user_name="foo"),
    dict(user_id=2, user_name="bar"),
    dict(user_id=3, user_name="def"),
)

rows = users.select().order_by(users.c.user_name).execute().fetchall()

eq_(rows, [(2, "bar"), (3, "def"), (1, "foo")])

eq_(sorted(rows), [(1, "foo"), (2, "bar"), (3, "def")])

#2848

Конструкция bindparam(), не имеющая типа, обновляется через копирование, когда тип становится доступным

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

Если дано не типизированное bindparam():

bp = bindparam("some_col")

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

expr = mytable.c.col == bp

Тип для bp остается NullType, однако если mytable.c.col имеет тип String, то expr.right, то есть правая часть бинарного выражения, примет тип String. Ранее сам bp менялся местами и имел тип String.

Аналогичным образом эта операция происходит в Insert или Update:

stmt = mytable.update().values(col=bp)

Выше bp остается неизменным, но при выполнении оператора будет использоваться тип String, в чем мы можем убедиться, рассмотрев словарь binds:

>>> compiled = stmt.compile()
>>> compiled.binds["some_col"].type
String

Эта возможность позволяет использовать пользовательские типы в операторах INSERT/UPDATE без необходимости явного указания этих типов в каждом выражении bindparam().

Потенциально обратно совместимые изменения связаны с двумя маловероятными сценариями. Поскольку связанный параметр является клонированным, пользователи не должны рассчитывать на внесение изменений на месте в конструкцию bindparam() после ее создания. Кроме того, код, использующий bindparam() внутри оператора Insert или Update, который полагается на то, что bindparam() не типизирован в соответствии с колонкой, которой он присваивается, больше не будет работать таким образом.

#2850

Колонки могут надежно получать свой тип от колонки, на которую ссылаются через ForeignKey

Существует давно известное поведение, которое гласит, что Column может быть объявлен без типа, если на этот Column ссылается ForeignKeyConstraint, и тип из ссылающегося столбца будет скопирован в этот. Проблема заключалась в том, что эта возможность никогда не работала очень хорошо и не поддерживалась. Основная проблема заключалась в том, что объект ForeignKey не знает, на какой целевой Column он ссылается, пока его не спросят, обычно это происходит в первый раз, когда внешний ключ используется для построения Join. Поэтому до этого момента родительский Column не будет иметь типа, точнее, он будет иметь тип по умолчанию NullType.

Хотя это заняло много времени, работа по реорганизации инициализации объектов ForeignKey была завершена, и эта функция наконец-то может работать приемлемо. Суть изменений заключается в том, что атрибут ForeignKey.column больше не инициализирует местоположение целевого Column; проблема с этой системой заключалась в том, что принадлежащий Column застревал с NullType в качестве своего типа до тех пор, пока ForeignKey не был использован.

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

Теперь корректно работают следующие сценарии:

  1. Тип на Column сразу же появляется, как только целевой Column становится связанным с тем же MetaData; это работает независимо от того, какая сторона сконфигурирована первой:

    >>> from sqlalchemy import Table, MetaData, Column, Integer, ForeignKey
    >>> metadata = MetaData()
    >>> t2 = Table("t2", metadata, Column("t1id", ForeignKey("t1.id")))
    >>> t2.c.t1id.type
    NullType()
    >>> t1 = Table("t1", metadata, Column("id", Integer, primary_key=True))
    >>> t2.c.t1id.type
    Integer()
  2. Теперь система работает и с ForeignKeyConstraint:

    >>> from sqlalchemy import Table, MetaData, Column, Integer, ForeignKeyConstraint
    >>> metadata = MetaData()
    >>> t2 = Table(
    ...     "t2",
    ...     metadata,
    ...     Column("t1a"),
    ...     Column("t1b"),
    ...     ForeignKeyConstraint(["t1a", "t1b"], ["t1.a", "t1.b"]),
    ... )
    >>> t2.c.t1a.type
    NullType()
    >>> t2.c.t1b.type
    NullType()
    >>> t1 = Table(
    ...     "t1",
    ...     metadata,
    ...     Column("a", Integer, primary_key=True),
    ...     Column("b", Integer, primary_key=True),
    ... )
    >>> t2.c.t1a.type
    Integer()
    >>> t2.c.t1b.type
    Integer()
  3. Он работает даже для «многоходовых» соединений, т.е. ForeignKey, который ссылается на Column, который ссылается на другой Column:

    >>> from sqlalchemy import Table, MetaData, Column, Integer, ForeignKey
    >>> metadata = MetaData()
    >>> t2 = Table("t2", metadata, Column("t1id", ForeignKey("t1.id")))
    >>> t3 = Table("t3", metadata, Column("t2t1id", ForeignKey("t2.t1id")))
    >>> t2.c.t1id.type
    NullType()
    >>> t3.c.t2t1id.type
    NullType()
    >>> t1 = Table("t1", metadata, Column("id", Integer, primary_key=True))
    >>> t2.c.t1id.type
    Integer()
    >>> t3.c.t2t1id.type
    Integer()

#1765

Диалектные изменения

Firebird fdb теперь является диалектом Firebird по умолчанию.

Диалект fdb теперь используется, если движок создается без спецификатора диалекта, т.е. firebird://. fdb - это kinterbasdb совместимый DBAPI, который, согласно проекту Firebird, теперь является их официальным Python-драйвером.

#2504

В Firebird fdb и kinterbasdb по умолчанию устанавливается retaining=False

Обе СУБД fdb и kinterbasdb поддерживают флаг retaining=True, который может быть передан методам commit() и rollback() своего соединения. Документальное обоснование этого флага заключается в том, что DBAPI может повторно использовать внутреннее состояние транзакции для последующих транзакций с целью повышения производительности. Однако более новая документация ссылается на анализ «сборки мусора» Firebird, согласно которому этот флаг может оказывать негативное влияние на способность базы данных обрабатывать задачи очистки, в результате чего, как сообщается, производительность может быть уменьшена.

Не совсем понятно, как этот флаг можно использовать, учитывая эту информацию, и поскольку он, по-видимому, только повышает производительность, то по умолчанию он принимает значение False. Значением можно управлять, передавая флаг retaining=True в вызов create_engine(). Это новый флаг, добавленный начиная с версии 0.8.2, поэтому приложения, работающие с версией 0.8.2, могут начать устанавливать его в значение True или False по своему усмотрению.

См.также

sqlalchemy.dialects.firebird.fdb

sqlalchemy.dialects.firebird.kinterbasdb

https://pythonhosted.org/fdb/usage-guide.html#retaining-transactions - информация о флаге «удерживающий».

#2763

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