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

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

В данном документе описаны изменения между SQLAlchemy версии 1.1 и SQLAlchemy версии 1.2.

Введение

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

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

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

Ориентация на Python 2.7 и выше

В SQLAlchemy 1.2 минимальная версия Python переведена на 2.7 и больше не поддерживается 2.6. Ожидается, что в серию 1.2 будут включены новые возможности языка, которые не поддерживались в Python 2.6. Что касается поддержки Python 3, то в настоящее время SQLAlchemy тестируется на версиях 3.5 и 3.6.

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

«Запеченная» загрузка теперь используется по умолчанию для ленивых загрузок

Расширение sqlalchemy.ext.baked, впервые появившееся в серии 1. 0, позволяет построить так называемый объект BakedQuery, который представляет собой объект, генерирующий объект Query совместно с кэш-ключом, представляющим структуру запроса; Этот кэш-ключ затем связывается с результирующим строковым SQL-оператором, так что последующее использование другого BakedQuery с той же структурой минует все накладные расходы на создание объекта Query, построение основного объекта select() внутри него, а также компиляцию select() в строку, что позволяет избавиться от большинства накладных расходов на вызов функций, обычно связанных с созданием и выдачей объекта ORM Query.

Теперь BakedQuery по умолчанию используется ORM при формировании «ленивого» запроса для ленивой загрузки конструкции relationship(), например, для стратегии загрузчика отношений lazy="select" по умолчанию. Это позволит значительно сократить количество вызовов функций в рамках использования приложением запросов «ленивой» загрузки коллекций и связанных объектов. Ранее в версиях 1.0 и 1.1 эта возможность была доступна через использование глобального метода API или с помощью стратегии baked_select, теперь же это единственная реализация для такого поведения. Кроме того, функция была улучшена таким образом, что кэширование по-прежнему может происходить для объектов, у которых после ленивой загрузки действуют дополнительные опции загрузчика.

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

#3954

Новая ускоренная загрузка «selectin», загружает все коллекции сразу, используя IN

Добавлена новая загрузка «selectin», во многом похожая на загрузку «subquery», но создающая более простой SQL-запрос, который можно кэшировать, а также более эффективный.

Ниже приведен запрос:

q = (
    session.query(User)
    .filter(User.name.like("%ed%"))
    .options(subqueryload(User.addresses))
)

SQL будет представлять собой запрос к User, а затем подзапрос к User.addresses (обратите внимание, что параметры также перечислены):

SELECT users.id AS users_id, users.name AS users_name
FROM users
WHERE users.name LIKE ?
('%ed%',)

SELECT addresses.id AS addresses_id,
       addresses.user_id AS addresses_user_id,
       addresses.email_address AS addresses_email_address,
       anon_1.users_id AS anon_1_users_id
FROM (SELECT users.id AS users_id
FROM users
WHERE users.name LIKE ?) AS anon_1
JOIN addresses ON anon_1.users_id = addresses.user_id
ORDER BY anon_1.users_id
('%ed%',)

При «селективной» загрузке вместо этого мы получаем SELECT, который ссылается на фактические значения первичного ключа, загруженные в родительский запрос:

q = (
    session.query(User)
    .filter(User.name.like("%ed%"))
    .options(selectinload(User.addresses))
)

Производит:

SELECT users.id AS users_id, users.name AS users_name
FROM users
WHERE users.name LIKE ?
('%ed%',)

SELECT users_1.id AS users_1_id,
       addresses.id AS addresses_id,
       addresses.user_id AS addresses_user_id,
       addresses.email_address AS addresses_email_address
FROM users AS users_1
JOIN addresses ON users_1.id = addresses.user_id
WHERE users_1.id IN (?, ?)
ORDER BY users_1.id
(1, 3)

Приведенный выше оператор SELECT обладает такими преимуществами:

  • В нем не используется подзапрос, только INNER JOIN, что означает, что он будет работать намного лучше в базе данных типа MySQL, которая не любит подзапросы

  • Его структура не зависит от исходного запроса; в сочетании с новым expanding IN parameter system мы можем в большинстве случаев использовать «запеченный» запрос для кэширования строкового SQL, значительно снижая накладные расходы на каждый запрос

  • Поскольку запрос выполняет поиск только по заданному списку идентификаторов первичных ключей, «селективная» загрузка потенциально совместима с Query.yield_per() для одновременной работы с фрагментами результата SELECT при условии, что драйвер базы данных позволяет использовать несколько одновременных курсоров (SQLite, PostgreSQL; не драйверы MySQL или драйверы SQL Server ODBC). Ни объединенная ускоренная загрузка, ни ускоренная загрузка подзапросов не совместимы с Query.yield_per().

Недостатком ускоренной загрузки селектинов являются потенциально большие SQL-запросы с большими списками IN-параметров. Сам список IN-параметров разбивается на группы по 500, поэтому в результирующем наборе из более чем 500 ведущих объектов будет больше дополнительных запросов «SELECT IN». Кроме того, поддержка составных первичных ключей зависит от возможности базы данных использовать кортежи с IN, например, (table.column_one, table_column_two) IN ((?, ?), (?, ?) (?, ?)). В настоящее время известно, что PostgreSQL и MySQL совместимы с этим синтаксисом, SQLite - нет.

#3944

Полиморфная загрузка «selectin», загрузка подклассов с помощью отдельных IN-запросов

Аналогично функции загрузки отношений «selectin», только что описанной в Новая ускоренная загрузка «selectin», загружает все коллекции сразу, используя IN, существует полиморфная загрузка «selectin». Это полиморфная загрузка, предназначенная в первую очередь для объединенной загрузки, которая позволяет загружать базовую сущность простым оператором SELECT, а атрибуты дополнительных подклассов загружаются дополнительными операторами SELECT:

