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

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

Данный документ описывает изменения между версией SQLAlchemy 0.9, находящейся на техническом обслуживании по состоянию на май 2014 года, и версией SQLAlchemy 1.0, выпущенной в апреле 2015 года.

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

Введение

Данное руководство знакомит с нововведениями в SQLAlchemy версии 1.0, а также документирует изменения, которые коснутся пользователей, переносящих свои приложения с SQLAlchemy серии 0.9 на 1.0.

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

Новые возможности и усовершенствования - ORM

Новый API Session Bulk INSERT/UPDATE

Создана новая серия методов Session, позволяющих подключаться непосредственно к средствам создания операторов INSERT и UPDATE. При правильном использовании эта экспертно-ориентированная система может позволить использовать ORM-сопоставления для генерации массовых операторов вставки и обновления, объединенных в группы executemany, что позволяет выполнять операции со скоростью, не уступающей прямому использованию ядра.

См.также

Операции с сыпучими материалами - введение и полная документация

#3100

Новый набор примеров производительности

Вдохновленный бенчмаркингом, проведенным для функции Операции с сыпучими материалами, а также для раздела FAQ Как профилировать приложение, работающее на SQLAlchemy?, был добавлен новый раздел примеров, содержащий несколько сценариев, призванных проиллюстрировать относительную производительность различных методов Core и ORM. Сценарии сгруппированы по вариантам использования и упакованы в единый консольный интерфейс, позволяющий запускать любую комбинацию демонстраций с выводом таймингов, результатов профилей Python и/или отображением профилей RunSnake.

«Запеченные» запросы

Функция «запеченных» запросов - это новый необычный подход, позволяющий прямолинейно строить запросы к объектам Query с использованием кэширования, что при последующих обращениях позволяет значительно снизить накладные расходы на вызов функций Python (более 75%). При задании объекта Query в виде серии лямбд, которые вызываются только один раз, запрос как заранее скомпилированный блок становится вполне реализуемым:

from sqlalchemy.ext import baked
from sqlalchemy import bindparam

bakery = baked.bakery()


def search_for_user(session, username, email=None):
    baked_query = bakery(lambda session: session.query(User))
    baked_query += lambda q: q.filter(User.name == bindparam("username"))

    baked_query += lambda q: q.order_by(User.id)

    if email:
        baked_query += lambda q: q.filter(User.email == bindparam("email"))

    result = baked_query(session).params(username=username, email=email).all()

    return result

#3054

Полный поиск объектов в ORM на 25% быстрее

Механика модуля loading.py, а также карта идентичностей прошли несколько этапов инлайнинга, рефакторинга и обрезки, так что теперь необработанная загрузка строк заполняет объекты на базе ORM примерно на 25% быстрее. Если предположить, что таблица состоит из 1 млн. строк, то следующий сценарий иллюстрирует тип загрузки, который был улучшен в наибольшей степени:

import time
from sqlalchemy import Integer, Column, create_engine, Table
from sqlalchemy.orm import Session
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class Foo(Base):
    __table__ = Table(
        "foo",
        Base.metadata,
        Column("id", Integer, primary_key=True),
        Column("a", Integer(), nullable=False),
        Column("b", Integer(), nullable=False),
        Column("c", Integer(), nullable=False),
    )


engine = create_engine("mysql+mysqldb://scott:tiger@localhost/test", echo=True)

sess = Session(engine)

now = time.time()

# avoid using all() so that we don't have the overhead of building
# a large list of full objects in memory
for obj in sess.query(Foo).yield_per(100).limit(1000000):
    pass

print("Total time: %d" % (time.time() - now))

Результаты локального MacBookPro варьируются от 19 секунд для 0.9 до 14 секунд для 1.0. Вызов Query.yield_per() всегда является хорошей идеей при пакетной обработке большого количества строк, так как он предотвращает необходимость выделения интерпретатором Python огромного объема памяти сразу для всех объектов и их инструментария. Без Query.yield_per() приведенный выше сценарий на MacBookPro выполняется 31 секунду на 0.9 и 26 секунд на 1.0 - дополнительное время уходит на настройку очень больших буферов памяти.

Новая реализация KeyedTuple значительно быстрее

Мы рассмотрели реализацию KeyedTuple в надежде улучшить запросы, подобные этому:

rows = sess.query(Foo.a, Foo.b, Foo.c).all()

Используется класс KeyedTuple, а не питоновский collections.namedtuple(), поскольку последний имеет очень сложную процедуру создания типов, которая по сравнению с KeyedTuple работает гораздо медленнее. Однако при выборке сотен тысяч строк collections.namedtuple() быстро обгоняет KeyedTuple, который становится резко медленнее при увеличении числа обращений к экземплярам. Что же делать? Создать новый тип, который будет хеджировать между подходами обоих типов. Сравнивая все три типа по параметрам «size» (количество возвращаемых строк) и «num» (количество отдельных запросов), новый «lightweight keyed tuple» либо превосходит оба типа, либо очень незначительно отстает от более быстрого объекта, в зависимости от сценария. В «слабом месте», когда мы одновременно создаем большое количество новых типов и получаем большое количество строк, облегченный объект полностью опережает и namedtuple, и KeyedTuple:

-----------------
size=10 num=10000                 # few rows, lots of queries
namedtuple: 3.60302400589         # namedtuple falls over
keyedtuple: 0.255059957504        # KeyedTuple very fast
lw keyed tuple: 0.582715034485    # lw keyed trails right on KeyedTuple
-----------------
size=100 num=1000                 # <--- sweet spot
namedtuple: 0.365247011185
keyedtuple: 0.24896979332
lw keyed tuple: 0.0889317989349   # lw keyed blows both away!
-----------------
size=10000 num=100
namedtuple: 0.572599887848
keyedtuple: 2.54251694679
lw keyed tuple: 0.613876104355
-----------------
size=1000000 num=10               # few queries, lots of rows
namedtuple: 5.79669594765         # namedtuple very fast
keyedtuple: 28.856498003          # KeyedTuple falls over
lw keyed tuple: 6.74346804619     # lw keyed trails right on namedtuple

#3176

Значительное улучшение использования структурной памяти

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

Стенд, использующий heapy для измерения стартового размера Nova, показывает, что при базовом импорте «nova.db.sqlalchemy.models» объекты SQLAlchemy, связанные словари и weakrefs занимают примерно на 3,7 мегабайта меньше, или 46%:

# reported by heapy, summation of SQLAlchemy objects +
# associated dicts + weakref-related objects with core of Nova imported:

    Before: total count 26477 total bytes 7975712
    After: total count 18181 total bytes 4236456

# reported for the Python module space overall with the
# core of Nova imported:

    Before: Partition of a set of 355558 objects. Total size = 61661760 bytes.
    After: Partition of a set of 346034 objects. Total size = 57808016 bytes.

Операторы UPDATE теперь объединяются в пакет с executemany() при промывке

Теперь операторы UPDATE могут быть объединены в рамках ORM flush в более производительный вызов executemany(), аналогично тому, как могут быть объединены операторы INSERT; этот вызов будет выполняться в рамках flush на основе следующих критериев:

  • два или более последовательных оператора UPDATE включают одинаковый набор изменяемых столбцов.

  • В операторе отсутствуют встроенные SQL-выражения в предложении SET.

  • В отображении не используется mapper.version_id_col, либо диалект бэкенда поддерживает «вменяемый» rowcount для операции executemany(); сейчас большинство DBAPI поддерживают это корректно.

Session.get_bind() обрабатывает более широкий спектр сценариев наследования

Метод Session.get_bind() вызывается всякий раз, когда в запросе или в процессе промывки блока работ запрашивается движок базы данных, соответствующий определенному классу. Метод был усовершенствован для работы с различными сценариями, ориентированными на наследование, в том числе:

  • Привязка к миксину или абстрактному классу:

    class MyClass(SomeMixin, Base):
        __tablename__ = "my_table"
        # ...
    
    
    session = Session(binds={SomeMixin: some_engine})
  • Привязка к наследуемым конкретным подклассам по отдельности на основе таблицы:

    class BaseClass(Base):
        __tablename__ = "base"
    
        # ...
    
    
    class ConcreteSubClass(BaseClass):
        __tablename__ = "concrete"
    
        # ...
    
        __mapper_args__ = {"concrete": True}
    
    
    session = Session(binds={base_table: some_engine, concrete_table: some_other_engine})

#3035

Session.get_bind() будет получать Mapper во всех соответствующих случаях Query

Был исправлен ряд проблем, когда Session.get_bind() не получал первичный Mapper от Query, хотя этот картограф был легко доступен (первичный картограф - это единственный или, как вариант, первый картограф, который связан с объектом Query).

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

Эти сценарии включают:

  • Query.count():

    session.query(User).count()
  • Query.update() и Query.delete(), как для оператора UPDATE/DELETE, так и для SELECT, используемого стратегией «fetch»:

    session.query(User).filter(User.id == 15).update(
        {"name": "foob"}, synchronize_session="fetch"
    )
    
    session.query(User).filter(User.id == 15).delete(synchronize_session="fetch")
  • Запросы к отдельным столбцам:

    session.query(User.id, User.name).all()
  • Функции SQL и другие выражения против косвенных отображений, таких как column_property:

    class User(Base):
        ...
    
        score = column_property(func.coalesce(self.tables.users.c.name, None))
    
    
    session.query(func.max(User.score)).scalar()

