Что нового в 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 строит автосоединение, он больше не может считать, что все столбцы на «удаленной» стороне алиасированы, а все столбцы на «локальной» стороне нет - столбец 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 = ?
  • Ранее сложные пользовательские условия присоединения, например, те, которые включают функции и/или CASTing типов, теперь будут работать так, как ожидается в большинстве случаев:

    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(), либо как средство улучшения читабельности для расширенных конфигураций, либо для прямого введения точной конфигурации, минуя обычную эвристику проверки соединений:

    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)
"user".id = address.user_id

>>> # 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:

См.также

dogpile_caching

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

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

#2547

Поддержка множественных значений для вставки

Метод 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"))
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

Поддержка типа PostgreSQL HSTORE теперь доступна в виде 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)
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, однако есть надежда, что новое поведение в целом будет более последовательным и интуитивно понятным в более широком спектре ситуаций. ORM, по крайней мере, с версии 0.4 включает поведение, при котором объект, который находится в «ожидании», то есть связан с 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 в качестве опции маппера.

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

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

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

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

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

#2262

MutableType

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

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

#2442

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

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

#2433

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