>>> from sqlalchemy.orm import selectin_polymorphic

>>> query = session.query(Employee).options(
...     selectin_polymorphic(Employee, [Manager, Engineer])
... )

>>> query.all()
{execsql}SELECT
    employee.id AS employee_id,
    employee.name AS employee_name,
    employee.type AS employee_type
FROM employee
()

SELECT
    engineer.id AS engineer_id,
    employee.id AS employee_id,
    employee.type AS employee_type,
    engineer.engineer_name AS engineer_engineer_name
FROM employee JOIN engineer ON employee.id = engineer.id
WHERE employee.id IN (?, ?) ORDER BY employee.id
(1, 2)

SELECT
    manager.id AS manager_id,
    employee.id AS employee_id,
    employee.type AS employee_type,
    manager.manager_name AS manager_manager_name
FROM employee JOIN manager ON employee.id = manager.id
WHERE employee.id IN (?) ORDER BY employee.id
(3,)

#3948

Атрибуты ORM, которые могут принимать специальные SQL-выражения

Добавлен новый ORM-атрибут типа query_expression(), аналогичный deferred(), за исключением того, что его SQL-выражение определяется во время запроса с помощью новой опции with_expression(); если она не указана, то по умолчанию атрибут принимает значение None:

from sqlalchemy.orm import query_expression
from sqlalchemy.orm import with_expression


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

    # will be None normally...
    expr = query_expression()


# but let's give it x + y
a1 = session.query(A).options(with_expression(A.expr, A.x + A.y)).first()
print(a1.expr)

#3058

ORM Поддержка удаления нескольких таблиц

Метод ORM Query.delete() поддерживает многотабличные критерии для DELETE, представленные в Поддержка критериев нескольких таблиц для DELETE. Эта возможность работает так же, как и критерии множественных таблиц для UPDATE, впервые представленные в версии 0.8 и описанные в Query.update() поддерживает UPDATE..FROM.

Ниже мы выполняем DELETE для SomeEntity, добавляя предложение FROM (или эквивалентное, в зависимости от бэкенда) для SomeOtherEntity:

query(SomeEntity).filter(SomeEntity.id == SomeOtherEntity.id).filter(
    SomeOtherEntity.foo == "bar"
).delete()

#959

Поддержка массового обновления гибридов, композитов

Как гибридные атрибуты (например, sqlalchemy.ext.hybrid), так и составные атрибуты (Типы составных колонн) теперь поддерживают использование в предложении SET оператора UPDATE при использовании Query.update().

:meth:`.hybrid_property.update_expression`F

class Person(Base):
    # ...

    first_name = Column(String(10))
    last_name = Column(String(10))

    @hybrid.hybrid_property
    def name(self):
        return self.first_name + " " + self.last_name

    @name.expression
    def name(cls):
        return func.concat(cls.first_name, " ", cls.last_name)

    @name.update_expression
    def name(cls, value):
        f, l = value.split(" ", 1)
        return [(cls.first_name, f), (cls.last_name, l)]

Выше, UPDATE может быть выведен с помощью:

session.query(Person).filter(Person.id == 5).update({Person.name: "Dr. No"})

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

session.query(Vertex).update({Edge.start: Point(3, 4)})

Гибридные атрибуты поддерживают повторное использование среди подклассов, переопределение @getter

sqlalchemy.ext.hybrid.hybrid_property @setter @expression @getter @property T

class FirstNameOnly(Base):
    # ...

    first_name = Column(String)

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

    @name.setter
    def name(self, value):
        self.first_name = value


class FirstNameLastName(FirstNameOnly):
    # ...

    last_name = Column(String)

    @FirstNameOnly.name.getter
    def name(self):
        return self.first_name + " " + self.last_name

    @name.setter
    def name(self, value):
        self.first_name, self.last_name = value.split(" ", maxsplit=1)

    @name.expression
    def name(cls):
        return func.concat(cls.first_name, " ", cls.last_name)

FirstNameOnly.name FirstNameLastName @getter @setter @expression @setter A

Примечание

@hybrid_property @hybrid_property @property T

class FirstNameOnly(Base):
    @hybrid_property
    def name(self):
        return self.first_name

    # WRONG - will raise AttributeError: can't set attribute when
    # assigning to .name
    @name.setter
    def _set_name(self, value):
        self.first_name = value


class FirstNameOnly(Base):
    @hybrid_property
    def name(self):
        return self.first_name

    # CORRECT - note regular Python @property works the same way
    @name.setter
    def name(self, value):
        self.first_name = value

#3911

#3912

Новое событие bulk_replace

Метод @validates получает все значения из набора массовых коллекций перед сравнением AttributeEvents.bulk_replace() AttributeEvents.append() AttributeEvents.remove() attributes.OP_BULK_REPLACE T

from sqlalchemy.orm.attributes import OP_BULK_REPLACE


@event.listens_for(SomeObject.collection, "bulk_replace")
def process_collection(target, values, initiator):
    values[:] = [_make_value(value) for value in values]


@event.listens_for(SomeObject.collection, "append", retval=True)
def process_collection(target, value, initiator):
    # make sure bulk_replace didn't already do it
    if initiator is None or initiator.op is not OP_BULK_REPLACE:
        return _make_value(value)
    else:
        return value

#3896

Новый обработчик события «изменено» для sqlalchemy.ext.mutable

AttributeEvents.modified() flag_modified() sqlalchemy.ext.mutable A

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy import event

Base = declarative_base()


class MyDataClass(Base):
    __tablename__ = "my_data"
    id = Column(Integer, primary_key=True)
    data = Column(MutableDict.as_mutable(JSONEncodedDict))