#3227 #3242 #1326

Улучшения в словаре .info

Коллекция InspectionAttr.info теперь доступна для всех типов объектов, которые можно получить из коллекции Mapper.all_orm_descriptors. Это включает в себя hybrid_property и association_proxy(). Однако, поскольку эти объекты являются дескрипторами, привязанными к классу, для получения атрибута к ним необходимо обращаться отдельно от класса, к которому они привязаны. Ниже это показано на примере пространства имен Mapper.all_orm_descriptors:

class SomeObject(Base):
    # ...

    @hybrid_property
    def some_prop(self):
        return self.value + 5


inspect(SomeObject).all_orm_descriptors.some_prop.info["foo"] = "bar"

Он также доступен в качестве аргумента конструктора для всех объектов SchemaItem (например, ForeignKey, UniqueConstraint и т.д.), а также для остальных ORM-конструкций, таких как synonym().

#2971

#2963

Конструкции ColumnProperty гораздо лучше работают с псевдонимами, order_by

Исправлен ряд проблем, связанных с column_property(), в частности, с конструкцией aliased(), а также с логикой «упорядочивания по метке», введенной в 0.9 (см. Конструкции меток теперь могут отображаться только в виде своего имени в ORDER BY).

Если задано отображение, подобное следующему:

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(ForeignKey("a.id"))


A.b = column_property(select([func.max(B.id)]).where(B.a_id == A.id).correlate(A))

Простой сценарий, включающий дважды «A.b», не будет корректно отображаться:

print(sess.query(A, a1).order_by(a1.b))

Это приведет к упорядочиванию по неправильному столбцу:

SELECT a.id AS a_id, (SELECT max(b.id) AS max_1 FROM b
WHERE b.a_id = a.id) AS anon_1, a_1.id AS a_1_id,
(SELECT max(b.id) AS max_2
FROM b WHERE b.a_id = a_1.id) AS anon_2
FROM a, a AS a_1 ORDER BY anon_1

Новый выход:

SELECT a.id AS a_id, (SELECT max(b.id) AS max_1
FROM b WHERE b.a_id = a.id) AS anon_1, a_1.id AS a_1_id,
(SELECT max(b.id) AS max_2
FROM b WHERE b.a_id = a_1.id) AS anon_2
FROM a, a AS a_1 ORDER BY anon_2

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

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    type = Column(String)

    __mapper_args__ = {"polymorphic_on": type, "with_polymorphic": "*"}

Order_by не сможет использовать метку, так как она будет анонимизирована из-за полиморфной загрузки:

SELECT a.id AS a_id, a.type AS a_type, (SELECT max(b.id) AS max_1
FROM b WHERE b.a_id = a.id) AS anon_1
FROM a ORDER BY (SELECT max(b.id) AS max_2
FROM b WHERE b.a_id = a.id)

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

SELECT a.id AS a_id, a.type AS a_type, (SELECT max(b.id) AS max_1
FROM b WHERE b.a_id = a.id) AS anon_1
FROM a ORDER BY anon_1

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

#3148 #3188

Новые возможности и усовершенствования - Ядро

Select/Query LIMIT / OFFSET может быть задан в виде произвольного SQL-выражения

Методы Select.limit() и Select.offset() теперь принимают в качестве аргументов не только целочисленные значения, но и любые выражения SQL. Объект ORM Query также передает любое выражение базовому объекту Select. Обычно это используется для передачи связанного параметра, который впоследствии может быть заменен значением:

sel = select([table]).limit(bindparam("mylimit")).offset(bindparam("myoffset"))

Диалекты, не поддерживающие нецелые выражения LIMIT или OFFSET, могут продолжать не поддерживать это поведение; сторонним диалектам также может потребоваться модификация, чтобы воспользоваться преимуществами нового поведения. Диалект, который в настоящее время использует атрибуты ._limit или ._offset, будет продолжать работать в тех случаях, когда ограничение/смещение было задано как простое целое значение. Однако при указании SQL-выражения эти два атрибута будут вызывать ошибку CompileError при обращении к ним. Сторонние диалекты, желающие поддерживать новую возможность, теперь должны обращаться к атрибутам ._limit_clause и ._offset_clause для получения полного SQL-выражения, а не целочисленного значения.

Флаг use_alter на ForeignKeyConstraint (обычно) больше не нужен

Методы MetaData.create_all() и MetaData.drop_all() теперь будут использовать систему, которая автоматически формирует оператор ALTER для ограничений внешнего ключа, участвующих во взаимозависимых циклах между таблицами, без необходимости указывать ForeignKeyConstraint.use_alter. Кроме того, для создания ограничений по внешним ключам с помощью ALTER больше не нужно указывать имя; имя требуется только для операции DROP. В случае DROP функция гарантирует, что только те ограничения, которые имеют явные имена, будут включены в операторы ALTER. В случае неразрешимого цикла в DROP система выдает краткое и понятное сообщение об ошибке, если DROP не может быть продолжен.

Флаги ForeignKeyConstraint.use_alter и ForeignKey.use_alter остаются на своих местах и продолжают оказывать то же действие, устанавливая те ограничения, для которых требуется ALTER в сценарии CREATE/DROP.

Начиная с версии 1.0.1, в случае SQLite, не поддерживающего ALTER, в случае, если при выполнении DROP в заданных таблицах возникает неразрешимый цикл, то выдается предупреждение, и таблицы сбрасываются с no упорядочением, что обычно нормально для SQLite, если только не включены ограничения. Чтобы устранить предупреждение и выполнить хотя бы частичное упорядочивание в базе данных SQLite, особенно в той, где включены ограничения, повторно установите флаги «use_alter» на те объекты ForeignKey и ForeignKeyConstraint, которые должны быть явно опущены из сортировки.

См.также

Создание/удаление ограничений внешних ключей с помощью ALTER - полное описание нового поведения.

#3282

ResultProxy «автоматическое закрытие» теперь является «мягким» закрытием

Во многих релизах объект ResultProxy всегда автоматически закрывался в момент извлечения всех строк результатов. Это позволяло использовать объект без необходимости явного обращения к ResultProxy.close(); поскольку все ресурсы DBAPI были освобождены, объект можно было смело выбрасывать. Однако объект сохранил строгое «закрытое» поведение, что означает, что любые последующие вызовы ResultProxy.fetchone(), ResultProxy.fetchmany() или ResultProxy.fetchall() теперь будут вызывать ошибку ResourceClosedError:

>>> result = connection.execute(stmt)
>>> result.fetchone()
(1, 'x')
>>> result.fetchone()
None  # indicates no more rows
>>> result.fetchone()
exception: ResourceClosedError

Такое поведение не согласуется с тем, что указано в pep-249, согласно которому можно многократно обращаться к методам fetch даже после исчерпания результатов. Кроме того, это противоречит поведению некоторых реализаций result proxy, например, BufferedColumnResultProxy, используемых диалектом cx_oracle для некоторых типов данных.

Для решения этой проблемы состояние «закрыто» ResultProxy было разбито на два состояния: «мягкое закрытие», которое делает большую часть того, что делает «закрытие», т.е. освобождает курсор DBAPI и в случае объекта «закрыть с результатом» также освобождает соединение, и состояние «закрыто», которое включает в себя все, что включает «мягкое закрытие», а также устанавливает методы выборки как «закрытые». Метод ResultProxy.close() теперь никогда не вызывается неявно, только метод ResultProxy._soft_close(), который является непубличным:

>>> result = connection.execute(stmt)
>>> result.fetchone()
(1, 'x')
>>> result.fetchone()
None  # indicates no more rows
>>> result.fetchone()
None  # still None
>>> result.fetchall()
[]
>>> result.close()
>>> result.fetchone()
exception: ResourceClosedError  # *now* it raises

#3330 #3329

Ограничения CHECK теперь поддерживают маркер %(column_0_name)s в соглашениях об именовании

В качестве источника %(column_0_name)s будет выступать первый столбец, встречающийся в выражении CheckConstraint:

metadata = MetaData(naming_convention={"ck": "ck_%(table_name)s_%(column_0_name)s"})

foo = Table("foo", metadata, Column("value", Integer))

CheckConstraint(foo.c.value > 5)

Будет оказывать:

CREATE TABLE foo (
    value INTEGER,
    CONSTRAINT ck_foo_value CHECK (value > 5)
)

Комбинация соглашений об именовании с ограничением, созданным с помощью SchemaType, например Boolean или Enum, теперь также будет использовать все соглашения об ограничениях CHECK.

#3299

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

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

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

m = MetaData()

t = Table("t", m, Column("a", Integer), Column("b", Integer))

uq = UniqueConstraint(t.c.a, t.c.b)  # will auto-attach to Table

assert uq in t.constraints

Для того чтобы помочь в некоторых случаях, которые обычно возникают при декларативном использовании, эта же логика автоприсоединения теперь может работать, даже если объекты Column еще не связаны с Table; устанавливаются дополнительные события, чтобы при связывании объектов Column также добавлялся объект Constraint:

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

m = MetaData()

a = Column("a", Integer)
b = Column("b", Integer)

uq = UniqueConstraint(a, b)

