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

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

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

Дата документа: 25 октября 2012 г. Обновлено: 9 марта 2013 г.

Введение

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

Релизы SQLAlchemy приближаются к версии 1.0, и в каждой новой версии, начиная с 0.5, происходит все меньше серьезных изменений в использовании. Большинство приложений, использующих современные шаблоны 0.7, должны быть перенесены на 0.8 без каких-либо изменений. Приложения, использующие шаблоны 0.6 и даже 0.5, также могут быть перенесены на 0.8, хотя более крупные приложения могут захотеть протестировать каждую промежуточную версию.

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

В настоящее время ориентирован на Python 2.5 и выше

SQLAlchemy 0.8 будет ориентирован на Python 2.5 и далее; совместимость с Python 2.4 отменяется.

Внутренние компоненты смогут использовать тернары Python (то есть x if y else z), что улучшит ситуацию по сравнению с использованием y and x or z, которое, естественно, является источником некоторых ошибок, а также менеджеры контекста (то есть with:) и, возможно, в некоторых случаях блоки try:/except:/else:, что улучшит читаемость кода.

Со временем SQLAlchemy откажется и от поддержки 2.5 - при достижении базовой версии 2.6 SQLAlchemy перейдет на использование совместимости 2.6/3.3 in-place, что позволит отказаться от использования инструмента 2to3 и поддерживать исходную базу, работающую с Python 2 и 3 одновременно.

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

Переписанная механика relationship()

В версии 0.8 значительно улучшена и усовершенствована система, определяющая способ соединения двух сущностей с помощью relationship(). Новая система включает в себя следующие возможности:

  • Аргумент primaryjoin больше не нужен при построении relationship() в отношении класса, имеющего несколько путей внешних ключей к цели. Только аргумент foreign_keys необходим для указания тех столбцов, которые должны быть включены:

    class Parent(Base):
        __tablename__ = "parent"
        id = Column(Integer, primary_key=True)
        child_id_one = Column(Integer, ForeignKey("child.id"))
        child_id_two = Column(Integer, ForeignKey("child.id"))
    
        child_one = relationship("Child", foreign_keys=child_id_one)
        child_two = relationship("Child", foreign_keys=child_id_two)
    
    
    class Child(Base):
        __tablename__ = "child"
        id = Column(Integer, primary_key=True)
  • Теперь поддерживаются отношения с самореферентными составными внешними ключами, в которых столбец указывает сам на себя. Канонический случай выглядит следующим образом:

    class Folder(Base):
        __tablename__ = "folder"
        __table_args__ = (
            ForeignKeyConstraint(
                ["account_id", "parent_id"], ["folder.account_id", "folder.folder_id"]
            ),
        )
    
        account_id = Column(Integer, primary_key=True)
        folder_id = Column(Integer, primary_key=True)
        parent_id = Column(Integer)
        name = Column(String)
    
        parent_folder = relationship(
            "Folder", backref="child_folders", remote_side=[account_id, folder_id]
        )

    Выше Folder ссылается на своего родителя Folder, присоединяясь от account_id к себе, а parent_id к folder_id. Когда SQLAlchemy строит auto-соединение, она больше не может считать, что все столбцы на «удаленной» стороне являются алиасированными, а все столбцы на «локальной» стороне - нет - столбец account_id является на обеих сторонах. Поэтому механика внутренних отношений была полностью переписана для поддержки совершенно другой системы, в которой генерируются две копии account_id, каждая из которых содержит различные аннотации для определения их роли в операторе. Обратите внимание на условие присоединения в базовой ускоренной загрузке:

    SELECT
        folder.account_id AS folder_account_id,
        folder.folder_id AS folder_folder_id,
        folder.parent_id AS folder_parent_id,
        folder.name AS folder_name,
        folder_1.account_id AS folder_1_account_id,
        folder_1.folder_id AS folder_1_folder_id,
        folder_1.parent_id AS folder_1_parent_id,
        folder_1.name AS folder_1_name
    FROM folder
        LEFT OUTER JOIN folder AS folder_1
        ON
            folder_1.account_id = folder.account_id
            AND folder.folder_id = folder_1.parent_id
    
    WHERE folder.folder_id = ? AND folder.account_id = ?
  • Ранее сложные пользовательские условия присоединения, например, с использованием функций и/или CAST типов, теперь в большинстве случаев будут работать как ожидалось:

    class HostEntry(Base):
        __tablename__ = "host_entry"
    
        id = Column(Integer, primary_key=True)
        ip_address = Column(INET)
        content = Column(String(50))
    
        # relationship() using explicit foreign_keys, remote_side
        parent_host = relationship(
            "HostEntry",
            primaryjoin=ip_address == cast(content, INET),
            foreign_keys=content,
            remote_side=ip_address,
        )

    Новая механика relationship() использует концепцию SQLAlchemy, известную как annotations. Эти аннотации также доступны для кода приложения в явном виде через функции foreign() и remote(), либо как средство улучшения читаемости для расширенных конфигураций, либо для прямого внедрения точной конфигурации, минуя обычную эвристику join-inspection:

    from sqlalchemy.orm import foreign, remote
    
    
    class HostEntry(Base):
        __tablename__ = "host_entry"
    
        id = Column(Integer, primary_key=True)
        ip_address = Column(INET)
        content = Column(String(50))
    
        # relationship() using explicit foreign() and remote() annotations
        # in lieu of separate arguments
        parent_host = relationship(
            "HostEntry",
            primaryjoin=remote(ip_address) == cast(foreign(content), INET),
        )