@event.listens_for(MyDataClass.data, "modified")
def modified_json(instance):
    print("json value modified:", instance.data)

``.data``A

#3303

Добавлены аргументы «для обновления» в Session.refresh

Session.refresh.with_for_update Session.refresh() Query.with_lockmode() Query.with_for_update() Session.refresh() A

session.refresh(some_object, with_for_update=True)

Session.refresh.with_for_update Query.with_for_update() T

session.refresh(some_objects, with_for_update={"read": True})

:paramref:`.Session.refresh.lockmode`T

#3991

Операторы мутации на месте работают для MutableSet, MutableList

__ior__ __iand__ __ixor__ __isub__ MutableSet __iadd__ MutableList I

model = session.query(MyModel).first()
model.json_set &= {1, 3}

#3853

AssociationProxy any(), has(), contains() работают с цепочками ассоциативных прокси

AssociationProxy.any() AssociationProxy.has() AssociationProxy.contains() AssociationProxy A.b_values AtoB.bvalue B T

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

    b_values = association_proxy("atob", "b_value")
    c_values = association_proxy("atob", "c_value")


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

    c = relationship("C")


class C(Base):
    __tablename__ = "c"
    id = Column(Integer, primary_key=True)
    b_id = Column(ForeignKey("b.id"))
    value = Column(String)


class AtoB(Base):
    __tablename__ = "atob"

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

    a = relationship("A", backref="atob")
    b = relationship("B", backref="atob")

    b_value = association_proxy("b", "value")
    c_value = association_proxy("b", "c")

A.b_values AssociationProxy.contains() A.b_values AtoB.b_value T

>>> s.query(A).filter(A.b_values.contains("hi")).all()
{execsql}SELECT a.id AS a_id
FROM a
WHERE EXISTS (SELECT 1
FROM atob
WHERE a.id = atob.a_id AND (EXISTS (SELECT 1
FROM b
WHERE b.id = atob.b_id AND b.value = :value_1)))

A.c_values AssociationProxy.any() A.c_values AtoB.c_value T

>>> s.query(A).filter(A.c_values.any(value="x")).all()
{execsql}SELECT a.id AS a_id
FROM a
WHERE EXISTS (SELECT 1
FROM atob
WHERE a.id = atob.a_id AND (EXISTS (SELECT 1
FROM b
WHERE b.id = atob.b_id AND (EXISTS (SELECT 1
FROM c
WHERE b.id = c.b_id AND c.value = :value_1)))))

#3769

Усовершенствования ключей идентификации для поддержки шардинга

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

Горизонтальное разделение WeatherLocation WeatherReport WeatherReport WeatherReport identity_token T

tokyo = WeatherLocation("Asia", "Tokyo")
newyork = WeatherLocation("North America", "New York")

tokyo.reports.append(Report(80.0))
newyork.reports.append(Report(75))

sess = create_session()

sess.add_all([tokyo, newyork, quito])

sess.commit()

# the Report class uses a simple integer primary key.  So across two
# databases, a primary key will be repeated.  The "identity_token" tracks
# in memory that these two identical primary keys are local to different
# databases.

newyork_report = newyork.reports[0]
tokyo_report = tokyo.reports[0]

assert inspect(newyork_report).identity_key == (Report, (1,), "north_america")
assert inspect(tokyo_report).identity_key == (Report, (1,), "asia")

# the token representing the originating shard is also available directly

assert inspect(newyork_report).identity_token == "north_america"
assert inspect(tokyo_report).identity_token == "asia"

#4137

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

Тип данных Boolean теперь обеспечивает строгие значения True/False/None

В версии 1.1 изменение, описанное в Неродные булевые целочисленные значения во всех случаях принудительно приводятся к нулю/одну/Нулю, привело к непреднамеренному побочному эффекту - изменению поведения Boolean при представлении нецелого значения, например, строки. В частности, строковое значение "0", которое ранее приводило к генерации значения False, теперь приводило к True. Что еще хуже, изменение поведения происходило только для некоторых бэкендов, но не для других, а значит, код, передающий строковые значения "0" в Boolean, будет работать непоследовательно для всех бэкендов.

Окончательное решение этой проблемы заключается в том, что строковые значения не поддерживаются в Boolean, поэтому в версии 1.2 при передаче нецелого значения / True/False/None вызывается жесткая ошибка TypeError. Кроме того, принимаются только целые значения 0 и 1.

Для приложений, желающих иметь более либеральную интерпретацию булевых значений, следует использовать TypeDecorator. Ниже приведен рецепт, позволяющий реализовать «либеральное» поведение типа данных Boolean, существовавшего до версии 1.1:

from sqlalchemy import Boolean
from sqlalchemy import TypeDecorator


class LiberalBoolean(TypeDecorator):
    impl = Boolean

    def process_bind_param(self, value, dialect):
        if value is not None:
            value = bool(int(value))
        return value

#4102

В пул соединений добавлено пессимистическое обнаружение разъединения

В документации по пулу соединений уже давно существует рецепт использования события движка ConnectionEvents.engine_connect() для выдачи простого оператора на проверяемое соединение с целью проверки его работоспособности. Теперь функциональность этого рецепта добавлена в сам пул соединений, если он используется в сочетании с соответствующим диалектом. С помощью нового параметра create_engine.pool_pre_ping каждое проверяемое соединение будет проверяться на свежесть перед возвратом:

engine = create_engine("mysql+pymysql://", pool_pre_ping=True)

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

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

#3919

Поведение оператора IN / NOT IN в отношении пустой коллекции теперь настраивается; упрощено выражение по умолчанию