t = Table("t", m, a, b)

assert uq in t.constraints  # constraint auto-attached

Приведенная выше возможность была добавлена позднее, начиная с версии 1.0.0b3. Исправление версии 1.0.4 для #3411 гарантирует, что эта логика не возникнет, если Constraint ссылается на смесь Column объектов и строковых имен столбцов; поскольку мы пока не отслеживаем добавление имен в Table:

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

m = MetaData()

a = Column("a", Integer)
b = Column("b", Integer)

uq = UniqueConstraint(a, "b")

t = Table("t", m, a, b)

# constraint *not* auto-attached, as we do not have tracking
# to locate when a name 'b' becomes available on the table
assert uq not in t.constraints

Так, событие прикрепления столбца «a» к таблице «t» произойдет раньше, чем прикрепление столбца «b» (поскольку «a» указано в конструкторе Table раньше, чем «b»), и при попытке прикрепления ограничение не сможет найти «b». Для согласованности, если ограничение ссылается на какие-либо строковые имена, то логика автоприсоединения колонок пропускается.

Изначальная логика автоприсоединения, конечно, сохраняется, если в момент построения Table уже содержит все целевые объекты Column: Constraint:

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

m = MetaData()

a = Column("a", Integer)
b = Column("b", Integer)


t = Table("t", m, a, b)

uq = UniqueConstraint(a, "b")

# constraint auto-attached normally as in older versions
assert uq in t.constraints

#3341 #3411

INSERT FROM SELECT теперь включает значения по умолчанию для Python и SQL-выражений

Insert.from_select() теперь включает значения по умолчанию для Python и SQL-выражений, если иное не указано; ограничение, при котором значения по умолчанию для несерверных колонок не включались в INSERT FROM SELECT, теперь снято, и эти выражения выводятся как константы в оператор SELECT:

from sqlalchemy import Table, Column, MetaData, Integer, select, func

m = MetaData()

t = Table(
    "t", m, Column("x", Integer), Column("y", Integer, default=func.somefunction())
)

stmt = select([t.c.x])
print(t.insert().from_select(["x"], stmt))

Будет оказывать:

INSERT INTO t (x, y) SELECT t.x, somefunction() AS somefunction_1
FROM t

Функция может быть отключена с помощью клавиши Insert.from_select.include_defaults.

Серверные значения столбцов по умолчанию теперь отображают литеральные значения

Флаг компилятора «literal binds» включается, если в компилируемом выражении SQL присутствует DefaultClause, заданный Column.server_default. Это позволяет корректно отображать литералы, встроенные в SQL, например:

from sqlalchemy import Table, Column, MetaData, Text
from sqlalchemy.schema import CreateTable
from sqlalchemy.dialects.postgresql import ARRAY, array
from sqlalchemy.dialects import postgresql

metadata = MetaData()

tbl = Table(
    "derp",
    metadata,
    Column("arr", ARRAY(Text), server_default=array(["foo", "bar", "baz"])),
)

print(CreateTable(tbl).compile(dialect=postgresql.dialect()))

Теперь рендеринг:

CREATE TABLE derp (
    arr TEXT[] DEFAULT ARRAY['foo', 'bar', 'baz']
)

Ранее литеральные значения "foo", "bar", "baz" отображались как связанные параметры, которые бесполезны в DDL.

#3087

UniqueConstraint теперь является частью процесса отражения таблицы

Объект Table, заполненный с помощью autoload=True, теперь будет включать конструкции UniqueConstraint, а также конструкции Index. Эта логика имеет несколько оговорок для PostgreSQL и MySQL:

PostgreSQL

В PostgreSQL реализовано такое поведение, что при создании UNIQUE-ограничения неявно создается и UNIQUE-индекс, соответствующий этому ограничению. Методы Inspector.get_indexes() и Inspector.get_unique_constraints() по-прежнему будут обоими возвращать эти записи, причем в методе Inspector.get_indexes() теперь внутри записи индекса присутствует маркер duplicates_constraint, указывающий на соответствующее ограничение при его обнаружении. Однако при выполнении полного отражения таблицы с помощью Table(..., autoload=True) конструкция Index обнаруживается как связанная с UniqueConstraint и не присутствует в коллекции Table.indexes; только UniqueConstraint будет присутствовать в коллекции Table.constraints. Эта логика дедупликации работает путем присоединения к таблице pg_constraint при запросе pg_index на предмет наличия связи между двумя конструкциями.

MySQL

В MySQL нет отдельных понятий для UNIQUE INDEX и UNIQUE constraint. Хотя при создании таблиц и индексов поддерживаются оба синтаксиса, они не хранятся по-разному. Методы Inspector.get_indexes() и Inspector.get_unique_constraints() будут продолжать обоюдно возвращать запись для индекса UNIQUE в MySQL, где Inspector.get_unique_constraints() содержит новый маркер duplicates_index внутри записи об ограничении, указывающий на то, что это дублирующая запись, соответствующая данному индексу. Однако при выполнении полного отражения таблицы с помощью Table(..., autoload=True) конструкция UniqueConstraint ни при каких обстоятельствах не является **** частью полностью отраженной конструкции Table; эта конструкция всегда представлена Index с параметром unique=True, присутствующим в коллекции Table.indexes.

#3184

Новые системы для безопасного излучения параметрических предупреждений

Долгое время существовало ограничение, согласно которому предупреждения не могли ссылаться на элементы данных, в результате чего конкретная функция могла выдавать бесконечное число уникальных предупреждений. Ключевым местом, где это происходит, является предупреждение Unicode type received non-unicode bind param value. Если поместить в это сообщение значение данных, то Python-коллекция __warningregistry__ для данного модуля или, в некоторых случаях, Python-глобальная warnings.onceregistry будут расти бесконечно, поскольку в большинстве сценариев предупреждений одна из этих двух коллекций заполняется каждым отдельным предупреждением.

Суть изменения заключается в том, что, используя специальный тип string, который специально изменяет способ хеширования строки, мы можем контролировать, чтобы большое количество параметризованных сообщений хешировалось только по небольшому набору возможных хеш-значений, так что предупреждение типа Unicode type received non-unicode bind param value может быть настроено на выдачу только определенное количество раз; после этого реестр предупреждений Python начнет регистрировать их как дубликаты.

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

from sqlalchemy import create_engine, Unicode, select, cast
import random
import warnings

e = create_engine("sqlite://")

# Use the "once" filter (which is also the default for Python
# warnings).  Exactly ten of these warnings will
# be emitted; beyond that, the Python warnings registry will accumulate
# new values as dupes of one of the ten existing.
warnings.filterwarnings("once")

for i in range(1000):
    e.execute(
        select([cast(("foo_%d" % random.randint(0, 1000000)).encode("ascii"), Unicode)])
    )

Формат предупреждения здесь следующий:

/path/lib/sqlalchemy/sql/sqltypes.py:186: SAWarning: Unicode type received
  non-unicode bind param value 'foo_4852'. (this warning may be
  suppressed after 10 occurrences)

#3178

Ключевые изменения в поведении - ORM

query.update() теперь разрешает строковые имена в имена отображаемых атрибутов

В документации к Query.update() указано, что заданный словарь values представляет собой «словарь с именами атрибутов в качестве ключей», что подразумевает отображение имен атрибутов. К сожалению, функция была разработана скорее для приема атрибутов и SQL-выражений, а не строк; при передаче строк эти строки передавались прямо в оператор обновления ядра без какого-либо разрешения того, как эти имена представлены в сопоставленном классе, т.е. имя должно было точно соответствовать столбцу таблицы, а не тому, как атрибут с таким именем был сопоставлен классу.

Имена строк теперь разрешаются как имена атрибутов:

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column("user_name", String(50))

Выше столбец user_name отображен как name. Ранее вызов Query.update(), которому передавались строки, должен был вызываться следующим образом:

session.query(User).update({"user_name": "moonbeam"})

Теперь заданная строка разрешается относительно сущности:

session.query(User).update({"name": "moonbeam"})

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

session.query(User).update({User.name: "moonbeam"})

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

class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column("user_name", String(50))

    @hybrid_property
    def fullname(self):
        return self.name


session.query(User).update({"fullname": "moonbeam"})

#3228

Предупреждения, выдаваемые при сравнении объектов со значениями None с отношениями

Это изменение появилось в версии 1.0.1. Некоторые пользователи выполняют запросы, которые по сути имеют следующий вид:

session.query(Address).filter(Address.user == User(id=None))

В настоящее время этот паттерн не поддерживается в SQLAlchemy. Для всех версий он выдает SQL, похожий на:

SELECT address.id AS address_id, address.user_id AS address_user_id,
address.email_address AS address_email_address
FROM address WHERE ? = address.user_id
(None,)

Обратите внимание, выше приведено сравнение WHERE ? = address.user_id, при котором связанное значение ? получает None, или NULL в SQL. Это всегда будет возвращать False в SQL. Приведенное здесь сравнение теоретически могло бы генерировать SQL следующим образом:

SELECT address.id AS address_id, address.user_id AS address_user_id,
address.email_address AS address_email_address
FROM address WHERE address.user_id IS NULL

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

SAWarning: Got None for value of column user.id; this is unsupported
for a relationship comparison and will not currently produce an
IS comparison (but may in a future release)

