Что нового в 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)