Выражение column.in_([]), которое предполагается ложным, теперь по умолчанию выдает выражение 1 != 1, а не column != column. Это изменит результат запроса, сравнивающего SQL-выражение или столбец, который оценивается как NULL при сравнении с пустым множеством, выдавая вместо NULL булево значение false или true (для NOT IN). Предупреждение, которое выдавалось бы при этом условии, также удалено. Старое поведение доступно при использовании параметра create_engine.empty_in_strategy в create_engine().

В SQL операторы IN и NOT IN не поддерживают сравнение с коллекцией значений, которая явно пуста; то есть такой синтаксис является незаконным:

mycolumn IN ()

Чтобы обойти это, SQLAlchemy и другие библиотеки баз данных определяют это условие и выдают альтернативное выражение, которое оценивается как false или, в случае NOT IN, как true, основываясь на теории, что «col IN ()» всегда ложно, поскольку в «пустом множестве» ничего нет. Обычно для получения константы false/true, переносимой по базам данных и работающей в контексте предложения WHERE, используется простая тавтология, например 1 != 1 для оценки false и 1 = 1 для оценки true (простая константа «0» или «1» часто не работает в качестве цели предложения WHERE).

SQLAlchemy на заре своего существования также начинала с такого подхода, но вскоре выяснилось, что SQL-выражение column IN () не будет иметь значения false, если «столбец» будет NULL; вместо этого выражение будет выдавать NULL, поскольку «NULL» означает «неизвестно», а сравнение с NULL в SQL обычно выдает NULL.

Для имитации этого результата SQLAlchemy перешла от использования 1 != 1 к использованию выражения expr != expr для пустых «IN» и expr = expr для пустых «NOT IN»; то есть вместо фиксированного значения используется фактическая левая часть выражения. Если левая часть переданного выражения оценивается как NULL, то при сравнении в целом также получается результат NULL вместо false или true.

К сожалению, пользователи в итоге пожаловались, что это выражение очень сильно влияет на производительность некоторых планировщиков запросов. Тогда было добавлено предупреждение при встрече пустого выражения IN, в котором говорилось о том, что SQLAlchemy продолжает быть «правильной» и призывалось избегать кода, генерирующего пустые предикаты IN в целом, поскольку обычно их можно смело опускать. Однако это, конечно, обременительно для запросов, которые строятся динамически на основе входных переменных, где входящий набор значений может быть пустым.

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

SELECT NULL IN (SELECT 1 WHERE 1 != 1)

В приведенном тесте мы видим, что сами базы данных не могут прийти к единому мнению. PostgreSQL, которую большинство считает наиболее «правильной» базой данных, возвращает False, поскольку, хотя «NULL» и представляет собой «неизвестное», «пустое множество» означает, что ничего нет, включая все неизвестные значения. С другой стороны, MySQL и MariaDB возвращают NULL для приведенного выше выражения, по умолчанию применяя более распространенное поведение «все сравнения с NULL возвращают NULL».

ColumnOperators.in_() ColumnOperators.notin_() 1 != 1 expr != expr where(~null_expr.in_([])) S

create_engine.empty_in_strategy "static" "dynamic" "dynamic_warn" "dynamic_warn" expr != expr T

#3907

Расширенные в последнее время наборы параметров IN позволяют использовать IN-выражения с кэшированными утверждениями

bindparam() IN A

stmt = select([table]).where(table.c.col.in_(bindparam("foo", expanding=True)))
conn.execute(stmt, {"foo": [1, 2, 3]})

Данную функцию следует рассматривать как экспериментальную в рамках серии 1.2.

#3953

Уплощенный приоритет операторов для операторов сравнения

Приоритет операторов типа IN, LIKE, equals, IS, MATCH и других операторов сравнения выровнен до одного уровня. Это приведет к тому, что при объединении операторов сравнения, например:, будет создаваться больше скобок:

(column("q") == null()) != (column("y") == null())

(q IS NULL) != (y IS NULL) q IS NULL != y IS NULL W

#3999

Поддержка SQL-комментариев к таблице, столбцу, включает DDL, отражение

Table.comment Column.comment T

Table(
    "my_table",
    metadata,
    Column("q", Integer, comment="the Q value"),
    comment="my Q table",
)

Inspector.get_columns() Inspector.get_table_comment() A

В настоящее время поддерживаются такие бэкенды, как MySQL, PostgreSQL и Oracle.

#1546

Поддержка критериев нескольких таблиц для DELETE

:class:`_expression.Delete`T

Дано утверждение:

stmt = (
    users.delete()
    .where(users.c.id == addresses.c.id)
    .where(addresses.c.email_address.startswith("ed%"))
)
conn.execute(stmt)

Результирующий SQL из приведенного выше оператора на бэкенде PostgreSQL будет выглядеть так:

DELETE FROM users USING addresses
WHERE users.id = addresses.id
AND (addresses.email_address LIKE %(email_address_1)s || '%%')

#959

Новая опция «autoescape» для startswith(), endswith()

ColumnOperators.startswith() ColumnOperators.endswith() ColumnOperators.contains() True % _ / standard_confirming_strings NO_BACKSLASH_ESCAPES T

Примечание

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

Выражение типа:

>>> column("x").startswith("total%score", autoescape=True)

Выражается в виде:

x LIKE :x_1 || '%' ESCAPE '/'

``“total/%score“``W

Аналогично, выражение, содержащее обратные косые черты:

>>> column("x").startswith("total/score", autoescape=True)

``“total//score“``W

#2694

Для типов данных «float» добавлена более сильная типизация

Float Numeric Decimal() float A

  • Float Numeric Decimal() A

    float_value = connection.scalar(
        select([literal(4.56)])  # the "BindParameter" will now be
        # Float, not Numeric(asdecimal=True)
    )
  • Numeric Float Integer Numeric Float asdecimal Float M

    # asdecimal flag is maintained
    expr = column("a", Integer) * column("b", Numeric(asdecimal=False))
    assert expr.type.asdecimal == False
    
    # Float subclass of Numeric is maintained
    expr = column("a", Integer) * column("b", Float())
    assert isinstance(expr.type, Float)
  • Float float() Decimal() T

