Что нового в SQLAlchemy 0.4?

О данном документе

В данном документе описаны изменения между версиями SQLAlchemy 0.3, выпущенной 14 октября 2007 года, и SQLAlchemy 0.4, выпущенной 12 октября 2008 года.

Дата документа: 21 марта 2008 г.

Первым делом

Если вы используете какие-либо функции ORM, убедитесь, что вы импортируете из sqlalchemy.orm:

from sqlalchemy import *
from sqlalchemy.orm import *

Во-вторых, везде, где раньше говорилось engine=, connectable=, bind_to=, something.engine, metadata.connect(), используйте bind:

myengine = create_engine("sqlite://")

meta = MetaData(myengine)

meta2 = MetaData()
meta2.bind = myengine

session = create_session(bind=myengine)

statement = select([table], bind=myengine)

Получили? Отлично! Теперь вы (на 95%) совместимы с 0.4. Если вы используете 0.3.10, то можете сразу же внести эти изменения; они будут работать и там.

Импорт модулей

В версии 0.3 команда «from sqlalchemy import *» импортировала все подмодули sqlalchemy в ваше пространство имен. Версия 0.4 больше не импортирует подмодули в пространство имен. Это может привести к необходимости добавления дополнительных импортов в код.

В версии 0.3 этот код работал:

from sqlalchemy import *


class UTCDateTime(types.TypeDecorator):
    pass

В версии 0.4 это необходимо сделать:

from sqlalchemy import *
from sqlalchemy import types


class UTCDateTime(types.TypeDecorator):
    pass

Объектно-реляционное отображение

Запрос по адресу

Новый API для запросов

Query стандартизирован на основе генеративного интерфейса (старый интерфейс все еще существует, просто он устарел). Хотя большая часть генеративного интерфейса доступна в 0.3, Query 0.4 имеет внутреннее устройство, соответствующее внешнему генеративному, и обладает гораздо большим количеством трюков. Сужение результатов осуществляется через filter() и filter_by(), ограничение/смещение - через срезы массива или limit()/offset(), объединение - через join() и outerjoin() (или более вручную, через select_from(), а также через сформированные вручную критерии).

Чтобы избежать предупреждений об устаревании, необходимо внести некоторые изменения в код 03

User.query.get_by( **kwargs )

User.query.filter_by(**kwargs).first()

User.query.select_by( **kwargs )

User.query.filter_by(**kwargs).all()

User.query.select()

User.query.filter(xxx).all()

Новые конструкции выражений, основанные на свойствах

Наиболее ощутимым отличием ORM является то, что теперь можно строить критерий запроса, используя атрибуты класса напрямую. Префикс «.c.» больше не нужен при работе с отображенными классами:

session.query(User).filter(and_(User.name == "fred", User.id > 17))

В то время как простые сравнения на основе столбцов не представляют особой сложности, в атрибутах классов доступны некоторые новые конструкции «более высокого уровня», включая те, которые ранее были доступны только в filter_by():

# comparison of scalar relations to an instance
filter(Address.user == user)

# return all users who contain a particular address
filter(User.addresses.contains(address))

# return all users who *dont* contain the address
filter(~User.address.contains(address))

# return all users who contain a particular address with
# the email_address like '%foo%'
filter(User.addresses.any(Address.email_address.like("%foo%")))

# same, email address equals 'foo@bar.com'.  can fall back to keyword
# args for simple comparisons
filter(User.addresses.any(email_address="foo@bar.com"))

# return all Addresses whose user attribute has the username 'ed'
filter(Address.user.has(name="ed"))

# return all Addresses whose user attribute has the username 'ed'
# and an id > 5 (mixing clauses with kwargs)
filter(Address.user.has(User.id > 5, name="ed"))

Коллекция Column остается доступной для сопоставленных классов в атрибуте .c. Обратите внимание, что выражения, основанные на свойствах, доступны только для отображенных свойств отображенных классов. .c по-прежнему используется для доступа к столбцам в регулярных таблицах и выбираемым объектам, создаваемым из SQL Expressions.

