Что нового в SQLAlchemy 0.5?¶
О данном документе
В данном документе описаны изменения между версиями SQLAlchemy 0.4, выпущенной 12 октября 2008 года, и SQLAlchemy 0.5, выпущенной 16 января 2010 года.
Дата документа: 4 августа 2009 г.
В этом руководстве описаны изменения в API, которые затрагивают пользователей, переносящих свои приложения с SQLAlchemy серии 0.4 на 0.5. Оно также рекомендуется для тех, кто работает с Essential SQLAlchemy, который охватывает только 0.4 и, кажется, даже содержит некоторые старые 0.3. Обратите внимание, что в SQLAlchemy 0.5 удалены многие поведения, которые были устаревшими в течение всего периода существования серии 0.4, а также устаревшие поведения, характерные для 0.4.
Основные изменения в документации¶
Некоторые разделы документации были полностью переписаны и могут служить введением в новые возможности ORM. В частности, объекты Query
и Session
имеют некоторые отличия в API и поведении, которые в корне меняют многие базовые принципы работы, особенно в части построения специализированных ORM-запросов и работы с устаревшим состоянием сессии, фиксациями и откатами.
Изъяны Источник¶
Другой источник информации задокументирован в серии юнит-тестов, иллюстрирующих актуальное использование некоторых распространенных паттернов Query
; этот файл можно посмотреть по адресу [source:sqlalchemy/trunk/test/orm/test_deprecations.py].
Изменения в требованиях¶
Требуется Python 2.4 или выше. Линейка SQLAlchemy 0.4 является последней версией с поддержкой Python 2.3.
Объектно-реляционное отображение¶
Выражения на уровне столбцов в Query. - как подробно описано в tutorial,
Query
имеет возможность создавать конкретные операторы SELECT, а не только те, которые направлены на полные строки:session.query(User.name, func.count(Address.id).label("numaddresses")).join( Address ).group_by(User.name)
Кортежи, возвращаемые любым многоколоночным/субъектным запросом, являются именованными“ кортежами:
for row in ( session.query(User.name, func.count(Address.id).label("numaddresses")) .join(Address) .group_by(User.name) ): print("name", row.name, "number", row.numaddresses)
Query
имеет аксессорstatement
, а также методsubquery()
, которые позволяют использоватьQuery
для создания более сложных комбинаций:subq = ( session.query(Keyword.id.label("keyword_id")) .filter(Keyword.name.in_(["beans", "carrots"])) .subquery() ) recipes = session.query(Recipe).filter( exists() .where(Recipe.id == recipe_keywords.c.recipe_id) .where(recipe_keywords.c.keyword_id == subq.c.keyword_id) )
Явные ORM-псевдонимы рекомендуются для псевдосоединений - Функция
aliased()
создает «псевдоним» класса, что позволяет осуществлять тонкий контроль псевдонимов в сочетании с ORM-запросами. В то время как псевдоним на уровне таблицы (например,table.alias()
) все еще можно использовать, псевдоним на уровне ORM сохраняет семантику объекта, сопоставленного ORM, что важно для сопоставлений наследования, опций и других сценариев. Например:Friend = aliased(Person) session.query(Person, Friend).join((Friend, Person.friends)).all()
query.join() значительно расширен - Теперь можно указывать целевой класс и предложение ON для соединения несколькими способами. Можно указать только целевой класс, и SQLA попытается сформировать к нему присоединение по внешнему ключу аналогично
table.join(someothertable)
. Можно указать целевой класс и явное условие ON, где условием ON может быть имяrelation()
, дескриптор реального класса или выражение SQL. Также можно использовать и старый способ - просто имяrelation()
или дескриптор класса. Смотрите учебник по ORM, в котором есть несколько примеров.Declarative рекомендуется для приложений, которые не требуют (и не предпочитают) абстракции между таблицами и мапперами - Модуль [/docs/05/reference/ext/declarative.html Declarative], который используется для объединения выражения
Table
,mapper()
и объектов классов, определенных пользователем, настоятельно рекомендуется, поскольку он упрощает конфигурацию приложения, обеспечивает шаблон «один маппер на класс» и позволяет использовать весь спектр настроек, доступных для отдельных вызововmapper()
. Раздельное использованиеmapper()
иTable
в настоящее время называется «классическим использованием SQLAlchemy» и, конечно, свободно смешивается с декларативным.Атрибут .c. был удален из классов (т.е.
MyClass.c.somecolumn
). Как и в 0.4, свойства уровня класса могут использоваться в качестве элементов запроса, т.е.Class.c.propname
теперь заменяется наClass.propname
, а атрибутc
продолжает оставаться на объектахTable
, где указывает на пространство имен объектовColumn
, присутствующих в таблице.Чтобы получить таблицу для отображаемого класса (если вы еще не храните ее у себя):
table = class_mapper(someclass).mapped_table
Итерация по столбцам:
for col in table.c: print(col)
Работа с конкретным столбцом:
table.c.somecolumn
Дескрипторы, связанные с классами, поддерживают полный набор операторов Column, а также документированные операторы, ориентированные на отношения, такие как
has()
,any()
,contains()
и т.д.Причина жесткого удаления
.c.
заключается в том, что в 0.5 дескрипторы, привязанные к классам, несут потенциально иной смысл, а также информацию о сопоставлении классов, чем обычные объектыColumn
, и есть случаи, когда необходимо использовать то или другое. В общем случае использование дескрипторов, связанных с классами, вызывает набор трансляций, связанных с отображением/полиморфизмом, а использование столбцов, связанных с таблицами, - нет. В 0.4 эти трансляции применялись ко всем выражениям, но в 0.5 полностью разграничены столбцы и дескрипторы с отображением, и трансляции применяются только к последним. Поэтому во многих случаях, особенно при работе с конфигурациями наследования объединенных таблиц, а также при использованииquery(<columns>)
,Class.propname
иtable.c.colname
не являются взаимозаменяемыми.Например,
session.query(users.c.id, users.c.name)
отличается отsession.query(User.id, User.name)
; в последнем случаеQuery
знает об используемом мэппере и может использовать дальнейшие специфические для мэппера операции, такие какquery.join(<propname>)
,query.with_parent()
и т.д., а в первом случае - нет. Кроме того, в сценариях полиморфного наследования дескрипторы, связанные с классами, ссылаются на столбцы, присутствующие в используемом полиморфном селекторе, причем не обязательно на столбец таблицы, который непосредственно соответствует дескриптору. Например, набор классов, связанных наследованием по объединенной таблице с таблицейperson
по столбцуperson_id
каждой таблицы, будет иметь атрибутClass.person_id
, привязанный к столбцуperson_id
вperson
, а не к таблице их подклассов. В версии 0.4 это поведение автоматически отображалось на объектыColumn
, связанные с таблицами. В версии 0.5 это автоматическое преобразование было удалено, так что вы фактически можете использовать связанные с таблицей столбцы как средство отмены преобразований, которые происходят при полиморфном запросе; это позволяетQuery
создавать оптимизированные select’ы среди объединенных таблиц или конкретных таблиц наследования, а также переносимые подзапросы и т.д.Сессия теперь автоматически синхронизируется с транзакциями. Теперь сессия по умолчанию автоматически синхронизируется с транзакцией, включая автопромывку и автоистечение срока действия. Транзакция присутствует всегда, если только она не отключена с помощью опции
autocommit
. Когда все три флага установлены по умолчанию, сессия изящно восстанавливается после откатов, и в нее очень трудно попасть несвежим данным. Подробности см. в новой документации по сессиям.Имплицитный заказ по удален. Это повлияет на пользователей ORM, которые полагаются на «неявное упорядочивание» SA, согласно которому все объекты Query, не имеющие
order_by()
, будут упорядочиваться по столбцу «id» или «oid» первичной таблицы, а все лениво/нетерпеливо загружаемые коллекции применяют аналогичное упорядочивание. В 0.5 автоматическое упорядочивание должно быть явно настроено для объектовmapper()
иrelation()
(при желании), или иначе при использованииQuery
.Для преобразования отображения 0.4 в 0.5, чтобы его поведение при упорядочивании было очень похоже на 0.4 или предыдущее, используйте настройку
order_by
наmapper()
иrelation()
:mapper( User, users, properties={"addresses": relation(Address, order_by=addresses.c.id)}, order_by=users.c.id, )
Для установки упорядочения на обратную ссылку используйте функцию
backref()
:"keywords": relation( Keyword, secondary=item_keywords, order_by=keywords.c.name, backref=backref("items", order_by=items.c.id), )
Использование декларативных ? Чтобы помочь справиться с новым требованием
order_by
,order_by
и друзья теперь могут быть заданы с помощью строк, которые впоследствии оцениваются в Python (это работает только с декларативными, а не обычными мапперами):class MyClass(MyDeclarativeBase): ... "addresses": relation("Address", order_by="Address.id")
Обычно рекомендуется устанавливать
order_by
наrelation()s
, загружающие списочные коллекции элементов, так как в противном случае упорядочивание не может быть нарушено. В остальных случаях лучше всего использоватьQuery.order_by()
для управления упорядочиванием загружаемых первичных сущностей.Сессия теперь autoflush=True/autoexpire=True/autocommit=False. - Чтобы настроить ее, достаточно вызвать
sessionmaker()
без аргументов. Теперь имяtransactional=True
сталоautocommit=False
. Промывка происходит при каждом выданном запросе (отключается с помощьюautoflush=False
), внутри каждогоcommit()
(как всегда) и перед каждымbegin_nested()
(чтобы откат к SAVEPOINT был осмысленным). После каждогоcommit()
и после каждогоrollback()
происходит истечение срока действия всех объектов. После отката ожидающие объекты удаляются, удаленные объекты переходят в разряд постоянных. Эти умолчания очень хорошо сочетаются друг с другом, и необходимость в таких старых приемах, какclear()
(который также переименован вexpunge_all()
), действительно отпадает.P.S.: сессии теперь можно использовать повторно после удаления
rollback()
. Изменения атрибутов скаляров и коллекций, добавления и удаления откатываются.session.add() заменяет session.save(), session.update(), session.save_or_update(). - методы
session.add(someitem)
иsession.add_all([list of items])
заменяютsave()
,update()
иsave_or_update()
. Эти методы останутся устаревшими в версии 0.5.Конфигурация backref стала менее многословной. - Функция
backref()
теперь использует аргументыprimaryjoin
иsecondaryjoin
обращенной впередrelation()
, если они не указаны в явном виде. Больше нет необходимости указыватьprimaryjoin
/secondaryjoin
в обоих направлениях по отдельности.Упрощены полиморфные опции. - Упрощено поведение ORM в отношении «полиморфной загрузки». В версии 0.4 функция mapper() имела аргумент
polymorphic_fetch
, который мог быть настроен какselect
илиdeferred
. Теперь эта опция удалена; теперь mapper будет просто откладывать столбцы, которые не присутствовали в операторе SELECT. Фактически используемый оператор SELECT управляется аргументом отображателяwith_polymorphic
(который также присутствует в 0.4 и заменяетselect_table
), а также методомwith_polymorphic()
наQuery
(также в 0.4).Улучшением отложенной загрузки наследуемых классов является то, что теперь во всех случаях картограф выдает «оптимизированную» версию оператора SELECT; т.е. если класс B наследуется от A, а несколько атрибутов, присутствующих только у класса B, истекли, то операция обновления будет включать в оператор SELECT только таблицу B и не будет JOIN к A.
Метод
execute()
наSession
преобразует обычные строки в конструкцииtext()
, так что все параметры bind могут быть указаны как «:bindname» без необходимости явного вызоваtext()
. Если здесь требуется «сырой» SQL, используйтеsession.connection().execute("raw text")
.session.Query().iterate_instances()
был переименован вinstances()
. Старый методinstances()
, возвращающий список вместо итератора, больше не существует. Если вы рассчитывали на такое поведение, то вам следует использоватьlist(your_query.instances())
.
Расширение ORM¶
В 0.5 мы расширяем возможности модификации и расширения ORM. Вот краткое описание:
MapperExtension. - Это классический класс расширения, который остался. Методы, которые редко должны быть востребованы, -
create_instance()
иpopulate_instance()
. Для управления инициализацией объекта при его загрузке из базы данных следует использовать методreconstruct_instance()
, а проще - декоратор@reconstructor
, описанный в документации.SessionExtension. - Это простой в использовании класс расширения для событий сессии. В частности, он предоставляет методы
before_flush()
,after_flush()
иafter_flush_postexec()
. Во многих случаях его использование рекомендуется вместоMapperExtension.before_XXX
, так как внутриbefore_flush()
можно свободно модифицировать flush-план сессии, чего нельзя сделать изMapperExtension
.AttributeExtension. - Этот класс теперь является частью публичного API и позволяет перехватывать события пользовательского интерфейса для атрибутов, включая операции установки и удаления атрибутов, добавления и удаления коллекций. Он также позволяет изменять устанавливаемое или добавляемое значение. Декоратор
@validates
, описанный в документации, предоставляет быстрый способ пометить любые отображаемые атрибуты как «проверяемые» определенным методом класса.Настройка инструментария атрибутов. - API предоставляется для амбициозных попыток полностью заменить инструментарий атрибутов SQLAlchemy или просто дополнить его в некоторых случаях. Этот API был создан для целей инструментария Trellis, но доступен как публичный API. Некоторые примеры приведены в дистрибутиве в каталоге
/examples/custom_attributes
.
Схема/Типы¶
Строка без длины больше не генерирует TEXT, а генерирует VARCHAR - Тип
String
больше не преобразуется магическим образом в типText
, если он указан без длины. Это сказывается только при создании CREATE TABLE, так как в этом случае будет создаватьсяVARCHAR
без параметра длины, что недопустимо для многих (но не для всех) баз данных. Для создания столбца типа TEXT (или CLOB, т.е. неограниченной строки) следует использовать типText
.PickleType() с mutable=True требует наличия метода __eq__() - Тип
PickleType
нуждается в сравнении значений при mutable=True. Метод сравненияpickle.dumps()
неэффективен и ненадежен. Если входящий объект не реализует__eq__()
и не являетсяNone
, то используется сравнениеdumps()
, но выдается предупреждение. Для типов, реализующих__eq__()
, к которым относятся все словари, списки и т.д., сравнение будет использоваться==
и теперь по умолчанию является надежным.Удалены методы convert_bind_param() и convert_result_value() из TypeEngine/TypeDecorator. - В книге O’Reilly, к сожалению, эти методы были задокументированы, хотя после версии 0.3 они были устаревшими. Для пользовательского типа, являющегося подклассом
TypeEngine
, для обработки привязки/результата следует использовать методыbind_processor()
иresult_processor()
. Любой пользовательский тип, будь то расширениеTypeEngine
илиTypeDecorator
, использующий старый стиль 0.3, может быть легко адаптирован к новому стилю с помощью следующего адаптера:class AdaptOldConvertMethods(object): """A mixin which adapts 0.3-style convert_bind_param and convert_result_value methods """ def bind_processor(self, dialect): def convert(value): return self.convert_bind_param(value, dialect) return convert def result_processor(self, dialect): def convert(value): return self.convert_result_value(value, dialect) return convert def convert_result_value(self, value, dialect): return value def convert_bind_param(self, value, dialect): return value
Для использования приведенного выше миксина:
class MyType(AdaptOldConvertMethods, TypeEngine): ...
Флаг
quote
наColumn
иTable
, а также флагquote_schema
наTable
теперь управляют цитированием как положительно, так и отрицательно. По умолчанию установлено значениеNone
, что означает, что действуют обычные правила кавычек. ПриTrue
кавычки включаются принудительно. ПриFalse
кавычки принудительно отключаются.Значение столбца
DEFAULT
в DDL теперь удобнее указывать с помощьюColumn(..., server_default='val')
, отказавшись отColumn(..., PassiveDefault('val'))
.default=
теперь предназначен исключительно для значений по умолчанию, инициируемых Python, и может сосуществовать с server_default. Новыйserver_default=FetchedValue()
заменяет идиомуPassiveDefault('')
для пометки столбцов как подверженных влиянию внешних триггеров и не имеет побочных эффектов DDL.Типы SQLite
DateTime
,Time
иDate
теперь принимают в качестве входных параметров связывания только объекты типа datetime, а не строки. Если вы хотите создать свой собственный «гибридный» тип, который принимает строки и возвращает результаты в виде объектов даты (в любом формате), создайтеTypeDecorator
, который будет построен на основеString
. Если вам нужны только строковые даты, используйтеString
.Кроме того, типы
DateTime
иTime
при использовании с SQLite теперь представляют поле «микросекунды» объекта Pythondatetime.datetime
так же, как иstr(datetime)
- как дробные секунды, а не как счетчик микросекунд. То есть:dt = datetime.datetime(2008, 6, 27, 12, 0, 0, 125) # 125 usec # old way "2008-06-27 12:00:00.125" # new way "2008-06-27 12:00:00.000125"
Таким образом, если существующая файловая база данных SQLite будет использоваться в версиях 0.4 и 0.5, необходимо либо обновить столбцы времени даты для хранения нового формата (ПРИМЕЧАНИЕ: пожалуйста, проверьте это, я уверен, что это правильно):
UPDATE mytable SET somedatecol = substr(somedatecol, 0, 19) || '.' || substr((substr(somedatecol, 21, -1) / 1000000), 3, -1);
или включить режим «legacy» следующим образом:
from sqlalchemy.databases.sqlite import DateTimeMixin DateTimeMixin.__legacy_microseconds__ = True
Пул подключений больше не является локальным по умолчанию¶
В 0.4 по умолчанию был установлен неудачный флаг «pool_threadlocal=True», что приводило к неожиданному поведению, например, при использовании нескольких Sessions в рамках одного потока. В версии 0.5 этот флаг отключен. Чтобы снова включить поведение 0.4, укажите pool_threadlocal=True
в create_engine()
, либо используйте стратегию «threadlocal» через strategy="threadlocal"
.
*args принимаются, *args больше не принимаются¶
Политика в отношении method(\*args)
и method([args])
заключается в том, что если метод принимает набор элементов переменной длины, представляющих собой фиксированную структуру, то он принимает \*args
. Если метод принимает набор элементов переменной длины, которые управляются данными, то он принимает [args]
.
Различные функции Query.options()
eagerload()
,eagerload_all()
,lazyload()
,contains_eager()
,defer()
,undefer()
теперь принимают в качестве аргумента переменную длину\*keys
, что позволяет формулировать путь с помощью дескрипторов, т.е:query.options(eagerload_all(User.orders, Order.items, Item.keywords))
Для обратной совместимости по-прежнему принимается один аргумент в виде массива.
Аналогично, методы
Query.join()
иQuery.outerjoin()
принимают переменную длину *args, при этом для обратной совместимости принимается один массив:query.join("orders", "items") query.join(User.orders, Order.items)
метод
in_()
на колонках и им подобных теперь принимает только аргумент списка. Он больше не принимает аргумент\*args
.
Удалено¶
entity_name - Эта возможность всегда была проблематичной и редко использовалась. Более глубокая проработка сценариев использования в версии 0.5 выявила дополнительные проблемы с
entity_name
, что привело к ее удалению. Если для одного класса требуются различные отображения, разбейте класс на отдельные подклассы и отобразите их отдельно. Пример этого приведен в [wiki:UsageRecipes/EntityName]. Более подробная информация по обоснованию описана на сайте https://groups.google.c om/group/sqlalchemy/browse_thread/thread/9e23a0641a88b96d? hl=en .очистка функцийget()/load().
Метод
load()
был удален. Его функциональность была несколько произвольной и, по сути, скопирована из Hibernate, где этот метод также не имеет особого смысла.Для получения эквивалентной функциональности:
x = session.query(SomeClass).populate_existing().get(7)
Session.get(cls, id)
иSession.load(cls, id)
были удалены.Session.get()
является избыточным по сравнению сsession.query(cls).get(id)
.MapperExtension.get()
также удаляется (как иMapperExtension.load()
). Чтобы переопределить функциональностьQuery.get()
, используйте подкласс:class MyQuery(Query): def get(self, ident): ... session = sessionmaker(query_cls=MyQuery)() ad1 = session.query(Address).get(1)
sqlalchemy.orm.relation()
Следующие устаревшие аргументы ключевых слов были удалены:
foreignkey, association, private, attributeext, is_backref
В частности,
attributeext
заменен наextension
- классAttributeExtension
теперь находится в публичном API.session.Query()
Следующие устаревшие функции были удалены:
list, scalar, count_by, select_whereclause, get_by, select_by, join_by, selectfirst, selectone, select, execute, select_statement, select_text, join_to, join_via, selectfirst_by, selectone_by, apply_max, apply_min, apply_avg, apply_sum
Кроме того, удален аргумент
id
в виде ключевого слова дляjoin()
,outerjoin()
,add_entity()
иadd_column()
. Для нацеливания псевдонимов таблиц вQuery
на столбцы результатов используйте конструкциюaliased
:from sqlalchemy.orm import aliased address_alias = aliased(Address) print(session.query(User, address_alias).join((address_alias, User.addresses)).all())
sqlalchemy.orm.Mapper
instances()
get_session() - этот метод был не очень заметен, но при использовании расширений типа
scoped_session()
или старогоSessionContextExt
использовался эффект связывания ленивых загрузок с определенной сессией, даже если родительский объект был полностью отсоединен. Возможно, что некоторые приложения, которые полагались на такое поведение, больше не будут работать так, как ожидалось; но лучшей практикой программирования здесь является обеспечение присутствия объектов внутри сессий, если требуется доступ к базе данных через их атрибуты.
mapper(MyClass, mytable)
Сопоставленные классы больше не инструментируются атрибутом класса «c»; например,
MyClass.c
.sqlalchemy.orm.collections
Псевдоним _prepare_instrumentation для prepare_instrumentation был удален.
sqlalchemy.orm
Удален псевдоним
EXT_PASS
дляEXT_CONTINUE
.sqlalchemy.engine
Псевдоним из
DefaultDialect.preexecute_sequences
в.preexecute_pk_sequences
был удален.Устаревшая функция engine_descriptors() была удалена.
sqlalchemy.ext.activemapper
Модуль удален.
sqlalchemy.ext.assignmapper
Модуль удален.
sqlalchemy.ext.associationproxy
Передача аргументов ключевых слов на прокси
.append(item, \**kw)
была удалена и теперь это просто.append(item)
sqlalchemy.ext.selectresults
,sqlalchemy.mods.selectresults
Модули удалены.
sqlalchemy.ext.declarative
declared_synonym()
удален.sqlalchemy.ext.sessioncontext
Модуль удален.
sqlalchemy.log
Псевдоним
SADeprecationWarning
дляsqlalchemy.exc.SADeprecationWarning
был удален.sqlalchemy.exc
exc.AssertionError
был удален и заменен одноименным встроенным модулем Python.sqlalchemy.databases.mysql
Устаревший метод диалекта
get_version_info
был удален.
Переименование или перемещение¶
sqlalchemy.exceptions
теперьsqlalchemy.exc
До версии 0.6 модуль может импортироваться под старым именем.
FlushError
,ConcurrentModificationError
,UnmappedColumnError
-> sqlalchemy.orm.excЭти исключения перенесены в пакет orm. Импорт „sqlalchemy.orm“ установит псевдонимы в sqlalchemy.exc для совместимости до версии 0.6.
sqlalchemy.logging
->sqlalchemy.log
Этот внутренний модуль был переименован. Теперь его не нужно выделять в специальный корпус при упаковке SA с помощью py2app и подобных инструментов, сканирующих импорт.
session.Query().iterate_instances()
->session.Query().instances()
.
Утративший силу¶
Session.save()
,Session.update()
,Session.save_or_update()
Все три заменяются на
Session.add()
sqlalchemy.PassiveDefault
Использовать
Column(server_default=...)
Под капотом транслируется в sqlalchemy.DefaultClause().session.Query().iterate_instances()
. Он был переименован вinstances()
.