Обратите внимание, что в релизе 1.0.0, включая все бета-версии, этот паттерн был нарушен в большинстве случаев; генерировалось значение типа SYMBOL('NEVER_SET'). Эта проблема была исправлена, но в результате выявления этого паттерна появилось предупреждение, чтобы мы могли более безопасно исправить это нарушенное поведение (теперь отраженное в #3373) в будущем релизе.

#3371

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

Это изменение появилось в версии 1.0.1; хотя мы предпочли бы, чтобы оно было в версии 1.0.0, оно стало очевидным только в результате появления #3371.

Дано отображение:

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(ForeignKey("a.id"))
    a = relationship("A")

Дано A, с первичным ключом 7, но который мы изменили на 10 без промывки:

s = Session(autoflush=False)
a1 = A(id=7)
s.add(a1)
s.commit()

a1.id = 10

В запросе на отношение «многие-к-одному» с этим объектом в качестве целевого будет использоваться значение 10 в связанных параметрах:

s.query(B).filter(B.a == a1)

Производит:

SELECT b.id AS b_id, b.a_id AS b_a_id
FROM b
WHERE ? = b.a_id
(10,)

Однако до этого изменения отрицание этого критерия не использовало бы 10, а использовало бы 7, если бы объект не был сначала очищен:

s.query(B).filter(B.a != a1)

Производит (в 0.9 и всех версиях до 1.0.1):

SELECT b.id AS b_id, b.a_id AS b_a_id
FROM b
WHERE b.a_id != ? OR b.a_id IS NULL
(7,)

Для переходного объекта это приведет к нарушению запроса:

SELECT b.id, b.a_id
FROM b
WHERE b.a_id != :a_id_1 OR b.a_id IS NULL
-- {u'a_id_1': symbol('NEVER_SET')}

Это несоответствие было устранено, и теперь во всех запросах будет использоваться текущее значение атрибута, в данном примере 10.

#3374

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

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

Дан объект, не имеющий состояния:

>>> obj = Foo()

Поведение SQLAlchemy всегда было таково, что если мы обращаемся к скалярному или много-одному атрибуту, который никогда не был установлен, то он возвращается в виде None:

>>> obj.someattr
None

Это значение None фактически является теперь частью состояния obj и не отличается от того, как если бы мы явно установили атрибут, например, obj.someattr = None. Однако в данном случае «set on get» будет вести себя по-другому с точки зрения истории и событий. Он не будет выдавать никаких событий, связанных с атрибутами, и, кроме того, если мы просмотрим историю, то увидим следующее:

>>> inspect(obj).attrs.someattr.history
History(added=(), unchanged=[None], deleted=())   # 0.9 and below

То есть, как если бы атрибут всегда был None и никогда не изменялся. Это явно отличается от того, если бы мы сначала установили атрибут:

>>> obj = Foo()
>>> obj.someattr = None
>>> inspect(obj).attrs.someattr.history
History(added=[None], unchanged=(), deleted=())  # all versions

Вышесказанное означает, что поведение нашей операции «set» может быть искажено тем фактом, что ранее доступ к значению осуществлялся через «get». В версии 1.0 эта несогласованность устранена: теперь при использовании стандартного «геттера» ничего не устанавливается.

>>> obj = Foo()
>>> obj.someattr
None
>>> inspect(obj).attrs.someattr.history
History(added=(), unchanged=(), deleted=())  # 1.0
>>> obj.someattr = None
>>> inspect(obj).attrs.someattr.history
History(added=[None], unchanged=(), deleted=())

Причина, по которой такое поведение не оказывает особого влияния, заключается в том, что оператор INSERT в реляционных базах данных в большинстве случаев рассматривает отсутствующее значение как то же самое, что и NULL. Получил ли SQLAlchemy событие истории для конкретного атрибута, установленного в None или нет, обычно не имеет значения, поскольку разница между отправкой None/NULL или нет не оказывает влияния. Однако, как показывает пример #3060 (описанный здесь в Приоритет изменения атрибутов, связанных с отношениями, по сравнению с атрибутами, связанными с ФК, может измениться), существуют редкие граничные случаи, когда мы действительно хотим, чтобы атрибут None был установлен положительно. Кроме того, разрешение события атрибута здесь означает, что теперь можно создавать функции «значения по умолчанию» для атрибутов, отображаемых в ORM.

В рамках этого изменения генерация неявного «None» теперь отключена и для других ситуаций, в которых это происходило раньше; в том числе при получении операции установки атрибута many-to-one; ранее «старым» значением было «None», если он не был установлен иным образом; теперь будет передано значение NEVER_SET, которое теперь может быть передано слушателю атрибутов. Этот символ также может быть получен при вызове служебных функций mapper, таких как Mapper.primary_key_from_instance(); если атрибуты первичного ключа вообще не заданы, то если раньше значение было None, то теперь это будет символ NEVER_SET, и никаких изменений в состоянии объекта не произойдет.

#3061

Приоритет изменения атрибутов, связанных с отношениями, по сравнению с атрибутами, связанными с ФК, может измениться

Как побочный эффект #3060, установка атрибута, связанного с отношениями, на None теперь является отслеживаемым событием истории, которое ссылается на намерение сохранить None для этого атрибута. Поскольку всегда было так, что установка атрибута, связанного с отношением, преобладает над прямым присвоением атрибутам внешнего ключа, изменение в поведении можно наблюдать при присвоении None. Если задано отображение:

class A(Base):
    __tablename__ = "table_a"

    id = Column(Integer, primary_key=True)


class B(Base):
    __tablename__ = "table_b"

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

В версии 1.0 атрибут relationship-bound имеет приоритет над атрибутом FK-bound во всех случаях, независимо от того, является ли присваиваемое значение ссылкой на объект A или None. В версии 0.9 поведение непоследовательно и действует только в случае присвоения значения; значение None не рассматривается:

a1 = A(id=1)
a2 = A(id=2)
session.add_all([a1, a2])
session.flush()

b1 = B()
b1.a = a1  # we expect a_id to be '1'; takes precedence in 0.9 and 1.0

b2 = B()
b2.a = None  # we expect a_id to be None; takes precedence only in 1.0

b1.a_id = 2
b2.a_id = 2

session.add_all([b1, b2])
session.commit()

assert b1.a is a1  # passes in both 0.9 and 1.0
assert b2.a is None  # passes in 1.0, in 0.9 it's a2

#3060

session.expunge() полностью отсоединит объект, который был удален

В поведении Session.expunge() была ошибка, которая приводила к несоответствию в поведении относительно удаленных объектов. Функция object_session(), а также атрибут InstanceState.session по-прежнему сообщали об объекте как о принадлежащем Session после удаления:

u1 = sess.query(User).first()
sess.delete(u1)

sess.flush()

assert u1 not in sess
assert inspect(u1).session is sess  # this is normal before commit

sess.expunge(u1)

assert u1 not in sess
assert inspect(u1).session is None  # would fail

Обратите внимание, что вполне нормально, если значение u1 not in sess равно True, в то время как inspect(u1).session все еще ссылается на сессию, транзакция продолжается после операции удаления и Session.expunge() не была вызвана; полное отсоединение обычно завершается после фиксации транзакции. Эта проблема также повлияет на функции, которые полагаются на Session.expunge(), такие как make_transient().

#3139

Явно запрещенная загрузка Joined/Subquery с помощью yield_per

Для того чтобы упростить использование метода Query.yield_per(), при использовании yield_per будет выдано исключение, если в действие вступят какие-либо загрузчики подзапросов или объединенные загрузчики, использующие коллекции, поскольку в настоящее время они не совместимы с yield-per (теоретически загрузка подзапросов, однако, возможна). При возникновении этой ошибки опция lazyload() может быть передана со звездочкой:

q = sess.query(Object).options(lazyload("*")).yield_per(100)

или использовать Query.enable_eagerloads():

q = sess.query(Object).enable_eagerloads(False).yield_per(100)

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

q = (
    sess.query(Object)
    .options(lazyload("*"), joinedload("some_manytoone"))
    .yield_per(100)
)

Изменения и исправления в работе с дублирующимися целями присоединения

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

Начиная с отображения в виде:

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

Base = declarative_base()


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


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

Запрос, который дважды присоединяется к A.bs:

print(s.query(A).join(A.bs).join(A.bs))

Будет оказывать:

SELECT a.id AS a_id
FROM a JOIN b ON a.id = b.a_id

Запрос дедуплицирует избыточный A.bs, поскольку пытается поддержать случай, подобный следующему:

s.query(A).join(A.bs).filter(B.foo == "bar").reset_joinpoint().join(A.bs, B.cs).filter(
    C.bar == "bat"
)

То есть A.bs является частью «пути». Как и в случае с #3367, если дважды прийти к одной и той же конечной точке, не являющейся частью большого пути, то теперь будет выдано предупреждение:

SAWarning: Pathed join target A.bs has already been joined to; skipping

Более серьезное изменение происходит при присоединении к сущности без использования пути, связанного с отношением. Если мы дважды присоединимся к B:

print(s.query(A).join(B, B.a_id == A.id).join(B, B.a_id == A.id))

В версии 0.9 это выглядит следующим образом:

SELECT a.id AS a_id
FROM a JOIN b ON b.a_id = a.id JOIN b AS b_1 ON b_1.a_id = a.id

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

В 1.0 автоматическое сглаживание не применяется, и мы получаем:

SELECT a.id AS a_id
FROM a JOIN b ON b.a_id = a.id JOIN b ON b.a_id = a.id

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

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

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

Base = declarative_base()


class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    type = Column(String)

    __mapper_args__ = {"polymorphic_on": type, "polymorphic_identity": "a"}


class ASub1(A):
    __mapper_args__ = {"polymorphic_identity": "asub1"}


class ASub2(A):
    __mapper_args__ = {"polymorphic_identity": "asub2"}


class B(Base):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)

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

    a = relationship("A", primaryjoin="B.a_id == A.id", backref="b")