См.также

Настройка способа присоединения отношений - заново переработанный раздел relationship(), в котором подробно описаны новейшие приемы настройки связанных атрибутов и доступа к коллекциям.

#1401 #610

Новая система проверки классов/объектов

Многие пользователи SQLAlchemy пишут системы, требующие возможности просмотра атрибутов сопоставленного класса, в том числе столбцов первичного ключа, объектных отношений, простых атрибутов и т.п., обычно для построения систем маршаллинга данных, таких как схемы преобразования JSON/XML и, конечно, многочисленные библиотеки форм.

Изначально точками интроспекции были модели Table и Column, которые имеют хорошо документированную систему. Хотя модели SQLAlchemy ORM также полностью интроспективны, это никогда не было полностью стабильной и поддерживаемой функцией, и пользователи, как правило, не имели четкого представления о том, как получить эту информацию.

В версии 0.8 для этого предусмотрен последовательный, стабильный и полностью документированный API, включающий систему проверки, которая работает с отображенными классами, экземплярами, атрибутами и другими конструкциями Core и ORM. Точкой входа в эту систему является функция уровня ядра inspect(). В большинстве случаев проверяемый объект уже является частью системы SQLAlchemy, например, Mapper, InstanceState, Inspector. В некоторых случаях были добавлены новые объекты, призванные обеспечить API проверки в определенных контекстах, например AliasedInsp и AttributeState.

Далее следует обзор некоторых ключевых возможностей:

>>> class User(Base):
...     __tablename__ = "user"
...     id = Column(Integer, primary_key=True)
...     name = Column(String)
...     name_syn = synonym(name)
...     addresses = relationship("Address")

>>> # universal entry point is inspect()
>>> b = inspect(User)

>>> # b in this case is the Mapper
>>> b
<Mapper at 0x101521950; User>

>>> # Column namespace
>>> b.columns.id
Column('id', Integer(), table=<user>, primary_key=True, nullable=False)

>>> # mapper's perspective of the primary key
>>> b.primary_key
(Column('id', Integer(), table=<user>, primary_key=True, nullable=False),)

>>> # MapperProperties available from .attrs
>>> b.attrs.keys()
['name_syn', 'addresses', 'id', 'name']

>>> # .column_attrs, .relationships, etc. filter this collection
>>> b.column_attrs.keys()
['id', 'name']

>>> list(b.relationships)
[<sqlalchemy.orm.properties.RelationshipProperty object at 0x1015212d0>]

>>> # they are also namespaces
>>> b.column_attrs.id
<sqlalchemy.orm.properties.ColumnProperty object at 0x101525090>

>>> b.relationships.addresses
<sqlalchemy.orm.properties.RelationshipProperty object at 0x1015212d0>

>>> # point inspect() at a mapped, class level attribute,
>>> # returns the attribute itself
>>> b = inspect(User.addresses)
>>> b
<sqlalchemy.orm.attributes.InstrumentedAttribute object at 0x101521fd0>

>>> # From here we can get the mapper:
>>> b.mapper
<Mapper at 0x101525810; Address>

>>> # the parent inspector, in this case a mapper
>>> b.parent
<Mapper at 0x101521950; User>

>>> # an expression
>>> print(b.expression)
{printsql}"user".id = address.user_id{stop}

>>> # inspect works on instances
>>> u1 = User(id=3, name="x")
>>> b = inspect(u1)

>>> # it returns the InstanceState
>>> b
<sqlalchemy.orm.state.InstanceState object at 0x10152bed0>

>>> # similar attrs accessor refers to the
>>> b.attrs.keys()
['id', 'name_syn', 'addresses', 'name']

>>> # attribute interface - from attrs, you get a state object
>>> b.attrs.id
<sqlalchemy.orm.state.AttributeState object at 0x10152bf90>

>>> # this object can give you, current value...
>>> b.attrs.id.value
3

>>> # ... current history
>>> b.attrs.id.history
History(added=[3], unchanged=(), deleted=())

>>> # InstanceState can also provide session state information
>>> # lets assume the object is persistent
>>> s = Session()
>>> s.add(u1)
>>> s.commit()

>>> # now we can get primary key identity, always
>>> # works in query.get()
>>> b.identity
(3,)

>>> # the mapper level key
>>> b.identity_key
(<class '__main__.User'>, (3,))

>>> # state within the session
>>> b.persistent, b.transient, b.deleted, b.detached
(True, False, False, False)

>>> # owning session
>>> b.session
<sqlalchemy.orm.session.Session object at 0x101701150>

#2208

Новая функция with_polymorphic(), может быть использована в любом месте