#4017

#4018

#4020

Поддержка ГРУППИРОВОЧНЫХ НАБОРОВ, КУБ, РОЛЛУП

:attr:`.func`A

>>> from sqlalchemy import select, table, column, func, tuple_
>>> t = table("t", column("value"), column("x"), column("y"), column("z"), column("q"))
>>> stmt = select([func.sum(t.c.value)]).group_by(
...     func.grouping_sets(
...         tuple_(t.c.x, t.c.y),
...         tuple_(t.c.z, t.c.q),
...     )
... )
>>> print(stmt)
{printsql}SELECT sum(t.value) AS sum_1
FROM t GROUP BY GROUPING SETS((t.x, t.y), (t.z, t.q))

#3429

Помощник параметров для многозначного INSERT с контекстным генератором по умолчанию

Контекстно-зависимые функции по умолчанию DefaultExecutionContext.current_parameters Insert Insert.values() DefaultExecutionContext.current_parameters DefaultExecutionContext.get_current_parameters() DefaultExecutionContext.get_current_parameters.isolate_multiinsert_groups True DefaultExecutionContext.current_parameters A

def mydefault(context):
    return context.get_current_parameters()["counter"] + 12


mytable = Table(
    "mytable",
    metadata_obj,
    Column("counter", Integer),
    Column("counter_plus_twelve", Integer, default=mydefault, onupdate=mydefault),
)

stmt = mytable.insert().values([{"counter": 5}, {"counter": 18}, {"counter": 20}])

conn.execute(stmt)

#4075

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

Событие сессии after_rollback() теперь выдается до истечения срока действия объектов

SessionEvents.after_rollback() SessionEvents.after_commit() T

sess = Session()

user = sess.query(User).filter_by(name="x").first()


@event.listens_for(sess, "after_rollback")
def after_rollback(session):
    # 'user.name' is now present, assuming it was already
    # loaded.  previously this would raise upon trying
    # to emit a lazy load.
    print("user name: %s" % user.name)


@event.listens_for(sess, "after_commit")
def after_commit(session):
    # 'user.name' is present, assuming it was already
    # loaded.  this is the existing behavior.
    print("user name: %s" % user.name)


if should_rollback:
    sess.rollback()
else:
    sess.commit()

:class:`.Session`N

#3934

``select_from()``F

:meth:`_query.Query.select_from`T

Manager Employee S

sess.query(Manager.id)

SQL будет сформирован как:

SELECT employee.id FROM employee WHERE employee.type IN ('manager')

Manager Query.select_from() H

sess.query(func.count(1)).select_from(Manager)

будет генерировать:

SELECT count(1) FROM employee

:meth:`_query.Query.select_from`W

SELECT count(1) FROM employee WHERE employee.type IN ('manager')

Приложениям, которые обходили эту проблему, добавляя предложение WHERE вручную, может потребоваться корректировка.

#3891

Предыдущая коллекция больше не мутирует при замене

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

>>> a1, a2, a3 = Address("a1"), Address("a2"), Address("a3")
>>> user.addresses = [a1, a2]

>>> previous_collection = user.addresses

# replace the collection with a new one
>>> user.addresses = [a2, a3]

>>> previous_collection
[Address('a1'), Address('a2')]

``previous_collection``A

#3913

Метод @validates получает все значения из набора массовых коллекций перед сравнением

``@validates``A

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

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

    @validates("bs")
    def convert_dict_to_b(self, key, value):
        return B(data=value["data"])


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

``B``A

a1 = A()
a1.bs.append({"data": "b1"})

``B``A

a1 = A()
a1.bs = [{"data": "b1"}]

AttributeEvents.bulk_replace() @validates A

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

class A(Base):
    # ...

    @validates("bs")
    def validate_b(self, key, value):
        assert value.data is not None
        return value

Выше, если мы начали с коллекции в виде:

a1 = A()

b1, b2 = B(data="one"), B(data="two")
a1.bs = [b1, b2]

А затем заменил коллекцию на ту, которая перекрывает первую:

b3 = B(data="three")
a1.bs = [b2, b3]

Ранее второе присваивание вызывало метод A.validate_b только один раз, для объекта b3. Объект b2 рассматривался как уже присутствующий в коллекции и не проверялся. При новом поведении и b2, и b3 передаются в A.validate_b перед передачей в коллекцию. Поэтому важно, чтобы методы валидации использовали идемпотентное поведение для такого случая.

#3896

Используйте функцию flag_dirty(), чтобы пометить объект как «грязный» без изменения атрибутов

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

a1 = A(data="adf")
s.add(a1)

s.flush()

# expire, similarly as though we said s.commit()
s.expire(a1, "data")

# will raise InvalidRequestError
attributes.flag_modified(a1, "data")

Это связано с тем, что процесс flush, скорее всего, не будет выполнен в любом случае, если атрибут не будет присутствовать к моменту выполнения flush. Чтобы пометить объект как «измененный», не обращаясь конкретно к какому-либо атрибуту, чтобы он учитывался в процессе flush для целей пользовательских обработчиков событий, таких как SessionEvents.before_flush(), используйте новую функцию flag_dirty():

from sqlalchemy.orm import attributes

attributes.flag_dirty(a1)

#3753

Ключевое слово «scope» удалено из scoped_session

Очень старый и недокументированный аргумент ключевого слова scope был удален:

from sqlalchemy.orm import scoped_session

Session = scoped_session(sessionmaker())