s = Session()

print(s.query(ASub1).join(B, ASub1.b).join(ASub2, B.a))

print(s.query(ASub1).join(B, ASub1.b).join(ASub2, ASub2.id == B.a_id))

Два запроса в нижней части эквивалентны и должны выдавать одинаковый SQL:

SELECT a.id AS a_id, a.type AS a_type
FROM a JOIN b ON b.a_id = a.id JOIN a ON b.a_id = a.id AND a.type IN (:type_1)
WHERE a.type IN (:type_2)

Приведенный выше SQL является некорректным, поскольку в нем дважды встречается «a» в списке FROM. Однако ошибка неявного псевдонима будет иметь место только во втором запросе и приведет к такому результату:

SELECT a.id AS a_id, a.type AS a_type
FROM a JOIN b ON b.a_id = a.id JOIN a AS a_1
ON a_1.id = b.a_id AND a_1.type IN (:type_1)
WHERE a_1.type IN (:type_2)

Где выше, второе присоединение к «a» является алиасированным. Хотя это и кажется удобным, но в целом это не соответствует принципу работы запросов с одним наследованием и является ошибочным и непоследовательным.

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

asub2_alias = aliased(ASub2)

print(s.query(ASub1).join(B, ASub1.b).join(asub2_alias, B.a.of_type(asub2_alias)))

#3233 #3367

Отложенные столбцы больше не являются неявно отложенными

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

Устранены устаревшие крючки событий ORM

Следующие крючки событий ORM, некоторые из которых устарели с версии 0.5, были удалены: translate_row, populate_instance, append_result, create_instance. Сценарии использования этих крючков возникли в самых ранних версиях SQLAlchemy 0.1 / 0.2 и давно уже не нужны. В частности, эти крючки были в значительной степени бесполезны, поскольку поведенческие контракты в рамках этих событий были сильно связаны с окружающими внутренними компонентами, такими как создание и инициализация экземпляра, а также расположение столбцов в ORM-генерируемой строке. Удаление этих крючков значительно упрощает механику загрузки объектов ORM.

Изменение API для новой функции Bundle при использовании пользовательских загрузчиков строк

Новый объект Bundle в версии 0.9 имеет небольшое изменение в API, когда метод create_row_processor() переопределяется на пользовательском классе. Ранее код примера выглядел так:

from sqlalchemy.orm import Bundle


class DictBundle(Bundle):
    def create_row_processor(self, query, procs, labels):
        """Override create_row_processor to return values as dictionaries"""

        def proc(row, result):
            return dict(zip(labels, (proc(row, result) for proc in procs)))

        return proc

Теперь неиспользуемый член result удален:

from sqlalchemy.orm import Bundle


class DictBundle(Bundle):
    def create_row_processor(self, query, procs, labels):
        """Override create_row_processor to return values as dictionaries"""

        def proc(row):
            return dict(zip(labels, (proc(row) for proc in procs)))

        return proc

Вложенность правого внутреннего соединения теперь используется по умолчанию для joinedload с innerjoin=True

Поведение joinedload.innerjoin, как и relationship.innerjoin, теперь заключается в использовании «вложенных» внутренних соединений, то есть вложенных справа, в качестве поведения по умолчанию, когда соединенная с внутренним соединением eager load цепляется к соединенной с внешним соединением eager load. Для того чтобы получить старое поведение цепочки всех соединенных eager-грузок в качестве внешних соединений, когда присутствует внешнее соединение, используйте innerjoin="unnested".

Как было введено в Правые вложенные внутренние объединения доступны в объединенных ускоренных загрузках начиная с версии 0.9, поведение innerjoin="nested" заключается в том, что внутреннее соединение (inner join eager load), соединенное с внешним соединением (outer join eager load), будет использовать правое вложенное соединение (right-nested join). "nested" теперь подразумевается при использовании innerjoin=True:

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

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

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

То есть использование вложенного справа соединения для соединения INNER, чтобы можно было вернуть полный результат users. Использование INNER join более эффективно, чем использование OUTER join, и позволяет параметру оптимизации joinedload.innerjoin действовать во всех случаях.

Чтобы получить более старое поведение, используйте innerjoin="unnested":

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

Это позволит избежать право-вложенных соединений и объединить соединения в цепочку, используя все OUTER-соединения, несмотря на директиву innerjoin:

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

Как отмечается в примечаниях к версии 0.9, единственным бэкендом базы данных, испытывающим трудности с право-вложенными соединениями, является SQLite; SQLAlchemy в версии 0.9 преобразует право-вложенное соединение в подзапрос как цель соединения на SQLite.

#3008

Подзапросы больше не применяются к uselist=False, объединенным с eager-загрузкой

Если задана объединенная нагрузка типа:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    b = relationship("B", uselist=False)


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))


s = Session()
print(s.query(A).options(joinedload(A.b)).limit(5))

SQLAlchemy рассматривает отношение A.b как «один ко многим, загружаемое как одно значение», что по сути является отношением «один к одному». Однако объединенная ускоренная загрузка всегда рассматривала вышеописанную ситуацию как ситуацию, когда основной запрос должен находиться внутри подзапроса, как это обычно требуется для коллекции объектов B, где в основном запросе применяется LIMIT:

SELECT anon_1.a_id AS anon_1_a_id, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id
FROM (SELECT a.id AS a_id
FROM a LIMIT :param_1) AS anon_1
LEFT OUTER JOIN b AS b_1 ON anon_1.a_id = b_1.a_id

Однако, поскольку связь внутреннего запроса с внешним заключается в том, что в случае uselist=False разделяется максимум одна строка (аналогично many-to-one), «подзапрос», используемый с LIMIT + joined eager loading, в этом случае отпадает:

SELECT a.id AS a_id, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id
FROM a LEFT OUTER JOIN b AS b_1 ON a.id = b_1.a_id
LIMIT :param_1

В случае, когда LEFT OUTER JOIN возвращает более одной строки, ORM всегда выдавала предупреждение и игнорировала дополнительные результаты для uselist=False, поэтому результаты в этой ошибочной ситуации не должны измениться.

#3249

query.update() / query.delete() повышается, если используется с join(), select_from(), from_self()

В SQLAlchemy 0.9.10 (по состоянию на 9 июня 2015 г. еще не выпущен) выдается предупреждение, если методы Query.update() или Query.delete() вызываются в запросе, в котором также были вызваны Query.join(), Query.outerjoin(), Query.select_from() или Query.from_self(). Это неподдерживаемые случаи использования, которые в серии 0.9 вплоть до версии 0.9.10 не вызывают никаких ошибок и выдают предупреждение. В версии 1.0 эти случаи вызывают исключение.

#3349

query.update() с synchronize_session='evaluate' поднимается при обновлении нескольких таблиц

«Оценщик» для Query.update() не работает при многотабличных обновлениях, и при наличии нескольких таблиц необходимо установить значение synchronize_session=False или synchronize_session='fetch'. Новое поведение заключается в том, что теперь вызывается явное исключение с сообщением о необходимости изменить настройку синхронизации. Это обновлено по сравнению с предупреждением, выдаваемым в версии 0.9.7.

#3117

Событие воскрешения было удалено

Событие ORM «resurrect» было полностью удалено. Это событие перестало выполнять какую-либо функцию после того, как в версии 0.8 из единицы работы была удалена старая «мутабельная» система.

Изменение критерия наследования одной таблицы при использовании from_self(), count()

Дано однотабличное отображение наследования, например:

class Widget(Base):
    __table__ = "widget_table"


class FooWidget(Widget):
    pass

Использование Query.from_self() или Query.count() против подкласса приведет к созданию подзапроса, но затем добавит критерии «WHERE» для подтипов во внешний запрос:

sess.query(FooWidget).from_self().all()

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

SELECT
    anon_1.widgets_id AS anon_1_widgets_id,
    anon_1.widgets_type AS anon_1_widgets_type
FROM (SELECT widgets.id AS widgets_id, widgets.type AS widgets_type,
FROM widgets) AS anon_1
WHERE anon_1.widgets_type IN (?)

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

SELECT
    anon_1.widgets_id AS anon_1_widgets_id,
    anon_1.widgets_type AS anon_1_widgets_type
FROM (SELECT widgets.id AS widgets_id, widgets.type AS widgets_type,
FROM widgets
WHERE widgets.type IN (?)) AS anon_1

Так что запросы, не содержащие «type», все равно будут работать!:

sess.query(FooWidget.id).count()

Рендеры:

SELECT count(*) AS count_1
FROM (SELECT widgets.id AS widgets_id
FROM widgets
WHERE widgets.type IN (?)) AS anon_1

