Что нового в 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

Расширения C были перенесены для поддержки 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() применяется для того, чтобы предотвратить изменения целевого атрибута от какого-либо влияния в процессе смыва. Это достигается путем исключения атрибута из рассмотрения в процессе смыва. Однако до сих пор изменения атрибута все равно регистрировали родительский объект как «грязный» и вызывали потенциальную возможность смыва. Изменение заключается в том, что флаг 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 это возвращало True для строк A, в которых не было b:

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, если проксируемый объект отсутствует. Это соответствует тому факту, что отсутствующие многие-к-одному возвращают None в SQLAlchemy, так же должно возвращаться и проксируемое значение. Например:

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, который будет выдавать callables загрузчика для незагруженного атрибута.

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

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() больше не рассматривает знак + как закодированный пробел

По какой-то причине функция Python unquote_plus() применялась к полю «пароль» URL, что является неправильным применением правил кодирования, описанных в 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())
(x COLLATE en_EN) DESC

>>> # 0.9
>>> print(column("x").collate("en_EN").desc())
x COLLATE en_EN DESC

#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()))
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)))
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, которая обходит валидатор для случая, когда событие инициировано из обратной ссылки:

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

Диалект 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 (в настоящее время это только 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, единственная база данных, которая не будет поддерживать этот очень распространенный синтаксис SQL даже в 2013 году, сама создает дополнительные сложности, переписывая приведенные выше запросы как:

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

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

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

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

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

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

#2836

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

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

Система событий атрибутов больше не смотрит на этот объект «инициатор», чтобы остановить рекурсивную серию событий атрибутов. Вместо этого система предотвращения бесконечной рекурсии из-за взаимозависимых обработчиков backref была перенесена на обработчики событий ORM backref, которые теперь берут на себя роль гаранта того, что цепочка взаимозависимых событий (например, 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 изменится по сравнению с исходным значением в строке событий, инициированных обратными ссылками, так как обработчики обратных ссылок теперь могут подставлять новое значение 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

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

Это изменение упрощает использование в Core так называемых «кавычных» флагов, таких как флаг 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()))
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())
... )
SELECT t.x, t.y FROM t WHERE false

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

>>> expr = true()
>>> expr = expr & (t1.c.y > 5)
>>> print(select([t1]).where(expr))
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()))
SELECT t.x, t.y FROM t WHERE 0 = 1

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

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

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

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

#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 по умолчанию

Оба DBAPI 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

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