Что нового в 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%',)

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

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

Недостатками нетерпеливой загрузки selectin являются потенциально большие 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() 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().

Для гибридов можно использовать простые выражения напрямую, или новый декоратор hybrid_property.update_expression() можно использовать для разбиения значения на несколько столбцов/выражений:

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)})

См.также

hybrid_bulk_update

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

Класс sqlalchemy.ext.hybrid.hybrid_property теперь поддерживает вызов мутаторов типа @setter, @expression и т.д. несколько раз в подклассах, и теперь предоставляет мутатор @getter, так что конкретный гибрид может быть повторно использован в подклассах или других классах. Теперь это похоже на поведение @property в стандартном Python:

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

Примечание

Обязательно прочитайте документацию по адресу hybrid_reuse_subclass для важных замечаний относительно того, как переопределить hybrid_property.expression() и hybrid_property.comparator(), поскольку в некоторых случаях может потребоваться специальный квалификатор hybrid_property.overrides, чтобы избежать конфликта имен с QueryableAttribute.

Примечание

Это изменение в @hybrid_property подразумевает, что при добавлении сеттеров и других состояний в @hybrid_property, методы должны сохранять имя исходного гибрида, иначе новый гибрид с дополнительным состоянием будет присутствовать в классе как не совпадающее имя. Это такое же поведение, как и у конструкции @property, которая является частью стандартного Python:

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 получает все значения из набора bulk-collection перед сравнением, добавлен новый метод AttributeEvents.bulk_replace(), который вызывается вместе с событиями AttributeEvents.append() и AttributeEvents.remove(). «bulk_replace» вызывается перед «append» и «remove», так что коллекция может быть изменена до сравнения с существующей коллекцией. После этого отдельные элементы добавляются в новую целевую коллекцию, вызывая событие «append» для новых элементов коллекции, как это было раньше. Ниже показано одновременное выполнение «bulk_replace» и «append», включая то, что «append» получит объект, уже обработанный «bulk_replace», если используется назначение коллекции. Новый символ attributes.OP_BULK_REPLACE может быть использован для определения того, является ли это событие «append» второй частью bulk replace:

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:

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.

#3303

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

Добавлен новый аргумент Session.refresh.with_for_update к методу Session.refresh(). Когда метод Query.with_lockmode() был упразднен в пользу Query.with_for_update(), метод Session.refresh() так и не был обновлен, чтобы отразить новую опцию:

session.refresh(some_object, with_for_update=True)

Аргумент Session.refresh.with_for_update принимает словарь опций, которые будут переданы в качестве тех же аргументов, которые передаются в Query.with_for_update():

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

Новый параметр заменяет параметр Session.refresh.lockmode.

#3991

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

Реализованы операторы мутации на месте __ior__, __iand__, __ixor__ и __isub__ для MutableSet и __iadd__ для MutableList. Хотя ранее эти методы успешно обновляли коллекцию, они некорректно вызывали события изменения. Операторы мутируют коллекцию, как и раньше, но дополнительно выдают правильное событие изменения, так что изменение становится частью следующего процесса flush:

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:

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:

>>> s.query(A).filter(A.b_values.contains("hi")).all()
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:

>>> s.query(A).filter(A.c_values.any(value="x")).all()
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 отслеживает это различие, так что два объекта могут сосуществовать в одной карте идентификации:

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 изменение, описанное в Non-native boolean integer values coerced to zero/one/None in all cases, привело к непреднамеренному побочному эффекту - изменению поведения 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 при сравнении с пустым множеством, выдавая булево значение false или true (для NOT IN) вместо NULL. Предупреждение, которое выдавалось бы при таком условии, также удалено. Старое поведение доступно при использовании параметра create_engine.empty_in_strategy в create_engine().

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

mycolumn IN ()

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

Архитектура SQL в SQLAlchemy стала более сложной, чем в то время, когда было принято это решение, поэтому теперь мы можем позволить вызывать любой из вариантов поведения во время компиляции SQL-строки. Ранее преобразование в выражение сравнения выполнялось во время построения, то есть в момент вызова операторов ColumnOperators.in_() или ColumnOperators.notin_(). С поведением во время компиляции, диалекту можно дать указание использовать любой из подходов, то есть «статическое» 1 != 1 сравнение или «динамическое» expr != expr сравнение. Значение по умолчанию было изменено на «статическое» сравнение, поскольку это соответствует поведению, которое PostgreSQL будет иметь в любом случае, и это также то, что предпочитает подавляющее большинство пользователей. Это изменит результат запроса, сравнивающего нулевое выражение с пустым множеством, в частности, запроса на отрицание where(~null_expr.in_([])), поскольку теперь оно оценивается как true, а не NULL.