#3177

Критерии наследования одной таблицы добавлены во все предложения ON безусловно

При соединении с целевым подклассом наследования с одной таблицей ORM всегда добавляет «критерий одной таблицы» при соединении по отношению. Если задано отображение as:

class Widget(Base):
    __tablename__ = "widget"
    id = Column(Integer, primary_key=True)
    type = Column(String)
    related_id = Column(ForeignKey("related.id"))
    related = relationship("Related", backref="widget")
    __mapper_args__ = {"polymorphic_on": type}


class FooWidget(Widget):
    __mapper_args__ = {"polymorphic_identity": "foo"}


class Related(Base):
    __tablename__ = "related"
    id = Column(Integer, primary_key=True)

Уже довольно долгое время поведение заключается в том, что JOIN на отношении приводит к появлению условия «единственного наследования» для типа:

s.query(Related).join(FooWidget, Related.widget).all()

Вывод SQL:

SELECT related.id AS related_id
FROM related JOIN widget ON related.id = widget.related_id AND widget.type IN (:type_1)

Выше, поскольку мы присоединились к подклассу FooWidget, Query.join() знал, что нужно добавить критерии AND widget.type IN ('foo') в предложение ON.

Изменение заключается в том, что критерий AND widget.type IN() теперь добавляется к любому предложению ON, а не только к тем, которые генерируются на основе отношения, в том числе и к тем, которые явно указаны:

# ON clause will now render as
# related.id = widget.related_id AND widget.type IN (:type_1)
s.query(Related).join(FooWidget, FooWidget.related_id == Related.id).all()

А также «неявное» присоединение, когда не указывается никакая формула ON:

# ON clause will now render as
# related.id = widget.related_id AND widget.type IN (:type_1)
s.query(Related).join(FooWidget).all()

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

#3222

Ключевые изменения в поведении - Основные

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

С момента создания SQLAlchemy особое внимание уделялось тому, чтобы не мешать использованию обычного текста. Системы выражений Core и ORM должны были обеспечить возможность использования любого количества точек, в которых пользователь может просто использовать обычные текстовые SQL-выражения, не только в том смысле, что вы можете послать полную SQL-строку в Connection.execute(), но и в том, что вы можете послать строки с SQL-выражениями во многие функции, такие как Select.where(), Query.filter() и Select.order_by().

Обратите внимание, что под «SQL-выражениями» мы понимаем полный фрагмент SQL-строки, например:

# the argument sent to where() is a full SQL expression
stmt = select([sometable]).where("somecolumn = 'value'")

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

# This is a normal Core expression with a string argument -
# we aren't talking about this!!
stmt = select([sometable]).where(sometable.c.somecolumn == "value")

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

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

stmt = select(["a", "b"]).where("a = b").select_from("sometable")

Высказывание строится нормально, с теми же принуждениями, что и раньше. Однако при этом выдаются следующие предупреждения:

SAWarning: Textual column expression 'a' should be explicitly declared
with text('a'), or use column('a') for more specificity
(this warning may be suppressed after 10 occurrences)

SAWarning: Textual column expression 'b' should be explicitly declared
with text('b'), or use column('b') for more specificity
(this warning may be suppressed after 10 occurrences)

SAWarning: Textual SQL expression 'a = b' should be explicitly declared
as text('a = b') (this warning may be suppressed after 10 occurrences)

SAWarning: Textual SQL FROM expression 'sometable' should be explicitly
declared as text('sometable'), or use table('sometable') for more
specificity (this warning may be suppressed after 10 occurrences)

Эти предупреждения пытаются показать, где именно возникла проблема, отображая параметры и место получения строки. В предупреждениях используется Session.get_bind() обрабатывает более широкий спектр сценариев наследования для того, чтобы параметризованные предупреждения могли выдаваться безопасно, не исчерпывая памяти, и, как всегда, если необходимо, чтобы предупреждения были исключениями, следует использовать Python Warnings Filter:

import warnings

warnings.simplefilter("error")  # all warnings raise an exception

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

from sqlalchemy import select, text

stmt = (
    select([text("a"), text("b")]).where(text("a = b")).select_from(text("sometable"))
)

и, как следует из предупреждений, мы можем придать нашему высказыванию большую конкретность относительно текста, если будем использовать column() и table():

from sqlalchemy import select, text, column, table

stmt = (
    select([column("a"), column("b")])
    .where(text("a = b"))
    .select_from(table("sometable"))
)

Отметим также, что table() и column() теперь могут быть импортированы из «sqlalchemy» без части «sql».

Приведенное здесь поведение применимо как к select(), так и к ключевым методам на Query, включая Query.filter(), Query.from_statement() и Query.having().

ORDER BY и GROUP BY являются частными случаями

Существует один случай, когда использование строки имеет особое значение, и в рамках этого изменения мы расширили ее функциональность. Когда у нас есть select() или Query, которые ссылаются на имя некоторого столбца или именованную метку, мы можем захотеть выполнить GROUP BY и/или ORDER BY по известным столбцам или меткам:

stmt = (
    select([user.c.name, func.count(user.c.id).label("id_count")])
    .group_by("name")
    .order_by("id_count")
)

В приведенном выше выражении мы ожидаем увидеть «ORDER BY id_count», а не повторную формулировку функции. Приведенный строковый аргумент активно сопоставляется с записью в предложении columns во время компиляции, поэтому приведенное выше выражение будет работать, как мы и ожидали, без предупреждений (хотя обратите внимание, что выражение "name" было преобразовано в users.name!)

SELECT users.name, count(users.id) AS id_count
FROM users GROUP BY users.name ORDER BY id_count

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

stmt = select([user.c.name, func.count(user.c.id).label("id_count")]).order_by(
    "some_label"
)

Выход делает то, что мы говорим, но опять же предупреждает нас:

SAWarning: Can't resolve label reference 'some_label'; converting to
text() (this warning may be suppressed after 10 occurrences)
SELECT users.name, count(users.id) AS id_count
FROM users ORDER BY some_label

Вышеописанное поведение применимо ко всем тем местам, где мы можем захотеть сослаться на так называемую «ссылку на метку»; ORDER BY и GROUP BY, а также в предложении OVER и предложении DISTINCT ON, которые ссылаются на столбцы (например, синтаксис PostgreSQL).

Мы по-прежнему можем задать любое произвольное выражение для ORDER BY или других, используя text():

stmt = select([users]).order_by(text("some special expression"))

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

#2992

Умолчания на стороне Python вызываются для каждого ряда в отдельности при использовании многозначной вставки

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

Эта функция была переработана таким образом, чтобы ее работа была более схожа со стилем вызова «executemany»:

import itertools

counter = itertools.count(1)
t = Table(
    "my_table",
    metadata,
    Column("id", Integer, default=lambda: next(counter)),
    Column("data", String),
)

conn.execute(
    t.insert().values(
        [
            {"data": "d1"},
            {"data": "d2"},
            {"data": "d3"},
        ]
    )
)

В приведенном примере, как и следовало ожидать, next(counter) будет вызываться для каждой строки отдельно:

INSERT INTO my_table (id, data) VALUES (?, ?), (?, ?), (?, ?)
(1, 'd1', 2, 'd2', 3, 'd3')

Ранее позиционный диалект не работал, так как для дополнительных позиций не создавалась привязка:

Incorrect number of bindings supplied. The current statement uses 6,
and there are 4 supplied.
[SQL: u'INSERT INTO my_table (id, data) VALUES (?, ?), (?, ?), (?, ?)']
[parameters: (1, 'd1', 'd2', 'd3')]

А при использовании «именованного» диалекта одно и то же значение «id» будет повторно использоваться в каждой строке (поэтому данное изменение является обратно несовместимым с системой, которая на это полагалась):

INSERT INTO my_table (id, data) VALUES (:id, :data_0), (:id, :data_1), (:id, :data_2)
-- {u'data_2': 'd3', u'data_1': 'd2', u'data_0': 'd1', 'id': 1}

Система также откажется вызывать значение по умолчанию «на стороне сервера» в виде inline-рендеринга SQL, поскольку нельзя гарантировать, что значение по умолчанию на стороне сервера совместимо с этим. Если в предложении VALUES для конкретного столбца отображается значение, то требуется значение со стороны Python; если опущенное значение ссылается только на умолчание со стороны сервера, то будет вызвано исключение:

t = Table(
    "my_table",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("data", String, server_default="some default"),
)

conn.execute(
    t.insert().values(
        [
            {"data": "d1"},
            {"data": "d2"},
            {},
        ]
    )
)

повысится:

sqlalchemy.exc.CompileError: INSERT value for column my_table.data is
explicitly rendered as a boundparameter in the VALUES clause; a
Python-side value or SQL expression is required

Ранее значение «d1» копировалось в значение третьей строки (но опять же, только в именованном формате!):

INSERT INTO my_table (data) VALUES (:data_0), (:data_1), (:data_0)
-- {u'data_1': 'd2', u'data_0': 'd1'}

#3288

Слушатели события не могут быть добавлены или удалены из бегущей строки этого события

