Что нового в 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()
, в котором подробно описаны новейшие приемы настройки связанных атрибутов и доступа к коллекциям.
Новая система проверки классов/объектов¶
Многие пользователи 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>
См.также
Новая функция 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() - обновлена документация по полиморфному контролю загрузки.
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")
)
)
)
События могут быть применены к неотображаемым суперклассам¶
Теперь события отображения и экземпляра могут быть связаны с не отображенным суперклассом, при этом эти события будут распространяться на подклассы по мере отображения этих подклассов. При этом следует использовать флаг 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"
# ...
Декларативное различие между модулями/пакетами¶
Ключевой особенностью Declarative является возможность ссылаться на другие отображаемые классы, используя их строковое имя. Реестр имен классов теперь чувствителен к владельцу модуля и пакета данного класса. Ссылаться на классы можно через точечные имена в выражениях:
class Snack(Base):
# ...
peanuts = relationship(
"nuts.Peanut", primaryjoin="nuts.Peanut.snack_id == Snack.id"
)
При разрешении можно использовать любое полное или частичное имя пакета, не дающее однозначного толкования. Если путь к конкретному классу остается неоднозначным, то выдается ошибка.
Новая функция 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)
См.также
Классы 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
.
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'
rollback() будет откатывать только «грязные» объекты из begin_nested().¶
Изменение в поведении, которое должно повысить эффективность работы пользователей, использующих SAVEPOINT через Session.begin_nested()
- при rollback()
истекает срок действия только тех объектов, которые стали грязными с момента последней промывки, остальная часть Session
остается нетронутой. Это связано с тем, что ROLLBACK на SAVEPOINT не прерывает изоляцию содержащей транзакции, поэтому истечение срока действия не требуется, за исключением тех изменений, которые не были смыты в текущей транзакции.
Пример кэширования теперь использует 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:
См.также
Новые основные возможности¶
Полностью расширяемая поддержка операторов на уровне типов в 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. Кроме того, это открывает путь к тому, чтобы существующие типы обзавелись большим количеством операторов, характерных для этих типов, например, большим количеством операторов строк, целых чисел и дат.
Поддержка множественных значений (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"},
]
)
См.также
Типовые выражения¶
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 на основе правил типов.
Система контроля состояния сердечника¶
Функция 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)
)
См.также
Тип 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()
Усовершенствованный тип 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]])
Новые, настраиваемые типы 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.
«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
См.также
«Префиксы» теперь поддерживаются для 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()
Поведенческие изменения¶
Рассмотрение «сиротского» объекта стало более агрессивным¶
Это запоздалое добавление к серии 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()
Событие 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()
)
Запрос теперь автокоррелирует, как это делает 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)
.
Корреляция теперь всегда зависит от контекста¶
Чтобы обеспечить более широкое разнообразие сценариев корреляции, поведение операторов 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, используемого в некоррелированном контексте.
create_all() и drop_all() теперь будут воспринимать пустой список как таковой¶
Методы MetaData.create_all()
и MetaData.drop_all()
теперь будут принимать пустой список объектов Table
и не будут выдавать сообщения CREATE или DROP. Ранее пустой список интерпретировался так же, как передача None
для коллекции, и CREATE/DROP выдавались для всех элементов безусловно.
Это исправление ошибки, но некоторые приложения могли полагаться на прежнее поведение.
Исправлено таргетирование события InstrumentationEvents
¶
В серии целевых событий InstrumentationEvents
документировано, что события будут вызываться только в соответствии с реальным классом, переданным в качестве целевого. В версии 0.7 это было не так, и любой слушатель событий, примененный к InstrumentationEvents
, вызывался для всех сопоставленных классов. В 0.8 была добавлена дополнительная логика, благодаря которой события будут вызываться только для тех классов, которые были переданы. Флаг propagate
здесь по умолчанию установлен на True
, поскольку события инструментария классов обычно используются для перехвата еще не созданных классов.
Больше нет магического приведения «=» к 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, поэтому такое поведение удалено.
Исправлено поведение Session.is_modified()
¶
Метод Session.is_modified()
принимает аргумент passive
, который в принципе не должен быть необходим, аргументом во всех случаях должно быть значение True
- если оставить значение по умолчанию False
, то это будет иметь эффект удара по базе данных и часто вызывать autoflush, что само по себе изменит результаты. В 0.8 аргумент passive
не будет иметь никакого значения, и выгруженные атрибуты никогда не будут проверяться на наличие истории, поскольку по определению для выгруженного атрибута не может быть ожидаемого изменения состояния.
См.также
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
.
Предупреждение single_parent теперь является ошибкой¶
Отношение relationship()
, которое является отношением многие-к-одному или многие-ко-многим и указывает «cascade=“all, delete-orphan“», что является неудобным, но тем не менее поддерживаемым случаем использования (с ограничениями), теперь будет выдавать ошибку, если в отношении не указана опция single_parent=True
. Ранее выдавалось только предупреждение, но в любом случае в системе атрибутов практически сразу следовал сбой.
Добавление аргумента inspector
к событию column_reflect
¶
В версии 0.7 было добавлено новое событие column_reflect
, предназначенное для того, чтобы отражение колонок дополнялось по мере отражения каждой из них. Мы немного ошиблись с этим событием, поскольку оно не дает возможности получить текущие Inspector
и Connection
, используемые для отражения, на случай, если потребуется дополнительная информация из базы данных. Поскольку это новое событие, которое пока не используется широко, мы будем добавлять аргумент inspector
в него напрямую:
@event.listens_for(Table, "column_reflect")
def listen_for_col(inspector, table, column_info):
...
Отключение автоопределения коллизий, кеширования для MySQL¶
Диалект MySQL выполняет два вызова, один из которых очень дорогой, для загрузки всех возможных коллизий из базы данных, а также информации о кешировании, при первом подключении Engine
. Ни одна из этих коллекций не используется ни в одной из функций SQLAlchemy, поэтому эти вызовы будут изменены и больше не будут выдаваться автоматически. Приложениям, которые могли полагаться на наличие этих коллекций в engine.dialect
, придется обращаться к _detect_collations()
и _detect_casing()
напрямую.
Предупреждение «Неиспользуемые имена столбцов» становится исключением¶
Обращение к несуществующему столбцу в конструкции insert()
или update()
приведет к ошибке, а не к предупреждению:
t1 = table("t1", column("x"))
t1.insert().values(x=5, z=5) # raises "Unconsumed column names: z"
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"]}
Имена строк результатов, нечувствительные к регистру, в большинстве случаев будут отключены¶
Очень старое поведение, имена столбцов в RowProxy
всегда сравнивались регистронезависимо:
>>> row = result.fetchone()
>>> row["foo"] == row["FOO"] == row["Foo"]
True
Это было сделано в интересах нескольких диалектов, которые в ранние годы нуждались в этом, например, Oracle и Firebird, но в современном использовании у нас есть более точные способы справиться с нечувствительным к регистру поведением этих двух платформ.
В дальнейшем такое поведение будет доступно только опционально, путем передачи флага `case_sensitive=False`
в `create_engine()`
, но в остальном имена столбцов, запрашиваемые из строки, должны совпадать по регистру.
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 - это очень простой инструмент, который также мог бы выиграть от участия в проекте тех, кто заинтересован в его стиле использования.
MutableType¶
Старая «мутабельная» система в SQLAlchemy ORM была удалена. Речь идет об интерфейсе MutableType
, который применялся к таким типам, как PickleType
и условно к TypeDecorator
, и с самых ранних версий SQLAlchemy обеспечивал ORM способ обнаружения изменений в так называемых «мутабельных» структурах данных, таких как JSON-структуры и pickled-объекты. Однако его реализация никогда не была разумной и навязывала единицам работы очень неэффективный режим использования, который приводил к дорогостоящему сканированию всех объектов при флеше. В версии 0.7 было введено расширение sqlalchemy.ext.mutable, позволяющее определяемым пользователем типам данных соответствующим образом посылать события в единицу работы при их изменении.
Сегодня использование MutableType
ожидаемо невелико, поскольку уже несколько лет существуют предупреждения о его неэффективности.
sqlalchemy.exceptions (в течение многих лет был sqlalchemy.exc)¶
Мы оставили псевдоним sqlalchemy.exceptions
, чтобы облегчить работу некоторых старых библиотек, которые еще не были обновлены до sqlalchemy.exc
. Однако некоторые пользователи все еще путаются в нем, поэтому в 0.8 мы полностью убираем его, чтобы устранить путаницу.