Метод Query.with_polymorphic() позволяет пользователю указать, какие таблицы должны присутствовать при запросе к объединенной таблице. К сожалению, этот метод неудобен и применяется только к первой сущности в списке, а в остальном имеет неудобное поведение как в использовании, так и во внутреннем устройстве. В конструкцию aliased() добавлено новое усовершенствование with_polymorphic(), позволяющее «псевдоним» любой сущности превратить в «полиморфную» версию самой себя, которую можно свободно использовать в любом месте:

from sqlalchemy.orm import with_polymorphic

palias = with_polymorphic(Person, [Engineer, Manager])
session.query(Company).join(palias, Company.employees).filter(
    or_(Engineer.language == "java", Manager.hair == "pointy")
)

См.также

Использование функции with_polymorphic() - обновлена документация по полиморфному контролю загрузки.

#2333

of_type() работает с alias(), with_polymorphic(), any(), has(), joinedload(), subqueryload(), contains_eager()

Метод PropComparator.of_type() используется для указания конкретного подтипа при построении SQL-выражений по relationship(), имеющего в качестве цели отображение polymorphic. Теперь этот метод можно использовать для указания любого количества целевых подтипов, объединив его с новой функцией with_polymorphic():

# use eager loading in conjunction with with_polymorphic targets
Job_P = with_polymorphic(Job, [SubJob, ExtraJob], aliased=True)
q = (
    s.query(DataContainer)
    .join(DataContainer.jobs.of_type(Job_P))
    .options(contains_eager(DataContainer.jobs.of_type(Job_P)))
)

Теперь метод одинаково хорошо работает в большинстве мест, где принимается обычный атрибут отношения, включая функции загрузчика joinedload(), subqueryload(), contains_eager() и методы сравнения PropComparator.any() и PropComparator.has():

# use eager loading in conjunction with with_polymorphic targets
Job_P = with_polymorphic(Job, [SubJob, ExtraJob], aliased=True)
q = (
    s.query(DataContainer)
    .join(DataContainer.jobs.of_type(Job_P))
    .options(contains_eager(DataContainer.jobs.of_type(Job_P)))
)

# pass subclasses to eager loads (implicitly applies with_polymorphic)
q = s.query(ParentThing).options(
    joinedload_all(ParentThing.container, DataContainer.jobs.of_type(SubJob))
)

# control self-referential aliasing with any()/has()
Job_A = aliased(Job)
q = (
    s.query(Job)
    .join(DataContainer.jobs)
    .filter(
        DataContainer.jobs.of_type(Job_A).any(
            and_(Job_A.id < Job.id, Job_A.type == "fred")
        )
    )
)

#2438 #1106

События могут быть применены к неотображаемым суперклассам

Теперь события отображения и экземпляра могут быть связаны с не отображенным суперклассом, при этом эти события будут распространяться на подклассы по мере отображения этих подклассов. При этом следует использовать флаг propagate=True. Эта возможность позволяет ассоциировать события с декларативным базовым классом:

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


@event.listens_for("load", Base, propagate=True)
def on_load(target, context):
    print("New instance loaded:", target)


# on_load() will be applied to SomeClass
class SomeClass(Base):
    __tablename__ = "sometable"

    # ...

#2585

Декларативное различие между модулями/пакетами

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

class Snack(Base):
    # ...

    peanuts = relationship(
        "nuts.Peanut", primaryjoin="nuts.Peanut.snack_id == Snack.id"
    )

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

#2338

Новая функция DeferredReflection в Declarative

Пример «отложенного отражения» был перенесен в поддерживаемую функцию в Declarative. Эта возможность позволяет строить декларативные сопоставленные классы только с метаданными Table, пока не будет вызван шаг prepare(), дающий Engine, с помощью которого можно полностью отразить все таблицы и установить реальные сопоставления. Система поддерживает переопределение столбцов, одиночное и совместное наследование, а также отдельные базы для каждого двигателя. Теперь полная декларативная конфигурация может быть создана на основе существующей таблицы, которая собирается при создании движка за один шаг:

class ReflectedOne(DeferredReflection, Base):
    __abstract__ = True


class ReflectedTwo(DeferredReflection, Base):
    __abstract__ = True


class MyClass(ReflectedOne):
    __tablename__ = "mytable"


class MyOtherClass(ReflectedOne):
    __tablename__ = "myothertable"


class YetAnotherClass(ReflectedTwo):
    __tablename__ = "yetanothertable"


ReflectedOne.prepare(engine_one)
ReflectedTwo.prepare(engine_two)

См.также

DeferredReflection

#2485

Классы ORM теперь принимаются в Core Constructs

Хотя SQL-выражения, используемые с Query.filter(), такие как User.id == 5, всегда были совместимы для использования с такими конструкциями ядра, как select(), сам отображаемый класс не распознавался при передаче в select(), Select.select_from() или Select.correlate(). Новая система регистрации SQL позволяет принимать сопоставленный класс в качестве предложения FROM внутри ядра:

from sqlalchemy import select

stmt = select([User]).where(User.id == 5)

Выше, сопоставленный класс User расширится до Table, которому сопоставлен User.

#2245

Query.update() поддерживает UPDATE..FROM

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

query(SomeEntity).filter(SomeEntity.id == SomeOtherEntity.id).filter(
    SomeOtherEntity.foo == "bar"
).update({"data": "x"})