Удаление слушателя события изнутри самого события привело бы к изменению элементов списка во время итерации, что привело бы к молчаливому срабатыванию все еще подключенных слушателей события. Чтобы предотвратить это и сохранить производительность, списки были заменены на collections.deque(), который не допускает никаких добавлений или удалений во время итерации, а вместо этого поднимает RuntimeError.

#3163

Конструкция INSERT…FROM SELECT теперь подразумевает inline=True.

Использование Insert.from_select() теперь подразумевает inline=True на insert(). Это позволяет исправить ошибку, когда конструкция INSERT…FROM SELECT случайно компилировалась как «неявный возврат» на поддерживающих бэкендах, что приводило к поломке в случае INSERT, вставляющего нулевые строки (поскольку неявный возврат ожидает строку), а также к произвольным возвращаемым данным в случае INSERT, вставляющего несколько строк (например, только первую строку из многих). Аналогичное изменение коснулось и INSERT..VALUES с несколькими наборами параметров: для этого оператора также больше не будет выдаваться неявный RETURNING. Поскольку обе эти конструкции имеют дело с переменным количеством строк, аксессор ResultProxy.inserted_primary_key не применяется. Ранее в документации было указано, что при использовании INSERT…FROM SELECT можно предпочесть inline=True, так как некоторые базы данных не поддерживают возврат и поэтому не могут выполнять «неявный» возврат, но в любом случае для INSERT…FROM SELECT не требуется неявный возврат. Для возврата переменного числа строк результата, если требуется вставить данные, следует использовать обычные явные Insert.returning().

#3169

autoload_with теперь подразумевает autoload=True

Для установки отражения Table можно передать только Table.autoload_with:

my_table = Table("my_table", metadata, autoload_with=some_engine)

#3027

Улучшения в обертке исключений DBAPI и событиях handle_error()

Обертывание исключений DBAPI в SQLAlchemy не происходило в случае, когда объект Connection был аннулирован, а затем при попытке повторного подключения возникала ошибка; данная проблема решена.

Кроме того, недавно добавленное событие ConnectionEvents.handle_error() теперь вызывается для ошибок, возникающих при первоначальном подключении, при повторном подключении, а также при использовании create_engine() в пользовательской функции подключения через create_engine.creator.

Объект ExceptionContext имеет новый член данных ExceptionContext.engine, который всегда будет ссылаться на используемый объект Engine в тех случаях, когда объект Connection недоступен (например, при первом подключении).

#3266

ForeignKeyConstraint.columns теперь является коллекцией столбцов (ColumnCollection)

Ранее ForeignKeyConstraint.columns представлял собой обычный список, содержащий либо строки, либо объекты Column, в зависимости от того, как был построен ForeignKeyConstraint и был ли он связан с таблицей. Теперь коллекция представляет собой ColumnCollection, и инициализируется только после того, как ForeignKeyConstraint ассоциируется с Table. Добавлен новый аксессор ForeignKeyConstraint.column_keys для безусловного возврата строковых ключей для локального набора столбцов независимо от того, как был построен объект и каково его текущее состояние.

Аксессор MetaData.sorted_tables является «детерминированным»

Сортировка таблиц, получаемая с помощью аксессора MetaData.sorted_tables, является «детерминированной»; порядок должен быть одинаковым во всех случаях, независимо от хеширования Python. Это достигается за счет того, что перед передачей таблиц топологическому алгоритму они сортируются по именам, которые сохраняют этот порядок в процессе итераций.

Обратите внимание, что это изменение пока не распространяется на порядок, применяемый при испускании MetaData.create_all() или MetaData.drop_all().

#3084

Константы null(), false() и true() больше не являются синглтонами

В версии 0.9 эти три константы были изменены так, чтобы возвращать значение «singleton»; к сожалению, это привело к тому, что запрос, подобный следующему, не отображается так, как ожидалось:

select([null(), null()])

рендеринг только SELECT NULL AS anon_1, так как две конструкции null() получались как один и тот же объект NULL, а модель SQLAlchemy’s Core основана на идентичности объектов для определения лексической значимости. Изменение в 0.9 не имело никакого значения, кроме желания сэкономить на накладных расходах на объекты; вообще говоря, неименованная конструкция должна оставаться лексически уникальной, чтобы ее метка была уникальной.

#3170

SQLite/Oracle имеют разные методы для сообщения имен временных таблиц/ представлений

Методы Inspector.get_table_names() и Inspector.get_view_names() в случае SQLite/Oracle также возвращали бы имена временных таблиц и представлений, что не предусмотрено ни одним другим диалектом (в случае MySQL, по крайней мере, это вообще невозможно). Эта логика вынесена в два новых метода Inspector.get_temp_table_names() и Inspector.get_temp_view_names().

Отметим, что отражение конкретной именованной временной таблицы или временного представления либо с помощью Table('name', autoload=True), либо с помощью методов типа Inspector.get_columns() продолжает работать для большинства, если не для всех диалектов. Для SQLite, в частности, исправлена ошибка отражения ограничений UNIQUE из временных таблиц, которая заключается в #3203.

#3204

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

Пересмотр правил создания/удаления типов ENUM

Правила для PostgreSQL ENUM стали более строгими в отношении создания и удаления TYPE.

Объект ENUM, который создается без того, чтобы быть явно связанным с объектом MetaData, будет создан и сброшен, что соответствует Table.create() и Table.drop():

table = Table(
    "sometable", metadata, Column("some_enum", ENUM("a", "b", "c", name="myenum"))
)

table.create(engine)  # will emit CREATE TYPE and CREATE TABLE
table.drop(engine)  # will emit DROP TABLE and DROP TYPE - new for 1.0

Это означает, что если во второй таблице также имеется перечисление с именем „myenum“, то описанная выше операция DROP будет неудачной. Для того чтобы приспособиться к случаю использования общего перечислимого типа, было улучшено поведение перечисления, связанного с метаданными.

Объект ENUM, который создается с тем, что он явно связан с объектом MetaData, не будет **создан или сброшен, соответствующий Table.create() и Table.drop(), за исключением Table.create(), вызываемого с флагом checkfirst=True:

my_enum = ENUM("a", "b", "c", name="myenum", metadata=metadata)

table = Table("sometable", metadata, Column("some_enum", my_enum))

# will fail: ENUM 'my_enum' does not exist
table.create(engine)

# will check for enum and emit CREATE TYPE
table.create(engine, checkfirst=True)

table.drop(engine)  # will emit DROP TABLE, *not* DROP TYPE

metadata.drop_all(engine)  # will emit DROP TYPE

metadata.create_all(engine)  # will emit CREATE TYPE

#3319

Новые опции таблиц PostgreSQL

Добавлена поддержка опций PG-таблиц TABLESPACE, ON COMMIT, WITH(OUT) OIDS и INHERITS при выводе DDL с помощью конструкции Table.

#2051

Новый метод get_enums() с диалектом PostgreSQL

Метод inspect() возвращает объект PGInspector в случае PostgreSQL, который включает новый метод PGInspector.get_enums(), возвращающий информацию обо всех доступных типах ENUM:

from sqlalchemy import inspect, create_engine

engine = create_engine("postgresql+psycopg2://host/dbname")
insp = inspect(engine)
print(insp.get_enums())

См.также

PGInspector.get_enums()

Диалект PostgreSQL отражает материализованные представления, внешние таблицы

Изменения следующие:

  • конструкция Table с autoload=True теперь будет соответствовать имени, существующему в базе данных в виде материализованного представления или внешней таблицы.

  • Inspector.get_view_names() будет возвращать имена простых и материализованных представлений.

  • Inspector.get_table_names() для PostgreSQL не меняется, он продолжает возвращать только имена простых таблиц.

  • Добавлен новый метод PGInspector.get_foreign_table_names(), который будет возвращать имена таблиц, специально помеченных как «иностранные» в таблицах схемы PostgreSQL.

Изменение в отражении включает добавление 'm' и 'f' в список квалификаторов, используемых при запросе pg_class.relkind, но это изменение является новым в версии 1.0.0, чтобы избежать каких-либо сюрпризов, связанных с обратной несовместимостью, для тех, кто использует версию 0.9 в производстве.

#2891

PostgreSQL has_table() теперь работает для временных таблиц

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

from sqlalchemy import *

metadata = MetaData()
user_tmp = Table(
    "user_tmp",
    metadata,
    Column("id", INT, primary_key=True),
    Column("name", VARCHAR(50)),
    prefixes=["TEMPORARY"],
)

e = create_engine("postgresql://scott:tiger@localhost/test", echo="debug")
with e.begin() as conn:
    user_tmp.create(conn, checkfirst=True)

    # checkfirst will succeed
    user_tmp.create(conn, checkfirst=True)

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

from sqlalchemy import *

metadata = MetaData()
user_tmp = Table(
    "user_tmp",
    metadata,
    Column("id", INT, primary_key=True),
    Column("name", VARCHAR(50)),
    prefixes=["TEMPORARY"],
)

e = create_engine("postgresql://scott:tiger@localhost/test", echo="debug")
with e.begin() as conn:
    user_tmp.create(conn, checkfirst=True)

    m2 = MetaData()
    user = Table(
        "user_tmp",
        m2,
        Column("id", INT, primary_key=True),
        Column("name", VARCHAR(50)),
    )

    # in 0.9, *will create* the new table, overwriting the old one.
    # in 1.0, *will not create* the new table
    user.create(conn, checkfirst=True)