Поведение теперь можно контролировать с помощью флага create_engine.empty_in_strategy, который по умолчанию имеет значение "static", но также может быть установлен на "dynamic" или "dynamic_warn", где значение "dynamic_warn" эквивалентно предыдущему поведению, выдавая expr != expr, а также предупреждение о производительности. Однако предполагается, что большинство пользователей оценят «статическое» значение по умолчанию.

#3907

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

Добавлен новый вид bindparam() под названием «расширяющийся». Он предназначен для использования в выражениях IN, где список элементов преобразуется в отдельные связанные параметры во время выполнения оператора, а не во время компиляции оператора. Это позволяет связать одно имя связанного параметра с IN-выражением из нескольких элементов, а также позволяет использовать кэширование запросов с IN-выражениями. Новая функция позволяет связанным функциям загрузки «select in» и загрузки «polymorphic in» использовать расширение запеченных запросов для снижения накладных расходов на вызов:

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.

#3999

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

В ядре появилась поддержка строковых комментариев, связанных с таблицами и столбцами. Они задаются с помощью аргументов Table.comment и Column.comment:

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

Выше, DDL будет соответствующим образом отображаться при создании таблицы, чтобы связать вышеуказанные комментарии с таблицей/столбцом в схеме. При автозагрузке или проверке вышеуказанной таблицы с помощью Inspector.get_columns() комментарии будут включены. Комментарий таблицы также доступен независимо с помощью метода Inspector.get_table_comment().

Текущая поддержка бэкендов включает MySQL, PostgreSQL и Oracle.

#1546

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

Конструкция Delete теперь поддерживает критерии множественных таблиц, реализованные для тех бэкендов, которые это поддерживают, в настоящее время это PostgreSQL, MySQL и Microsoft SQL Server (поддержка также добавлена для неработающего диалекта Sybase). Функция работает так же, как и критерии множественных таблиц для UPDATE, впервые представленные в сериях 0.7 и 0.8.

Учитывая утверждение:

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()

Параметр «autoescape» добавлен к ColumnOperators.startswith(), ColumnOperators.endswith(), ColumnOperators.contains(). При установке этого параметра в значение True все вхождения %, _ автоматически экранируются символом экранирования, который по умолчанию представляет собой прямую косую черту /; вхождения самого символа экранирования также экранируются. Передняя косая черта используется для того, чтобы избежать конфликтов с такими параметрами, как standard_confirming_strings в PostgreSQL, значение по умолчанию которого изменилось в PostgreSQL 9.1, и параметрами NO_BACKSLASH_ESCAPES в MySQL. Существующий параметр «escape» теперь может быть использован для изменения символа автоперевода, если это необходимо.

Примечание

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

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

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

Переводится как:

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

Где значение параметра «x_1» равно 'total/%score'.

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

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

Будет отображаться так же, со значением параметра «x_1» как 'total//score'.

#2694

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

Ряд изменений позволяет использовать тип данных Float для более тесной связи со значениями с плавающей точкой Python, вместо более общего Numeric. Изменения в основном связаны с обеспечением того, чтобы значения Python с плавающей точкой ошибочно не принуждались к Decimal(), а при необходимости принуждались к float на стороне результата, если приложение работает с обычными плавающими числами.

  • Обычное значение Python «float», переданное в выражение SQL, теперь будет подтягиваться в литеральный параметр с типом Float; ранее тип был Numeric, с флагом по умолчанию «asdecimal=True», что означало, что тип результата будет приведен к Decimal(). В частности, это приводило к непонятному предупреждению на SQLite:

    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:

    # 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() к значениям результата безоговорочно, если известно, что DBAPI поддерживает собственный режим Decimal(). Некоторые бэкенды не всегда гарантируют, что число с плавающей точкой возвращается как обычное float, а не как точное числовое значение, например, MySQL.

#4017

#4018

#4020

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