В частности, поддерживается обновление сущностей с объединенным наследованием, если цель UPDATE является локальной для таблицы, по которой выполняется фильтрация, или если родительские и дочерние таблицы являются смешанными, то они объединяются явно в запросе. Ниже приводится Engineer как объединенный подкласс Person:

query(Engineer).filter(Person.id == Engineer.id).filter(
    Person.name == "dilbert"
).update({"engineer_data": "java"})

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

UPDATE engineer SET engineer_data='java' FROM person
WHERE person.id=engineer.id AND person.name='dilbert'

#2365

rollback() будет откатывать только «грязные» объекты из begin_nested().

Изменение в поведении, которое должно повысить эффективность работы пользователей, использующих SAVEPOINT через Session.begin_nested() - при rollback() истекает срок действия только тех объектов, которые стали грязными с момента последней промывки, остальная часть Session остается нетронутой. Это связано с тем, что ROLLBACK на SAVEPOINT не прерывает изоляцию содержащей транзакции, поэтому истечение срока действия не требуется, за исключением тех изменений, которые не были смыты в текущей транзакции.

#2452

Пример кэширования теперь использует dogpile.cache

В примере с кэшированием теперь используется dogpile.cache. Dogpile.cache - это переписанная часть кэширования Beaker, отличающаяся значительно более простой и быстрой работой, а также поддержкой распределенной блокировки.

Обратите внимание, что API SQLAlchemy, используемые в примере Dogpile, а также в предыдущем примере Beaker, несколько изменились, в частности, это изменение необходимо, как показано в примере Beaker:

--- examples/beaker_caching/caching_query.py
+++ examples/beaker_caching/caching_query.py
@@ -222,7 +222,8 @@

         """
         if query._current_path:
-            mapper, key = query._current_path[-2:]
+            mapper, prop = query._current_path[-2:]
+            key = prop.key

             for cls in mapper.class_.__mro__:
                 if (cls, key) in self._relationship_options:

#2589

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

Полностью расширяемая поддержка операторов на уровне типов в Core

В Core до сих пор не было никакой системы добавления поддержки новых операторов SQL к Column и другим конструкциям выражений, кроме метода ColumnOperators.op(), которого «достаточно» для того, чтобы все работало. В Core также никогда не было системы, позволяющей переопределять поведение существующих операторов. До сих пор единственным способом гибкого переопределения операторов был слой ORM, использующий column_property() с аргументом comparator_factory. Поэтому сторонние библиотеки, такие как GeoAlchemy, были вынуждены ориентироваться на ORM и использовать множество хаков для применения новых операций, а также для их корректного распространения.

Новая система операторов в Core добавляет единственный крючок, которого не хватало все это время, - ассоциацию новых и переопределенных операторов с типами. Поскольку, в конце концов, не столбец, оператор CAST или функция SQL в действительности определяют, какие операции присутствуют, а тип выражения. Детали реализации минимальны - к основному типу ColumnElement добавляется лишь несколько дополнительных методов, чтобы он обращался к своему объекту TypeEngine для получения необязательного набора операторов. Новые или измененные операции могут быть связаны с любым типом либо путем подклассификации существующего типа, либо с помощью TypeDecorator, либо «глобально», путем присоединения нового объекта Comparator к существующему классу типа.

Например, для добавления поддержки логарифмов в типы Numeric:

from sqlalchemy.types import Numeric
from sqlalchemy.sql import func


class CustomNumeric(Numeric):
    class comparator_factory(Numeric.Comparator):
        def log(self, other):
            return func.log(self.expr, other)

Новый тип можно использовать как любой другой тип:

data = Table(
    "data",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("x", CustomNumeric(10, 5)),
    Column("y", CustomNumeric(10, 5)),
)

stmt = select([data.c.x.log(data.c.y)]).where(data.c.x.log(2) < value)
print(conn.execute(stmt).fetchall())

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

#2547

Поддержка множественных значений (Multiple-VALUES) при вставке

Метод Insert.values() теперь поддерживает список словарей, что позволяет отобразить оператор multi-VALUES, например VALUES (<row1>), (<row2>), .... Это актуально только для бэкендов, поддерживающих данный синтаксис, включая PostgreSQL, SQLite и MySQL. Это не то же самое, что обычный стиль INSERT executemany(), который остается неизменным:

users.insert().values(
    [
        {"name": "some name"},
        {"name": "some other name"},
        {"name": "yet another name"},
    ]
)

См.также

Insert.values()

#2623

Типовые выражения

SQL-выражения теперь могут быть связаны с типами. Исторически сложилось так, что TypeEngine всегда позволял использовать функции на стороне Python, которые получали как связанные параметры, так и значения строк результатов, пропуская их через функцию преобразования на стороне Python по пути к базе данных/обратно. Новая возможность позволяет реализовать аналогичную функциональность, только на стороне базы данных:

from sqlalchemy.types import String
from sqlalchemy import func, Table, Column, MetaData


class LowerString(String):
    def bind_expression(self, bindvalue):
        return func.lower(bindvalue)

    def column_expression(self, col):
        return func.lower(col)


metadata = MetaData()
test_table = Table("test_table", metadata, Column("data", LowerString))

Выше тип LowerString определяет SQL-выражение, которое будет выдаваться всякий раз, когда столбец test_table.c.data будет отображаться в предложение columns оператора SELECT:

>>> print(select([test_table]).where(test_table.c.data == "HI"))
{printsql}SELECT lower(test_table.data) AS data
FROM test_table
WHERE test_table.data = lower(:data_1)

Эта возможность также активно используется в новом выпуске GeoAlchemy для встраивания выражений PostGIS в SQL на основе правил типов.

#1534

Система контроля состояния сердечника

Функция inspect(), введенная в Новая система проверки классов/объектов, применима и к ядру. Применяясь к Engine, она выдает объект Inspector:

from sqlalchemy import inspect
from sqlalchemy import create_engine

engine = create_engine("postgresql://scott:tiger@localhost/test")
insp = inspect(engine)
print(insp.get_table_names())

Она также может быть применена к любому ClauseElement, возвращающему сам ClauseElement, например Table, Column, Select и т.д. Это позволяет ему свободно работать между конструкциями Core и ORM.

Новый метод Select.correlate_except()

select() теперь имеет метод Select.correlate_except(), который указывает «коррелировать по всем пунктам FROM, кроме указанных». Он может быть использован для сценариев отображения, в которых связанный подзапрос должен коррелировать нормально, за исключением определенного целевого selectable:

class SnortEvent(Base):
    __tablename__ = "event"

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

    signatures = relationship("Signature", lazy=False)


class Signature(Base):
    __tablename__ = "signature"

    id = Column(Integer, primary_key=True)

    sig_count = column_property(
        select([func.count("*")])
        .where(SnortEvent.signature == id)
        .correlate_except(SnortEvent)
    )

См.также

Select.correlate_except()

Тип PostgreSQL HSTORE

Поддержка типа HSTORE в PostgreSQL теперь доступна в виде HSTORE. Этот тип отлично использует новую систему операторов, предоставляя полный набор операторов для типов HSTORE, включая индексный доступ, конкатенацию и методы контаминации, такие как comparator_factory.has_key(), comparator_factory.has_any() и comparator_factory.matrix():

from sqlalchemy.dialects.postgresql import HSTORE

data = Table(
    "data_table",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("hstore_data", HSTORE),
)

engine.execute(select([data.c.hstore_data["some_key"]])).scalar()

engine.execute(select([data.c.hstore_data.matrix()])).scalar()

См.также

HSTORE

hstore

#2606

Усовершенствованный тип PostgreSQL ARRAY

Тип ARRAY принимает необязательный аргумент «размерность», что привязывает его к фиксированному числу размерностей и значительно повышает эффективность при получении результатов:

# old way, still works since PG supports N-dimensions per row:
Column("my_array", postgresql.ARRAY(Integer))

# new way, will render ARRAY with correct number of [] in DDL,
# will process binds and results more efficiently as we don't need
# to guess how many levels deep to go
Column("my_array", postgresql.ARRAY(Integer, dimensions=2))

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

result = conn.execute(select([mytable.c.arraycol[2]]))

доступ к срезам в SELECT:

result = conn.execute(select([mytable.c.arraycol[2:4]]))

Обновление срезов в UPDATE:

conn.execute(mytable.update().values({mytable.c.arraycol[2:3]: [7, 8]}))

литералы отдельно стоящих массивов:

>>> from sqlalchemy.dialects import postgresql
>>> conn.scalar(select([postgresql.array([1, 2]) + postgresql.array([3, 4, 5])]))
[1, 2, 3, 4, 5]

конкатенация массивов, где внизу правая часть [4, 5, 6] принудительно преобразуется в литерал массива:

select([mytable.c.arraycol + [4, 5, 6]])

См.также

ARRAY

array

#2441

Новые, настраиваемые типы DATE, TIME для SQLite

SQLite не имеет встроенных типов DATE, TIME или DATETIME, и вместо этого обеспечивает некоторую поддержку хранения значений даты и времени в виде строк или целых чисел. В 0.8 типы даты и времени для SQLite были усовершенствованы и стали гораздо более настраиваемыми в отношении конкретного формата, включая необязательность части «микросекунды», а также практически все остальное.

Column("sometimestamp", sqlite.DATETIME(truncate_microseconds=True))
Column(
    "sometimestamp",
    sqlite.DATETIME(
        storage_format=(
            "%(year)04d%(month)02d%(day)02d"
            "%(hour)02d%(minute)02d%(second)02d%(microsecond)06d"
        ),
        regexp="(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{6})",
    ),
)
Column(
    "somedate",
    sqlite.DATE(
        storage_format="%(month)02d/%(day)02d/%(year)04d",
        regexp="(?P<month>\d+)/(?P<day>\d+)/(?P<year>\d+)",
    ),
)

Огромная благодарность Нейту Дубу за спринтерскую работу над этим проектом на Pycon 2012.

См.также

DATETIME

DATE

TIME

#2363

«COLLATE» поддерживается во всех диалектах; в частности, в MySQL, PostgreSQL, SQLite

Ключевое слово «collate», давно принятое в диалекте MySQL, теперь установлено для всех типов String и будет отображаться на любом бэкенде, в том числе при использовании таких функций, как MetaData.create_all() и cast():

>>> stmt = select([cast(sometable.c.somechar, String(20, collation="utf8"))])
>>> print(stmt)
{printsql}SELECT CAST(sometable.somechar AS VARCHAR(20) COLLATE "utf8") AS anon_1
FROM sometable

См.также

String

#2276

«Префиксы» теперь поддерживаются для update(), delete()

Для MySQL «префикс» может быть представлен в любой из этих конструкций. Например:

stmt = table.delete().prefix_with("LOW_PRIORITY", dialect="mysql")


stmt = table.update().prefix_with("LOW_PRIORITY", dialect="mysql")

Этот метод является новым в дополнение к тем, которые уже существовали на insert(), select() и Query.

См.также

Update.prefix_with()

Delete.prefix_with()

Insert.prefix_with()

Select.prefix_with()

Query.prefix_with()

#2431

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

Рассмотрение «сиротского» объекта стало более агрессивным

Это запоздалое добавление к серии 0.8, однако есть надежда, что новое поведение будет в целом более последовательным и интуитивно понятным в более широком спектре ситуаций. По крайней мере, с версии 0.4 ORM включает в себя поведение, при котором объект, находящийся в состоянии «ожидания», то есть связанный с Session, но еще не вставленный в базу данных, автоматически исключается из Session, когда он становится «сиротой», что означает, что он был деассоциирован с родительским объектом, который ссылается на него с delete-orphan каскадом на сконфигурированный relationship(). Такое поведение призвано примерно повторять поведение персистентного (то есть уже вставленного) объекта, когда ORM будет выдавать DELETE для таких объектов, ставших сиротами на основе перехвата событий отсоединения.

Изменение поведения касается объектов, на которые ссылаются несколько видов родителей, каждый из которых указывает delete-orphan; типичным примером является association object, соединяющий два других вида объектов по схеме «многие-ко-многим». Ранее поведение было таково, что ожидающий объект удалялся только тогда, когда он был деассоциирован со всеми своими родителями. В результате изменения поведения ожидающий объект удаляется, как только он будет удален из любого родительского объекта, с которым он был ранее связан. Такое поведение призвано более точно соответствовать поведению постоянных объектов, которые удаляются сразу же после того, как они отсоединяются от любого родителя.

Обоснование старого поведения восходит, по крайней мере, к версии 0.4 и, по сути, было защитным решением, призванным уменьшить путаницу, когда объект еще только создавался для INSERT. Но на самом деле объект в любом случае повторно ассоциируется с Session, как только он присоединяется к новому родителю.

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

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

Новое поведение позволяет работать следующему тестовому случаю:

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

Base = declarative_base()


class User(Base):
    __tablename__ = "user"
    id = Column(Integer, primary_key=True)
    name = Column(String(64))


class UserKeyword(Base):
    __tablename__ = "user_keyword"
    user_id = Column(Integer, ForeignKey("user.id"), primary_key=True)
    keyword_id = Column(Integer, ForeignKey("keyword.id"), primary_key=True)

    user = relationship(
        User, backref=backref("user_keywords", cascade="all, delete-orphan")
    )

    keyword = relationship(
        "Keyword", backref=backref("user_keywords", cascade="all, delete-orphan")
    )

    # uncomment this to enable the old behavior
    # __mapper_args__ = {"legacy_is_orphan": True}


class Keyword(Base):
    __tablename__ = "keyword"
    id = Column(Integer, primary_key=True)
    keyword = Column("keyword", String(64))


from sqlalchemy import create_engine
from sqlalchemy.orm import Session

# note we're using PostgreSQL to ensure that referential integrity
# is enforced, for demonstration purposes.
e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)

Base.metadata.drop_all(e)
Base.metadata.create_all(e)

session = Session(e)

u1 = User(name="u1")
k1 = Keyword(keyword="k1")

session.add_all([u1, k1])

uk1 = UserKeyword(keyword=k1, user=u1)

# previously, if session.flush() were called here,
# this operation would succeed, but if session.flush()
# were not called here, the operation fails with an
# integrity error.
# session.flush()
del u1.user_keywords[0]

session.commit()

#2655

Событие after_attach срабатывает после того, как элемент ассоциирован с сессией, а не до этого; событие before_attach добавлено

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

@event.listens_for(Session, "after_attach")
def after_attach(session, instance):
    assert instance in session

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

@event.listens_for(Session, "before_attach")
def before_attach(session, instance):
    instance.some_necessary_attribute = (
        session.query(Widget).filter_by(instance.widget_name).first()
    )

#2464

Запрос теперь автокоррелирует, как это делает select()

Ранее для того, чтобы столбец или подзапрос WHERE соотносился с родителем, необходимо было вызывать Query.correlate():

subq = (
    session.query(Entity.value)
    .filter(Entity.id == Parent.entity_id)
    .correlate(Parent)
    .as_scalar()
)
session.query(Parent).filter(subq == "some value")

Это было противоположно поведению обычной конструкции select(), которая по умолчанию предполагала автокорреляцию. Приведенное выше утверждение в 0.8 будет коррелировать автоматически:

subq = session.query(Entity.value).filter(Entity.id == Parent.entity_id).as_scalar()
session.query(Parent).filter(subq == "some value")

как и в select(), корреляция может быть отключена вызовом query.correlate(None) или установлена вручную путем передачи сущности, query.correlate(someentity).

#2179

Корреляция теперь всегда зависит от контекста

Чтобы обеспечить более широкое разнообразие сценариев корреляции, поведение операторов Select.correlate() и Query.correlate() несколько изменилось: оператор SELECT будет опускать «коррелированную» цель из предложения FROM только в том случае, если оператор действительно используется в этом контексте. Кроме того, теперь оператор SELECT, помещенный в качестве FROM во вложенный оператор SELECT, не может «коррелировать» (т.е. опускать) предложение FROM.

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

from sqlalchemy.sql import table, column, select

t1 = table("t1", column("x"))
t2 = table("t2", column("y"))
s = select([t1, t2]).correlate(t1)

print(s)

До этого изменения все вышеперечисленное возвращалось:

SELECT t1.x, t2.y FROM t2

что является некорректным SQL, поскольку «t1» не упоминается ни в одном предложении FROM.

Теперь, в отсутствие вложенного SELECT, он возвращается:

SELECT t1.x, t2.y FROM t1, t2

В пределах SELECT корреляция вступает в силу, как и ожидалось:

s2 = select([t1, t2]).where(t1.c.x == t2.c.y).where(t1.c.x == s)
print(s2)
SELECT t1.x, t2.y FROM t1, t2
WHERE t1.x = t2.y AND t1.x =
    (SELECT t1.x, t2.y FROM t2)

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

#2668

create_all() и drop_all() теперь будут воспринимать пустой список как таковой

Методы MetaData.create_all() и MetaData.drop_all() теперь будут принимать пустой список объектов Table и не будут выдавать сообщения CREATE или DROP. Ранее пустой список интерпретировался так же, как передача None для коллекции, и CREATE/DROP выдавались для всех элементов безусловно.

Это исправление ошибки, но некоторые приложения могли полагаться на прежнее поведение.

#2664

Исправлено таргетирование события InstrumentationEvents

В серии целевых событий InstrumentationEvents документировано, что события будут вызываться только в соответствии с реальным классом, переданным в качестве целевого. В версии 0.7 это было не так, и любой слушатель событий, примененный к InstrumentationEvents, вызывался для всех сопоставленных классов. В 0.8 была добавлена дополнительная логика, благодаря которой события будут вызываться только для тех классов, которые были переданы. Флаг propagate здесь по умолчанию установлен на True, поскольку события инструментария классов обычно используются для перехвата еще не созданных классов.

#2590

Больше нет магического приведения «=» к IN при сравнении с подзапросом в MS-SQL

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

scalar_subq = select([someothertable.c.id]).where(someothertable.c.data == "foo")
select([sometable]).where(sometable.c.id == scalar_subq)

SQL Server не допускает сравнения на равенство для скалярного SELECT, то есть «x = (SELECT something)». Диалект MSSQL преобразует это в IN. Однако то же самое произойдет и при сравнении типа «(SELECT something) = x», и в целом этот уровень догадок выходит за рамки возможностей SQLAlchemy, поэтому такое поведение удалено.

#2277

Исправлено поведение Session.is_modified()

Метод Session.is_modified() принимает аргумент passive, который в принципе не должен быть необходим, аргументом во всех случаях должно быть значение True - если оставить значение по умолчанию False, то это будет иметь эффект удара по базе данных и часто вызывать autoflush, что само по себе изменит результаты. В 0.8 аргумент passive не будет иметь никакого значения, и выгруженные атрибуты никогда не будут проверяться на наличие истории, поскольку по определению для выгруженного атрибута не может быть ожидаемого изменения состояния.

См.также

Session.is_modified()

#2320

Column.key соблюдается в Select.c атрибуте select() с Select.apply_labels()

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

s = select([table1]).apply_labels()
s.c.table1_col1
s.c.table1_col2

До версии 0.8, если у Column был другой Column.key, то этот ключ игнорировался, несовместимо с тем, когда Select.apply_labels() не использовался:

# before 0.8
table1 = Table("t1", metadata, Column("col1", Integer, key="column_one"))
s = select([table1])
s.c.column_one  # would be accessible like this
s.c.col1  # would raise AttributeError

s = select([table1]).apply_labels()
s.c.table1_column_one  # would raise AttributeError
s.c.table1_col1  # would be accessible like this

В 0.8 в обоих случаях выполняется Column.key:

# with 0.8
table1 = Table("t1", metadata, Column("col1", Integer, key="column_one"))
s = select([table1])
s.c.column_one  # works
s.c.col1  # AttributeError

s = select([table1]).apply_labels()
s.c.table1_column_one  # works
s.c.table1_col1  # AttributeError

Все остальное поведение в отношении «name» и «key» остается прежним, включая то, что рендеринг SQL по-прежнему будет использовать форму <tablename>_<colname> - акцент здесь был сделан на предотвращении рендеринга содержимого Column.key в оператор SELECT, чтобы не возникало проблем со специальными/неаскетичными символами, используемыми в Column.key.

#2397

Предупреждение single_parent теперь является ошибкой

Отношение relationship(), которое является отношением многие-к-одному или многие-ко-многим и указывает «cascade=“all, delete-orphan“», что является неудобным, но тем не менее поддерживаемым случаем использования (с ограничениями), теперь будет выдавать ошибку, если в отношении не указана опция single_parent=True. Ранее выдавалось только предупреждение, но в любом случае в системе атрибутов практически сразу следовал сбой.

#2405

Добавление аргумента inspector к событию column_reflect

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

@event.listens_for(Table, "column_reflect")
def listen_for_col(inspector, table, column_info):
    ...

#2418

Отключение автоопределения коллизий, кеширования для MySQL

Диалект MySQL выполняет два вызова, один из которых очень дорогой, для загрузки всех возможных коллизий из базы данных, а также информации о кешировании, при первом подключении Engine. Ни одна из этих коллекций не используется ни в одной из функций SQLAlchemy, поэтому эти вызовы будут изменены и больше не будут выдаваться автоматически. Приложениям, которые могли полагаться на наличие этих коллекций в engine.dialect, придется обращаться к _detect_collations() и _detect_casing() напрямую.

#2404

Предупреждение «Неиспользуемые имена столбцов» становится исключением

Обращение к несуществующему столбцу в конструкции insert() или update() приведет к ошибке, а не к предупреждению:

t1 = table("t1", column("x"))
t1.insert().values(x=5, z=5)  # raises "Unconsumed column names: z"

#2415

Inspector.get_primary_keys() устарел, используйте Inspector.get_pk_constraint

Эти два метода на Inspector были избыточными, где get_primary_keys() возвращал ту же информацию, что и get_pk_constraint(), за вычетом имени ограничения:

>>> insp.get_primary_keys()
["a", "b"]

>>> insp.get_pk_constraint()
{"name":"pk_constraint", "constrained_columns":["a", "b"]}

#2422

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

Очень старое поведение, имена столбцов в RowProxy всегда сравнивались регистронезависимо:

>>> row = result.fetchone()
>>> row["foo"] == row["FOO"] == row["Foo"]
True

Это было сделано в интересах нескольких диалектов, которые в ранние годы нуждались в этом, например, Oracle и Firebird, но в современном использовании у нас есть более точные способы справиться с нечувствительным к регистру поведением этих двух платформ.

В дальнейшем такое поведение будет доступно только опционально, путем передачи флага `case_sensitive=False` в `create_engine()`, но в остальном имена столбцов, запрашиваемые из строки, должны совпадать по регистру.

#2423

InstrumentationManager и инструментарий альтернативных классов теперь является расширением

Класс sqlalchemy.orm.interfaces.InstrumentationManager переведен в sqlalchemy.ext.instrumentation.InstrumentationManager. Система «альтернативных приборов» была создана для очень небольшого числа установок, которым требовалось работать с существующими или необычными системами приборов класса, и в целом используется крайне редко. Сложность этой системы была экспортирована в модуль ext.. Он остается неиспользуемым до тех пор, пока не будет импортирован, как правило, когда сторонняя библиотека импортирует InstrumentationManager, и тогда он вводится обратно в sqlalchemy.orm, заменяя стандартный InstrumentationFactory на ExtendedInstrumentationRegistry.

Удалено

SQLSoup

SQLSoup - это удобный пакет, представляющий собой альтернативный интерфейс поверх ORM SQLAlchemy. SQLSoup теперь вынесен в отдельный проект и документирован/выпущен отдельно; см. https://bitbucket.org/zzzeek/sqlsoup.

SQLSoup - это очень простой инструмент, который также мог бы выиграть от участия в проекте тех, кто заинтересован в его стиле использования.

#2262

MutableType

Старая «мутабельная» система в SQLAlchemy ORM была удалена. Речь идет об интерфейсе MutableType, который применялся к таким типам, как PickleType и условно к TypeDecorator, и с самых ранних версий SQLAlchemy обеспечивал ORM способ обнаружения изменений в так называемых «мутабельных» структурах данных, таких как JSON-структуры и pickled-объекты. Однако его реализация никогда не была разумной и навязывала единицам работы очень неэффективный режим использования, который приводил к дорогостоящему сканированию всех объектов при флеше. В версии 0.7 было введено расширение sqlalchemy.ext.mutable, позволяющее определяемым пользователем типам данных соответствующим образом посылать события в единицу работы при их изменении.

Сегодня использование MutableType ожидаемо невелико, поскольку уже несколько лет существуют предупреждения о его неэффективности.

#2442

sqlalchemy.exceptions (в течение многих лет был sqlalchemy.exc)

Мы оставили псевдоним sqlalchemy.exceptions, чтобы облегчить работу некоторых старых библиотек, которые еще не были обновлены до sqlalchemy.exc. Однако некоторые пользователи все еще путаются в нем, поэтому в 0.8 мы полностью убираем его, чтобы устранить путаницу.

#2433

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