session = Session(scope=None)

Назначение этого ключевого слова заключалось в попытке обеспечить «область видимости» переменных, где None указывало на «отсутствие области видимости» и, следовательно, возвращало новое Session. Это ключевое слово никогда не было документировано и теперь при встрече с ним будет выдаваться сообщение TypeError. Не предполагается, что это ключевое слово будет использоваться, однако если пользователи сообщат о проблемах, связанных с ним, во время бета-тестирования, то оно может быть восстановлено с внесением поправок.

#3796

Уточнения к post_update в сочетании с onupdate

Отношения, использующие функцию relationship.post_update, теперь будут лучше взаимодействовать со столбцом, для которого задано значение Column.onupdate. Если вставляется объект с явным значением для столбца, то оно переставляется во время UPDATE, чтобы правило «onupdate» не перезаписало его:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    favorite_b_id = Column(ForeignKey("b.id", name="favorite_b_fk"))
    bs = relationship("B", primaryjoin="A.id == B.a_id")
    favorite_b = relationship(
        "B", primaryjoin="A.favorite_b_id == B.id", post_update=True
    )
    updated = Column(Integer, onupdate=my_onupdate_function)


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


a1 = A()
b1 = B()

a1.bs.append(b1)
a1.favorite_b = b1
a1.updated = 5
s.add(a1)
s.flush()

Выше было показано, что UPDATE будет происходить после INSERT, что приведет к срабатыванию функции «onupdate» и перезаписи значения «5». Теперь SQL выглядит следующим образом:

INSERT INTO a (favorite_b_id, updated) VALUES (?, ?)
(None, 5)
INSERT INTO b (a_id) VALUES (?)
(1,)
UPDATE a SET favorite_b_id=?, updated=? WHERE a.id = ?
(1, 5, 1)

Кроме того, если значение «updated» не установлено, то по событию a1.updated мы корректно получаем обратно только что сгенерированное значение; ранее логика, обновляющая атрибут для того, чтобы сгенерированное значение присутствовало, не срабатывала при обновлении. Событие InstanceEvents.refresh_flush() в этом случае также выдается при обновлении в режиме flush.

#3471

#3472

post_update интегрируется с версионностью ORM

Функция post_update, описанная в Строки, указывающие сами на себя / Взаимозависимые строки, предполагает, что в ответ на изменения определенного связанного с отношениями внешнего ключа, в дополнение к INSERT/UPDATE/DELETE, которые обычно выполняются для целевого ряда, выдается оператор UPDATE. Этот оператор UPDATE теперь участвует в функции версионирования, которая описана в Настройка счетчика версий.

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

class Node(Base):
    __tablename__ = "node"
    id = Column(Integer, primary_key=True)
    version_id = Column(Integer, default=0)
    parent_id = Column(ForeignKey("node.id"))
    favorite_node_id = Column(ForeignKey("node.id"))

    nodes = relationship("Node", primaryjoin=remote(parent_id) == id)
    favorite_node = relationship(
        "Node", primaryjoin=favorite_node_id == remote(id), post_update=True
    )

    __mapper_args__ = {"version_id_col": version_id}

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

node = Node()
session.add(node)
session.commit()  # node is now version #1

node = session.query(Node).get(node.id)
node.favorite_node = Node()
session.commit()  # node is now version #2

Обратите внимание, что это означает, что объект, получивший UPDATE в ответ на изменение других атрибутов и второй UPDATE в связи с изменением отношения post_update, теперь получит два обновления счетчика версий за один флэш. Однако если объект подвергся INSERT в рамках текущего флеша, то счетчик версий не будет увеличен в дополнительный раз, если только не используется схема версионирования на стороне сервера.

Причина, по которой post_update выдает UPDATE даже для UPDATE, теперь обсуждается в Почему post_update выдает UPDATE в дополнение к первому UPDATE?.

#3496

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

Поведение пользовательских операторов при вводе было приведено в соответствие

Операторы, определяемые пользователем, могут быть созданы «на лету» с помощью функции Operators.op(). Ранее поведение выражения при вводе такого оператора было непоследовательным и неконтролируемым.

В то время как в версии 1.1 выражение, подобное следующему, выдавало результат без типа возврата (предполагается, что -%> - это некоторый специальный оператор, поддерживаемый базой данных):

>>> column("x", types.DateTime).op("-%>")(None).type
NullType()

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

>>> column("x", types.String(50)).op("-%>")(None).type
String(length=50)

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

>>> column("x", types.DateTime).op("-%>")(None).type
DateTime()

Поскольку большинство определяемых пользователем операторов, как правило, являются операторами «сравнения», часто одним из многих специальных операторов, определенных PostgreSQL, флаг Operators.op.is_comparison был исправлен, чтобы соответствовать документированному поведению, позволяющему возвращать тип Boolean во всех случаях, в том числе для ARRAY и JSON:

>>> column("x", types.String(50)).op("-%>", is_comparison=True)(None).type
Boolean()
>>> column("x", types.ARRAY(types.Integer)).op("-%>", is_comparison=True)(None).type
Boolean()
>>> column("x", types.JSON()).op("-%>", is_comparison=True)(None).type
Boolean()

Для облегчения работы с булевыми операторами сравнения добавлен новый сокращенный метод Operators.bool_op(). Этот метод следует предпочесть для операторов булевых сравнений, выполняемых «на лету»:

>>> print(column("x", types.Integer).bool_op("-%>")(5))
{printsql}x -%> :x_1

Знаки процента в функции literal_column() теперь условно экранируются

Конструкция literal_column теперь экранирует символы знака процента условно, в зависимости от того, использует ли используемый DBAPI чувствительный к знаку процента параметр-стиль (например, „format“ или „pyformat“).

