Что нового в 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 теперь представляют поле «микросекунды» объекта Python datetime.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().

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