Что нового в 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, в 0.4 Query имеет внутреннее устройство, соответствующее генеративному снаружи, и гораздо больше трюков. Все сужение результатов происходит через 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.

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

У нас уже давно есть 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(), изменения в коллекции не видны до тех пор, пока сессия не будет промыта. Эта функция особенно удобна при использовании сессии «autoflushing», которая будет промываться перед каждым запросом.

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 days….

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

u = User(name="wendy")

Session.commit()

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

Флаг weak_identity_map теперь по умолчанию установлен в значение True на Session. Экземпляры, которые имеют внешние ссылки и выпадают из области видимости, автоматически удаляются из сессии. Однако объекты, в которых присутствуют «грязные» изменения, будут оставаться с сильной ссылкой до тех пор, пока эти изменения не будут удалены, и тогда объект вернется к слабой ссылке (это работает и для «изменяемых» типов, таких как 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

Существует New API для подклассификации TypeDecorator. Использование 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 Engine были обнаружены ошибки, связанные с обработкой первичных ключей. Эти ошибки могли привести к тому, что программы, которые прекрасно работали с другими движками, такими как 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}

Связанный с соединением MetaData, Sessions

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

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

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

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