Ранее невозможно было создать конструкцию literal_column, содержащую один знак процента:

>>> from sqlalchemy import literal_column
>>> print(literal_column("some%symbol"))
{printsql}some%%symbol

Знак процента теперь не изменяется для диалектов, в которых не заданы параметры „format“ или „pyformat“; в таких диалектах, как большинство диалектов MySQL, в которых задан один из этих параметров, знак процента будет по-прежнему приводиться к соответствующему виду:

>>> from sqlalchemy import literal_column
>>> print(literal_column("some%symbol"))
{printsql}some%symbol{stop}
>>> from sqlalchemy.dialects import mysql
>>> print(literal_column("some%symbol").compile(dialect=mysql.dialect()))
{printsql}some%%symbol{stop}

В рамках этого изменения удвоение, имевшее место при использовании операторов ColumnOperators.contains(), ColumnOperators.startswith() и ColumnOperators.endswith(), также уточнено и теперь происходит только в тех случаях, когда это необходимо.

#3740

Ключевое слово COLLATE на уровне столбцов теперь заключает имя collation в кавычки

Исправлена ошибка в функциях collate() и ColumnOperators.collate(), используемых для предоставления специальных коллизий столбцов на уровне оператора, когда имя, чувствительное к регистру, не заключалось в кавычки:

stmt = select([mytable.c.x, mytable.c.y]).order_by(
    mytable.c.somecolumn.collate("fr_FR")
)

теперь отображается:

SELECT mytable.x, mytable.y,
FROM mytable ORDER BY mytable.somecolumn COLLATE "fr_FR"

Ранее имя «fr_FR», чувствительное к регистру, не заключалось в кавычки. В настоящее время ручное цитирование имени «fr_FR» не обнаруживается, поэтому приложения, которые вручную цитируют идентификатор, должны быть скорректированы. Обратите внимание, что данное изменение не влияет на использование коллизий на уровне типов (например, заданных на уровне типа данных, как String на уровне таблицы), где коллизии уже применяются.

#3785

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

Поддержка пакетного режима / помощников быстрого выполнения

Было выявлено, что метод psycopg2 cursor.executemany() плохо работает, особенно с операциями INSERT. Для облегчения этой проблемы в psycopg2 были добавлены помощники Fast Execution Helpers, которые перерабатывают операторы в меньшее количество обходов сервера путем пакетной отправки нескольких DML-операторов. В SQLAlchemy 1.2 появилась поддержка этих помощников, которые будут прозрачно использоваться всякий раз, когда Engine использует cursor.executemany() для вызова оператора с несколькими наборами параметров. По умолчанию эта возможность отключена и может быть включена с помощью аргумента use_batch_mode в команде create_engine():

engine = create_engine(
    "postgresql+psycopg2://scott:tiger@host/dbname", use_batch_mode=True
)

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

#4109

Поддержка спецификации полей в INTERVAL, включая полное отражение

Спецификатор «fields» в типе данных INTERVAL в PostgreSQL позволяет указать, какие поля интервала следует хранить, включая такие значения, как «YEAR», «MONTH», «YEAR TO MONTH» и т.д. Тип данных INTERVAL теперь позволяет указывать такие значения:

from sqlalchemy.dialects.postgresql import INTERVAL

Table("my_table", metadata, Column("some_interval", INTERVAL(fields="DAY TO SECOND")))

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

>>> inspect(engine).get_columns("my_table")
[{'comment': None,
  'name': u'some_interval', 'nullable': True,
  'default': None, 'autoincrement': False,
  'type': INTERVAL(fields=u'day to second')}]

#3959

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

Поддержка INSERT…ON DUPLICATE KEY UPDATE

Предложение ON DUPLICATE KEY UPDATE в INSERT, поддерживаемое MySQL, теперь поддерживается с помощью специфичной для MySQL версии объекта Insert, через sqlalchemy.dialects.mysql.dml.insert(). Этот подкласс Insert добавляет новый метод Insert.on_duplicate_key_update(), реализующий синтаксис MySQL:

from sqlalchemy.dialects.mysql import insert

insert_stmt = insert(my_table).values(id="some_id", data="some data to insert")

on_conflict_stmt = insert_stmt.on_duplicate_key_update(
    data=insert_stmt.inserted.data, status="U"
)

conn.execute(on_conflict_stmt)

Вышеуказанное будет отображаться:

INSERT INTO my_table (id, data)
VALUES (:id, :data)
ON DUPLICATE KEY UPDATE data=VALUES(data), status=:status_1

#4009

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

Значительный рефакторинг диалекта cx_Oracle, системы типизации