Автоматическое сглаживание соединений

У нас уже давно есть join() и outerjoin():

session.query(Order).join("items")

Теперь вы можете присвоить им псевдонимы:

session.query(Order).join("items", aliased=True).filter(Item.name="item 1").join(
    "items", aliased=True
).filter(Item.name == "item 3")

Приведенные выше действия создадут два соединения из orders->items с использованием псевдонимов. Вызов filter(), следующий за каждым из них, подгонит критерий таблицы к критерию псевдонима. Чтобы получить доступ к объектам Item, используйте add_entity() и нацельте каждое соединение на id:

session.query(Order).join("items", id="j1", aliased=True).filter(
    Item.name == "item 1"
).join("items", aliased=True, id="j2").filter(Item.name == "item 3").add_entity(
    Item, id="j1"
).add_entity(
    Item, id="j2"
)

Возвращает кортежи в форме: (Order, Item, Item).

Самореферентные запросы

Итак, query.join() теперь может создавать псевдонимы. Что это нам дает? Самореферентные запросы! Объединения могут выполняться без каких-либо Alias объектов:

# standard self-referential TreeNode mapper with backref
mapper(
    TreeNode,
    tree_nodes,
    properties={
        "children": relation(
            TreeNode, backref=backref("parent", remote_side=tree_nodes.id)
        )
    },
)

# query for node with child containing "bar" two levels deep
session.query(TreeNode).join(["children", "children"], aliased=True).filter_by(
    name="bar"
)

Чтобы добавить критерий для каждой таблицы по пути в aliased join, можно использовать from_joinpoint для продолжения объединения с одной и той же строкой псевдонимов:

# search for the treenode along the path "n1/n12/n122"

# first find a Node with name="n122"
q = sess.query(Node).filter_by(name="n122")

# then join to parent with "n12"
q = q.join("parent", aliased=True).filter_by(name="n12")

# join again to the next parent with 'n1'.  use 'from_joinpoint'
# so we join from the previous point, instead of joining off the
# root table
q = q.join("parent", aliased=True, from_joinpoint=True).filter_by(name="n1")

node = q.first()

query.populate_existing()

Нетерпеливая версия query.load() (или session.refresh()). Каждый экземпляр, загруженный из запроса, включая все нетерпеливо загруженные элементы, обновляется немедленно, если уже присутствует в сессии:

session.query(Blah).populate_existing().all()

Отношения

Клаузы SQL, встроенные в обновления/вставки

Для поточного выполнения SQL-клаузул, встраиваемых прямо в UPDATE или INSERT, во время выполнения flush():

myobject.foo = mytable.c.value + 1

user.pwhash = func.md5(password)

order.hash = text("select hash from hashing_table")

В столбце-атрибуте после выполнения операции устанавливается отложенный загрузчик, так что при следующем обращении он выдает SQL для загрузки нового значения.

Самореферентные и циклические нагрузки

Поскольку наше искусство работы с псевдонимами улучшилось, relation() может присоединяться к одной и той же таблице *любое количество раз*; вы указываете ему, как глубоко вы хотите зайти. Покажем самореференцию TreeNode более наглядно:

nodes = Table(
    "nodes",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("parent_id", Integer, ForeignKey("nodes.id")),
    Column("name", String(30)),
)


class TreeNode(object):
    pass


mapper(
    TreeNode,
    nodes,
    properties={"children": relation(TreeNode, lazy=False, join_depth=3)},
)

Что же происходит, когда мы говорим:

create_session().query(TreeNode).all()

? Присоединение по псевдонимам, на три уровня вглубь от родительского:

SELECT
nodes_3.id AS nodes_3_id, nodes_3.parent_id AS nodes_3_parent_id, nodes_3.name AS nodes_3_name,
nodes_2.id AS nodes_2_id, nodes_2.parent_id AS nodes_2_parent_id, nodes_2.name AS nodes_2_name,
nodes_1.id AS nodes_1_id, nodes_1.parent_id AS nodes_1_parent_id, nodes_1.name AS nodes_1_name,
nodes.id AS nodes_id, nodes.parent_id AS nodes_parent_id, nodes.name AS nodes_name
FROM nodes LEFT OUTER JOIN nodes AS nodes_1 ON nodes.id = nodes_1.parent_id
LEFT OUTER JOIN nodes AS nodes_2 ON nodes_1.id = nodes_2.parent_id
LEFT OUTER JOIN nodes AS nodes_3 ON nodes_2.id = nodes_3.parent_id
ORDER BY nodes.oid, nodes_1.oid, nodes_2.oid, nodes_3.oid

Обратите внимание на чистые имена псевдонимов. Присоединение не зависит от того, выполняется ли оно к той же самой непосредственной таблице или к какому-либо другому объекту, который затем циклически возвращается к началу. Любая цепочка нетерпеливых загрузок может циклически возвращаться на себя, если указано join_depth. В противном случае ускоренная загрузка автоматически останавливается при попадании в цикл.

Виды композитов

Это один из примеров из лагеря Hibernate. Составные типы позволяют определить пользовательский тип данных, состоящий из более чем одного столбца (или одного столбца, если хотите). Определим новый тип Point. Хранит координаты x/y:

class Point(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __composite_values__(self):
        return self.x, self.y

    def __eq__(self, other):
        return other.x == self.x and other.y == self.y

    def __ne__(self, other):
        return not self.__eq__(other)

Способ определения объекта Point специфичен для пользовательского типа; конструктор принимает список аргументов, а метод __composite_values__() выдает последовательность этих аргументов. Порядок будет соответствовать нашему картографу, как мы увидим через некоторое время.

Создадим таблицу вершин, хранящую по две точки в каждой строке:

vertices = Table(
    "vertices",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("x1", Integer),
    Column("y1", Integer),
    Column("x2", Integer),
    Column("y2", Integer),
)

Затем отобразим его на карту! Создадим объект Vertex, который хранит два объекта Point:

class Vertex(object):
    def __init__(self, start, end):
        self.start = start
        self.end = end


mapper(
    Vertex,
    vertices,
    properties={
        "start": composite(Point, vertices.c.x1, vertices.c.y1),
        "end": composite(Point, vertices.c.x2, vertices.c.y2),
    },
)

После установки композитного типа его можно использовать так же, как и любой другой тип:

v = Vertex(Point(3, 4), Point(26, 15))
session.save(v)
session.flush()

# works in queries too
q = session.query(Vertex).filter(Vertex.start == Point(3, 4))

Если вы хотите определить, как отображаемые атрибуты формируют SQL-клаузы при использовании в выражениях, создайте собственный подкласс sqlalchemy.orm.PropComparator, определив в нем любой из распространенных операторов (например, __eq__(), __le__() и т.д.), и передайте его в composite(). Составные типы также работают как первичные ключи и могут быть использованы в query.get():

# a Document class which uses a composite Version
# object as primary key
document = query.get(Version(1, "a"))

dynamic_loader() отношения

Коллекция relation(), которая возвращает живой объект Query для всех операций чтения. Операции записи ограничены только append() и remove(), изменения в коллекции не видны до тех пор, пока сессия не будет промыта. Эта возможность особенно удобна при использовании «автопромывочной» сессии, которая будет промываться перед каждым запросом.

mapper(
    Foo,
    foo_table,
    properties={
        "bars": dynamic_loader(
            Bar,
            backref="foo",
            # <other relation() opts>
        )
    },
)

session = create_session(autoflush=True)
foo = session.query(Foo).first()

foo.bars.append(Bar(name="lala"))

for bar in foo.bars.filter(Bar.name == "lala"):
    print(bar)

session.commit()

Новые опции: undefer_group(), eagerload_all()

Несколько удобных опций запроса. undefer_group() помечает всю группу «отложенных» столбцов как не отложенные:

mapper(
    Class,
    table,
    properties={
        "foo": deferred(table.c.foo, group="group1"),
        "bar": deferred(table.c.bar, group="group1"),
        "bat": deferred(table.c.bat, group="group1"),
    },
)

session.query(Class).options(undefer_group("group1")).filter(...).all()

и eagerload_all() задает цепочку атрибутов, которые должны быть найдены за один проход:

mapper(Foo, foo_table, properties={"bar": relation(Bar)})
mapper(Bar, bar_table, properties={"bat": relation(Bat)})
mapper(Bat, bat_table)

# eager load bar and bat
session.query(Foo).options(eagerload_all("bar.bat")).filter(...).all()

Новый API для работы с коллекциями

Коллекции больше не передаются через прокси {{InstrumentedList}}, а доступ к членам, методам и атрибутам является прямым. Декораторы теперь перехватывают объекты, входящие и выходящие из коллекции, и теперь можно легко написать пользовательский класс коллекции, который сам управляет своим членством. Гибкие декораторы также заменяют интерфейс именованных методов пользовательских коллекций в 0.3, позволяя легко адаптировать любой класс для использования в качестве контейнера коллекции.

Коллекции, основанные на словарях, теперь гораздо проще в использовании и полностью соответствуют dict. Для __iter__ больше не нужно менять dict, а новые встроенные типы dict покрывают многие потребности:

# use a dictionary relation keyed by a column
relation(Item, collection_class=column_mapped_collection(items.c.keyword))
# or named attribute
relation(Item, collection_class=attribute_mapped_collection("keyword"))
# or any function you like
relation(Item, collection_class=mapped_collection(lambda entity: entity.a + entity.b))

Существующие в 0.3 dict-подобные и производные от свободных объектов классы коллекций необходимо будет обновить для работы с новым API. В большинстве случаев это сводится к добавлению пары декораторов в определение класса.

Сопоставленные отношения из внешних таблиц/подзапросы

Эта возможность появилась в 0.3, но в 0.4 она была улучшена благодаря улучшению возможности преобразования подзапросов к таблице в подзапросы к псевдониму этой таблицы; это очень важно для ускоренной загрузки, псевдослучайных объединений в запросах и т.д. Это уменьшает необходимость создания мапперов для операторов select, когда нужно просто добавить несколько дополнительных столбцов или подзапросов:

mapper(
    User,
    users,
    properties={
        "fullname": column_property(
            (users.c.firstname + users.c.lastname).label("fullname")
        ),
        "numposts": column_property(
            select([func.count(1)], users.c.id == posts.c.user_id)
            .correlate(users)
            .label("posts")
        ),
    },
)

Типичный запрос выглядит следующим образом:

SELECT (SELECT count(1) FROM posts WHERE users.id = posts.user_id) AS count,
users.firstname || users.lastname AS fullname,
users.id AS users_id, users.firstname AS users_firstname, users.lastname AS users_lastname
FROM users ORDER BY users.oid

API горизонтального масштабирования (Sharding)

[browser:/sqlalchemy/trunk/examples/sharding/attribute_shard .py].

Сессии

Новая парадигма создания сеанса; SessionContext, assignmapper Утрачены

Именно так, весь этот набор заменяется двумя конфигурационными функциями. Использование обеих функций позволит получить наиболее приближенное к 0.1 ощущение, которое мы имеем с 0.1 (т.е. наименьшее количество набора текста).

Настройте свой собственный класс Session прямо там, где вы определяете свой engine (или в любом другом месте):

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine("myengine://")
Session = sessionmaker(bind=engine, autoflush=True, transactional=True)

# use the new Session() freely
sess = Session()
sess.save(someobject)
sess.flush()

Если вам необходимо постконфигурировать сессию, например, с помощью движка, добавьте его позже с помощью configure():

Session.configure(bind=create_engine(...))

Все поведение SessionContext и методы query и __init__ из assignmapper перенесены в новую функцию scoped_session(), которая совместима как с sessionmaker, так и с create_session():

from sqlalchemy.orm import scoped_session, sessionmaker

Session = scoped_session(sessionmaker(autoflush=True, transactional=True))
Session.configure(bind=engine)

u = User(name="wendy")

sess = Session()
sess.save(u)
sess.commit()

# Session constructor is thread-locally scoped.  Everyone gets the same
# Session in the thread when scope="thread".
sess2 = Session()
assert sess is sess2

При использовании потоково-локального Session возвращаемый класс имеет весь интерфейс Session's, реализованный в виде методов класса, а функциональность «assignmapper» доступна с помощью метода класса mapper. Подобно старому objectstore дней….

# "assignmapper"-like functionality available via ScopedSession.mapper
Session.mapper(User, users_table)

u = User(name="wendy")

Session.commit()

Сессии снова являются слабыми ссылками по умолчанию

Флаг weak_identity_map теперь по умолчанию устанавливается в значение True на сессии. Экземпляры, на которые имеются внешние ссылки и которые выходят из области видимости, удаляются из сессии автоматически. Однако объекты, в которых присутствуют «грязные» изменения, будут оставаться сильно ссылающимися до тех пор, пока эти изменения не будут удалены, после чего объект вернется к слабо ссылающимся (это работает и для «мутируемых» типов, например, picklable-атрибутов). Установка значения weak_identity_map в False восстанавливает старое поведение с сильными ссылками для тех, кто использует сессию как кэш.

Автотранзакционные сессии

Как вы могли заметить выше, мы вызываем commit() на Session. Флаг transactional=True означает, что Session всегда находится в транзакции, commit() персистирует постоянно.

Сеансы автоматической промывки

Кроме того, autoflush=True означает, что Session будет flush() перед каждым query, а также при вызове flush() или commit(). Таким образом, теперь это будет работать:

Session = sessionmaker(bind=engine, autoflush=True, transactional=True)

u = User(name="wendy")

sess = Session()
sess.save(u)

# wendy is flushed, comes right back from a query
wendy = sess.query(User).filter_by(name="wendy").one()

Транзакционные методы перенесены на сессии

commit() и rollback(), а также begin() теперь находятся непосредственно на Session. Больше не нужно ни для чего использовать SessionTransaction (он остается в фоновом режиме).

Session = sessionmaker(autoflush=True, transactional=False)

sess = Session()
sess.begin()

# use the session

sess.commit()  # commit transaction

Совместное использование Session с вложенной транзакцией на уровне движка (т.е. неORM) не представляет сложности:

Session = sessionmaker(autoflush=True, transactional=False)

conn = engine.connect()
trans = conn.begin()
sess = Session(bind=conn)

# ... session is transactional

# commit the outermost transaction
trans.commit()

Вложенные сеансовые транзакции с SAVEPOINT

Доступен на уровне движка и ORM. Документация по ORM на данный момент:

https://www.sqlalchemy.org/docs/04/session.html#unitofwork_managing

Сеансы двухфазной фиксации

Доступен на уровне движка и ORM. Документация по ORM на данный момент:

https://www.sqlalchemy.org/docs/04/session.html#unitofwork_managing

Наследование

Полиморфное наследование без присоединений и объединений

Новая документация по наследованию: https://www.sqlalchemy.org/docs/04 /mappers.html#advdatamapping_mapper_inheritance_joined

Улучшенное полиморфное поведение с помощью get()

Все классы, входящие в иерархию наследования объединенной таблицы, получают _instance_key с использованием базового класса, т.е. (BaseClass, (1, ), None). Таким образом, когда вы вызываете get() по базовому классу Query, он может находить экземпляры подклассов в текущей карте идентификации без запроса к базе данных.

Типы

Пользовательские подклассы sqlalchemy.types.TypeDecorator

Для подклассификации TypeDecorator существует New API. Использование API версии 0.3 в некоторых случаях приводит к ошибкам компиляции.

SQL-выражения

Новая, детерминированная генерация меток/алиасов

Все «анонимные» метки и псевдонимы теперь используют простой формат <имя>_<число>. SQL гораздо легче читается и совместим с кэшами оптимизаторов плана. Просто посмотрите некоторые примеры в учебниках: https://www.sqlalchemy.org/docs/04/ormtutorial.html https://www.sqlalchemy.org/docs/04/sqlexpression.html.

Генеративные конструкции select()

Это, безусловно, правильный путь для select(). См. htt p://www.sqlalchemy.org/docs/04/sqlexpression.html#sql_transf orm .

Новая операторская система

Операторы SQL и практически все ключевые слова SQL теперь абстрагированы на уровне компилятора. Теперь они действуют разумно и учитывают тип/бэкенд, см.: https://www.sqlalchemy.org/docs/04/sqlexpression.html#sql_operators.

Все аргументы ключевого слова type переименованы в type_

Как и написано:

b = bindparam("foo", type_=String)

in_ Функция изменена на прием последовательности или выбор

Функция in_ теперь принимает в качестве единственного аргумента последовательность значений или selectable. Прежний API, в котором значения передавались в качестве позиционных аргументов, по-прежнему работает, но уже устарел. Это означает, что

my_table.select(my_table.c.id.in_(1, 2, 3))
my_table.select(my_table.c.id.in_(*listOfIds))

следует изменить на

my_table.select(my_table.c.id.in_([1, 2, 3]))
my_table.select(my_table.c.id.in_(listOfIds))

Схема и отражение

MetaData, BoundMetaData, DynamicMetaData

В серии 0.3.x имена BoundMetaData и DynamicMetaData были упразднены в пользу MetaData и ThreadLocalMetaData. В версии 0.4 старые имена были удалены. Обновление выполняется просто:

+-------------------------------------+-------------------------+
|If You Had                           | Now Use                 |
+=====================================+=========================+
| ``MetaData``                        | ``MetaData``            |
+-------------------------------------+-------------------------+
| ``BoundMetaData``                   | ``MetaData``            |
+-------------------------------------+-------------------------+
| ``DynamicMetaData`` (with one       | ``MetaData``            |
| engine or threadlocal=False)        |                         |
+-------------------------------------+-------------------------+
| ``DynamicMetaData``                 | ``ThreadLocalMetaData`` |
| (with different engines per thread) |                         |
+-------------------------------------+-------------------------+

Редко используемый параметр name для типов MetaData удален. Конструктор ThreadLocalMetaData теперь не принимает аргументов. Оба типа теперь могут быть привязаны к Engine или одному Connection.

Одношаговое многотабличное отражение

Теперь можно загружать определения таблиц и автоматически создавать объекты Table из всей базы данных или схемы за один проход:

>>> metadata = MetaData(myengine, reflect=True)
>>> metadata.tables.keys()
['table_a', 'table_b', 'table_c', '...']

В MetaData также появился метод .reflect(), позволяющий более тонко управлять процессом загрузки, в том числе указывать подмножество доступных для загрузки таблиц.

Выполнение SQL

engine, connectable и bind_to теперь bind.

Transactions, NestedTransactions и TwoPhaseTransactions

События пула подключений

Теперь пул соединений формирует события при создании, проверке и возврате в пул новых DB-API-соединений. Это можно использовать, например, для выполнения сеансовых операторов настройки SQL на новых соединениях.

Oracle Engine Fixed

В версии 0.3.11 в движке Oracle были обнаружены ошибки, связанные с обработкой первичных ключей. Эти ошибки могли приводить к тому, что программы, нормально работающие с другими движками, например sqlite, не работали при использовании движка Oracle. В версии 0.4 движок Oracle Engine был переработан и исправил эти проблемы с первичными ключами.

Выходные параметры для Oracle

result = engine.execute(
    text(
        "begin foo(:x, :y, :z); end;",
        bindparams=[
            bindparam("x", Numeric),
            outparam("y", Numeric),
            outparam("z", Numeric),
        ],
    ),
    x=5,
)
assert result.out_parameters == {"y": 10, "z": 75}

Connection-bound MetaData, Sessions

MetaData и Session могут быть явно привязаны к соединению:

conn = engine.connect()
sess = create_session(bind=conn)

Более быстрые, более надежные ResultProxy объекты

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