#3264

Ключевое слово PostgreSQL FILTER

Ключевое слово FILTER стандарта SQL для агрегатных функций теперь поддерживается в PostgreSQL, начиная с версии 9.4. SQLAlchemy позволяет это делать с помощью FunctionElement.filter():

func.count(1).filter(True)

Диалект PG8000 поддерживает кодирование на стороне клиента

Параметр create_engine.encoding теперь учитывается диалектом pg8000, используя обработчик соединения, который выдает SET CLIENT_ENCODING, соответствующий выбранной кодировке.

Встроенная поддержка JSONB в PG8000

Добавлена поддержка версий PG8000 выше 1.10.1, в которых нативно поддерживается JSONB.

Поддержка диалекта psycopg2cffi на PyPy

Добавлена поддержка диалекта pypy psycopg2cffi.

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

Тип MySQL TIMESTAMP теперь отображается как NULL / NOT NULL во всех случаях

Диалект MySQL всегда обходил неявное значение NOT NULL по умолчанию, связанное с колонками TIMESTAMP, выдавая NULL для такого типа, если колонка задана с помощью флага nullable=True. Однако в MySQL 5.6.6 и выше появился новый флаг explicit_defaults_for_timestamp, который исправляет нестандартное поведение MySQL, делая его похожим на поведение любого другого типа; чтобы приспособиться к этому, SQLAlchemy теперь выдает NULL/NOT NULL безусловно для всех столбцов TIMESTAMP.

#3155

MySQL SET Type Переработан для поддержки пустых наборов, юникода, обработки пустых значений

В типе SET исторически не было системы раздельной обработки пустых наборов и пустых значений; поскольку разные драйверы по-разному относятся к пустым строкам и представлениям пустых наборов строк, тип SET пытался только хеджировать между этими вариантами поведения, предпочитая рассматривать пустой набор как set(['']), как и сейчас принято в MySQL-Connector-Python DBAPI. Частично это объяснялось тем, что иначе невозможно хранить пустую строку в MySQL SET, поскольку драйвер возвращает нам строки, не имея возможности отличить set(['']) от set(). Пользователь должен был сам определить, означает ли set(['']) «пустое множество» или нет.

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

  • рассматривать пустую строку '', возвращаемую MySQL-python, как пустое множество set();

  • для преобразования однопустого набора значений set(['']), возвращаемого MySQL-Connector-Python, в пустой набор set();

  • Для обработки случая, когда тип set, по желанию пользователя, включает в список возможных значений пустое значение '', реализована новая возможность (необходимая в данном случае), при которой значение set сохраняется и загружается как целочисленное побитовое значение; для этого добавлен флаг SET.retrieve_as_bitwise.

Использование флага SET.retrieve_as_bitwise позволяет сохранять и извлекать набор без двусмысленности значений. Теоретически этот флаг можно включать во всех случаях, если заданный список значений типа в точности соответствует порядку, объявленному в базе данных; это лишь делает вывод SQL-эха несколько более необычным.

В остальном поведение по умолчанию SET остается прежним - обход значений с использованием строк. Поведение на основе строк теперь полностью поддерживает юникод, включая MySQL-python с use_unicode=0.

#3283

Внутренние исключения MySQL «нет такой таблицы» не передаются в обработчики событий

Диалект MySQL теперь будет отключать события ConnectionEvents.handle_error() для тех операторов, которые он использует для определения существования или отсутствия таблицы. Это достигается с помощью опции выполнения skip_user_error_events, которая отключает событие handle error для области данного выполнения. Таким образом, пользовательскому коду, переписывающему исключения, не нужно беспокоиться о диалекте MySQL или других диалектах, которые иногда должны перехватывать специфические для SQLAlchemy исключения.

Изменено значение по умолчанию raise_on_warnings для MySQL-Connector

Изменено значение по умолчанию параметра «raise_on_warnings» на False для MySQL-Connector. По какой-то причине это значение было установлено в True. Флаг «buffered», к сожалению, должен оставаться в значении True, так как MySQLconnector не позволяет закрыть курсор, пока все результаты не будут полностью получены.

#2515

Булевые символы MySQL «true», «false» снова работают

Переработка операторов IS/IS NOT, а также булевых типов в #2682 в версии 0.9 не позволила диалекту MySQL использовать символы «true» и «false» в контексте «IS» / «IS NOT». По-видимому, несмотря на то, что в MySQL нет типа «boolean», он поддерживает IS / IS NOT при использовании специальных символов «true» и «false», хотя в других случаях они являются синонимами «1» и «0» (и IS/IS NOT не работают с числовыми значениями).

Таким образом, изменение заключается в том, что диалект MySQL остается «non native boolean», но символы true() и false() снова выдают ключевые слова «true» и «false», так что выражение типа column.is_(true()) снова работает в MySQL.

#3186

Оператор match() теперь возвращает агностический MatchType, совместимый с возвращаемым значением MySQL с плавающей точкой

Типом возврата выражения ColumnOperators.match() теперь является новый тип MatchType. Это подкласс Boolean, который может быть перехвачен диалектом для получения другого типа результата во время выполнения SQL.

Теперь код, подобный следующему, будет работать корректно и возвращать плавающие точки на MySQL:

>>> connection.execute(
...     select(
...         [
...             matchtable.c.title.match("Agile Ruby Programming").label("ruby"),
...             matchtable.c.title.match("Dive Python").label("python"),
...             matchtable.c.title,
...         ]
...     ).order_by(matchtable.c.id)
... )
[
    (2.0, 0.0, 'Agile Web Development with Ruby On Rails'),
    (0.0, 2.0, 'Dive Into Python'),
    (2.0, 0.0, "Programming Matz's Ruby"),
    (0.0, 0.0, 'The Definitive Guide to Django'),
    (0.0, 1.0, 'Python in a Nutshell')
]

#3263

Диалект Drizzle теперь является внешним диалектом

Диалект для Drizzle теперь является внешним диалектом, доступным по адресу https://bitbucket.org/zzzeek/sqlalchemy-drizzle. Этот диалект был добавлен в SQLAlchemy непосредственно перед тем, как SQLAlchemy смогла хорошо приспособиться к сторонним диалектам; в дальнейшем все базы данных, не относящиеся к категории «повсеместного использования», являются сторонними диалектами. Реализация диалекта не изменилась и по-прежнему основана на диалектах MySQL + MySQLdb внутри SQLAlchemy. Диалект пока не выпущен и находится в статусе «чердака», однако он проходит большинство тестов и в целом находится в приличном рабочем состоянии, если кто-то захочет заняться его доводкой.

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

Именованные и неименованные ограничения SQLite UNIQUE и FOREIGN KEY будут проверять и отражать

Ограничения UNIQUE и FOREIGN KEY теперь полностью отражаются в SQLite как с именами, так и без них. Ранее имена внешних ключей игнорировались, а неименованные уникальные ограничения пропускались. В частности, это поможет при использовании новых возможностей Alembic по миграции на SQLite.

Для этого, как для внешних ключей, так и для уникальных ограничений, результат PRAGMA foreign_keys, index_list и index_info объединяется с разбором регулярных выражений оператора CREATE TABLE в целом для формирования полной картины имен ограничений, а также для различения ограничений UNIQUE, которые были созданы как UNIQUE, и неименованных INDEX’ов.

#3244

#3261

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

Имя драйвера PyODBC требуется при подключении к SQL Server на основе имени хоста

Подключение к SQL Server с помощью PyODBC без использования DSN-соединения, например, с явным именем хоста, теперь требует указания имени драйвера - SQLAlchemy больше не будет пытаться угадать имя по умолчанию:

engine = create_engine(
    "mssql+pyodbc://scott:tiger@myhost:port/databasename?driver=SQL+Server+Native+Client+10.0"
)

Принятое ранее в SQLAlchemy жесткое значение по умолчанию «SQL Server» устарело под Windows, и SQLAlchemy не может быть поставлена задача выбора лучшего драйвера на основе определения операционной системы/драйвера. Использование DSN всегда предпочтительнее при использовании ODBC, чтобы полностью избежать этой проблемы.

#3182

В SQL Server 2012 большие текстовые / двоичные типы отображаются как VARCHAR, NVARCHAR, VARBINARY

В SQL Server 2012 и выше изменено отображение типов TextClause, UnicodeText и LargeBinary, причем в соответствии с рекомендациями Microsoft об их устаревании можно полностью контролировать их поведение. Подробности см. в разделе Обесценивание больших текстовых/двоичных типов.

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

Улучшена поддержка CTE в Oracle

Поддержка CTE была исправлена для Oracle, а также появилась новая функция CTE.with_suffixes(), которая может помочь в работе со специальными директивами Oracle:

included_parts = (
    select([part.c.sub_part, part.c.part, part.c.quantity])
    .where(part.c.part == "p1")
    .cte(name="included_parts", recursive=True)
    .suffix_with(
        "search depth first by part set ord1",
        "cycle part set y_cycle to 1 default 0",
        dialect="oracle",
    )
)

#3220

Новые ключевые слова Oracle для DDL

Такие ключевые слова, как COMPRESS, ON COMMIT, BITMAP:

Параметры таблиц Oracle

Специфические опции индексов Oracle

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