Что нового в 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.Mapperinstances()
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.declarativedeclared_synonym()удален.sqlalchemy.ext.sessioncontextМодуль удален.
sqlalchemy.logПсевдоним
SADeprecationWarningдляsqlalchemy.exc.SADeprecationWarningбыл удален.sqlalchemy.excexc.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().