Все три функции GROUPING SETS, CUBE, ROLLUP доступны через пространство имен func. В случае с CUBE и ROLLUP эти функции уже работали в предыдущих версиях, однако для GROUPING SETS в компиляторе добавляется место для пробела. В документации все три функции теперь называются так:

>>> 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)
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, которая определяет несколько пунктов VALUES через метод Insert.values(), пользовательская функция вызывается несколько раз, по одному разу для каждого набора параметров, однако не было способа узнать, какое подмножество ключей в DefaultExecutionContext.current_parameters относится к данному столбцу. Добавлена новая функция DefaultExecutionContext.get_current_parameters(), которая включает аргумент ключевого слова DefaultExecutionContext.get_current_parameters.isolate_multiinsert_groups по умолчанию True, которая выполняет дополнительную работу по доставке подсловаря DefaultExecutionContext.current_parameters, содержащего имена, локализованные для текущего обрабатываемого предложения VALUES:

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(), которое также испускается до удаления «моментального снимка»:

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()

Обратите внимание, что Session все еще запрещает испускание SQL в рамках этого события; это означает, что незагруженные атрибуты все еще не смогут загружаться в рамках этого события.

#3934

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

Метод Query.select_from() теперь учитывает дискриминатор столбцов наследования одной таблицы при генерации SQL; ранее учитывались только выражения в списке столбцов запроса.

Предположим, что Manager является подклассом Employee. Запрос, подобный следующему:

sess.query(Manager.id)

Сгенерирует SQL как:

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

Однако, если бы Manager был указан только Query.select_from() и отсутствовал в списке колонок, дискриминатор не был бы добавлен:

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

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

SELECT count(1) FROM employee

После исправления Query.select_from() теперь работает правильно, и мы получаем:

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

#3913

Метод @validates получает все значения из набора bulk-collection перед сравнением

Метод, использующий @validates, теперь будет получать все члены коллекции во время операции «bulk set», прежде чем сравнение будет применено к существующей коллекции.

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

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 при добавлении коллекции:

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

Однако, присвоение коллекции будет неудачным, поскольку ORM будет считать, что входящие объекты уже являются экземплярами B, поскольку он пытается сравнить их с существующими членами коллекции, прежде чем выполнить добавление коллекции, которое фактически вызывает валидатор. Это сделало бы невозможным применение операций bulk set для работы с не-ORM объектами, такими как словари, которые нуждаются в предварительной модификации:

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

Новая логика использует новое событие AttributeEvents.bulk_replace(), чтобы гарантировать, что все значения будут отправлены в функцию @validates заранее.

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

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() также испускается, когда в этом случае происходит обновление или смывание.

#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))
x -%> :x_1

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

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

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

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

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

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

В рамках этого изменения удвоение, которое присутствовало при использовании таких операторов, как 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
)

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

См.также

psycopg2_batch_mode

#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

См.также

mysql_insert_on_duplicate_key_update

#4009

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

Крупный рефактор диалекта cx_Oracle, система типизации

С появлением 6.x серии cx_Oracle DBAPI, диалект 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().

    Теперь диалект использует cx_Oracle outputtypehandler для обработки этих вызовов .read(), так что они всегда вызываются заранее, независимо от того, сколько строк извлекается, так что эта ошибка больше не может возникнуть. В результате, использование 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. См. примечания в oracle_constraint_reflection для информации о поведенческих причудах здесь, включая то, что большинство Table объектов все еще не будут включать какие-либо UniqueConstraint объекты, поскольку они обычно отражаются через Index.

См.также

oracle_constraint_reflection

#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 ведет себя так, что имя схемы с точкой внутри воспринимается как пара идентификаторов «база данных», «владелец», которая обязательно разделяется на эти отдельные компоненты во время операций отражения таблиц и компонентов, а также при оформлении кавычек для имени схемы, чтобы эти два символа приводились отдельно. Теперь аргумент схемы может быть передан с использованием скобок, чтобы вручную указать, где происходит это разделение, что позволяет использовать имена базы данных и/или владельца, которые сами по себе содержат одну или несколько точек:

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 теперь соблюдается при передаче «schema» диалектом SQL Server; заданный символ не будет разделен на точки, если флаг кавычек равен True, и будет интерпретироваться как «владелец».

См.также

multipart_schema_names

#2626

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

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

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