С появлением серии 6.x DBAPI cx_Oracle диалект SQLAlchemy cx_Oracle был переработан и упрощен, чтобы воспользоваться последними улучшениями в cx_Oracle, а также отказаться от поддержки паттернов, которые были более актуальны до появления серии 5.x cx_Oracle.

  • Минимальная поддерживаемая версия cx_Oracle теперь составляет 5.1.3; рекомендуется использовать 5.3 или последнюю серию 6.x.

  • Рефакторингу подверглась работа с типами данных. По рекомендации разработчиков cx_Oracle метод cursor.setinputsizes() больше не используется ни для каких типов данных, кроме LOB. В результате параметры auto_setinputsizes и exclude_setinputsizes являются устаревшими и больше не имеют никакого значения.

  • Флаг coerce_to_decimal, установленный в значение False, указывающее на то, что коэрцитивность числовых типов с точностью и масштабом до Decimal не должна иметь места, действует только на нетипизированные (например, простые строки без объектов TypeEngine) выражения. Выражение Core, включающее тип или подтип Numeric, теперь будет следовать правилам десятичного принуждения, характерным для этого типа.

  • Поддержка «двухфазных» транзакций в диалекте, уже отмененная в серии 6.x cx_Oracle, теперь полностью удалена, поскольку эта возможность никогда не работала корректно и вряд ли использовалась в производстве. В результате флаг диалекта allow_twophase устарел и также не имеет никакого значения.

  • Исправлена ошибка, связанная с ключами столбцов, присутствующими при RETURNING. Если оператор имеет следующий вид:

    result = conn.execute(table.insert().values(x=5).returning(table.c.a, table.c.b))

    Ранее ключами в каждой строке результата были ret_0 и ret_1, которые являются идентификаторами, внутренними для реализации cx_Oracle RETURNING. Теперь ключами будут a и b, как и положено для других диалектов.

  • Тип данных LOB в cx_Oracle представляет возвращаемые значения в виде объекта cx_Oracle.LOB, который представляет собой связанный с курсором прокси, возвращающий конечное значение данных через метод .read(). Исторически сложилось так, что если до того, как эти LOB-объекты были прочитаны, было прочитано больше строк (в частности, больше, чем значение cursor.arraysize, которое вызывает чтение новой порции строк), то эти LOB-объекты выдавали ошибку «LOB-переменная больше не действительна после последующей выборки». SQLAlchemy обходила эту проблему, как автоматически вызывая .read() для этих LOB в своей системе типизации, так и используя специальный BufferedColumnResultSet, который обеспечивал буферизацию этих данных в случае использования вызовов типа cursor.fetchmany() или cursor.fetchall().

    Теперь для обработки этих вызовов .read() в диалекте используется хендлер выходного типа cx_Oracle, который всегда вызывается заранее, независимо от того, сколько строк извлекается, так что эта ошибка больше не возникает. В результате было удалено использование BufferedColumnResultSet, а также некоторые другие внутренние компоненты Core ResultSet, которые были специфичны для данного случая использования. Объекты типа также упрощены, поскольку им больше не нужно обрабатывать результат двоичного столбца.

    Кроме того, в cx_Oracle 6.x устранены условия, при которых данная ошибка возникает в любом случае, поэтому ошибка больше невозможна. Ошибка может возникнуть на SQLAlchemy в том случае, если используется редко (если вообще используется) используемая опция auto_convert_lobs=False, в сочетании с предыдущей серией cx_Oracle 5.x, и считывается больше строк, чем успевают потребить LOB-объекты. Обновление до версии cx_Oracle 6.x позволит решить эту проблему.

Ограничения Oracle Unique, Check теперь отражаются

Ограничения UNIQUE и CHECK теперь отражаются через Inspector.get_unique_constraints() и Inspector.get_check_constraints(). Отраженный объект Table теперь будет включать в себя и объекты CheckConstraint. Информацию об особенностях поведения здесь см. в примечаниях Отражение ограничений, в том числе о том, что большинство объектов Table по-прежнему не будут включать объекты UniqueConstraint, поскольку они обычно отражаются через Index.

#4003

Имена ограничений внешнего ключа Oracle теперь «нормализованы по имени»

Имена ограничений внешнего ключа, передаваемые объекту ForeignKeyConstraint при отражении таблицы, а также в рамках метода Inspector.get_foreign_keys(), теперь будут «нормализованы по имени», то есть выражены в нижнем регистре для нечувствительного к регистру имени, а не в необработанном формате UPPERCASE, который использует Oracle:

>>> insp.get_indexes("addresses")
[{'unique': False, 'column_names': [u'user_id'],
  'name': u'address_idx', 'dialect_options': {}}]

>>> insp.get_pk_constraint("addresses")
{'name': u'pk_cons', 'constrained_columns': [u'id']}

>>> insp.get_foreign_keys("addresses")
[{'referred_table': u'users', 'referred_columns': [u'id'],
  'referred_schema': None, 'name': u'user_id_fk',
  'constrained_columns': [u'user_id']}]

Ранее результат работы с внешними ключами имел вид:

[
    {
        "referred_table": "users",
        "referred_columns": ["id"],
        "referred_schema": None,
        "name": "USER_ID_FK",
        "constrained_columns": ["user_id"],
    }
]

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

#3276

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

Поддерживаются имена схем SQL Server со встроенными точками

Диалект SQL Server ведет себя так, что имя схемы с точкой внутри воспринимается как пара идентификаторов «база данных», «владелец», которая обязательно разбивается на эти отдельные компоненты при операциях отражения таблиц и компонентов, а также при выводе кавычек для имени схемы, чтобы эти два символа были приведены отдельно. Теперь аргумент schema может быть передан с использованием скобок, чтобы вручную указать, где происходит это разделение, что позволяет использовать имена баз данных и/или владельцев, которые сами по себе содержат одну или несколько точек:

Table("some_table", metadata, Column("q", String(50)), schema="[MyDataBase.dbo]")

В приведенной выше таблице «владелец» будет считаться MyDataBase.dbo, который также будет заключен в кавычки при рендеринге, а «база данных» - None. Для индивидуальной ссылки на имя базы данных и владельца используйте две пары скобок:

Table(
    "some_table",
    metadata,
    Column("q", String(50)),
    schema="[MyDataBase.SomeDB].[MyDB.owner]",
)

Кроме того, конструкция quoted_name теперь учитывается при передаче в «схему» диалекта SQL Server; заданный символ не будет разделен на точки, если флаг кавычек равен True, и будет интерпретироваться как «владелец».

#2626

Поддержка уровня изоляции AUTOCOMMIT

Диалекты PyODBC и pymssql теперь поддерживают уровень изоляции «AUTOCOMMIT», задаваемый командой Connection.execution_options(), которая будет устанавливать правильные флаги на объекте соединения DBAPI.

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