Примеры ORM

Дистрибутив SQLAlchemy включает в себя множество примеров кода, иллюстрирующих определенный набор шаблонов, некоторые типичные и некоторые не очень типичные. Все они запускаются и могут быть найдены в каталоге /examples дистрибутива. Описания и исходный код для всех примеров можно найти здесь.

Дополнительные примеры SQLAlchemy, некоторые из которых предоставлены пользователями, доступны на вики по адресу https://www.sqlalchemy.org/trac/wiki/UsageRecipes.

Картографические рецепты

Список примыканий

Пример структуры словаря словарей, отображенной с помощью модели списка смежности.

Например:

node = TreeNode('rootnode')
node.append('node1')
node.append('node3')
session.add(node)
session.commit()

dump_tree(node)

Листинг файлов:adjacency_list.py

Ассоциации

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

Listing of files:

  • basic_association.py - Illustrate a many-to-many relationship between an «Order» and a collection of «Item» objects, associating a purchase price with each via an association object called «OrderItem»

  • proxied_association.py - Same example as basic_association, adding in usage of sqlalchemy.ext.associationproxy to make explicit references to OrderItem optional.

  • dict_of_sets_with_default.py - An advanced association proxy example which illustrates nesting of association proxies to produce multi-level Python collections, in this case a dictionary with string keys and sets of integers as values, which conceal the underlying mapped classes.

Интеграция Asyncio

Примеры, иллюстрирующие возможности движка asyncio в SQLAlchemy.

Listing of files:

  • async_orm_writeonly.py - Illustrates using write only relationships for simpler handling of ORM collections under asyncio.

  • basic.py - Illustrates the asyncio engine / connection interface.

  • gather_orm_statements.py - Illustrates how to run many statements concurrently using asyncio.gather() along many asyncio database connections, merging ORM results into a single AsyncSession.

  • greenlet_orm.py - Illustrates use of the sqlalchemy.ext.asyncio.AsyncSession object for asynchronous ORM use, including the optional run_sync() method.

  • async_orm.py - Illustrates use of the sqlalchemy.ext.asyncio.AsyncSession object for asynchronous ORM use.

Направленные графы

Пример сохранения для структуры направленного графа. Граф хранится как набор ребер, каждое из которых ссылается на «нижний» и «верхний» узел в таблице узлов. Проиллюстрированы базовая персистентность и запрос к нижним и верхним соседям:

n2 = Node(2)
n5 = Node(5)
n2.add_neighbor(n5)
print(n2.higher_neighbors())

Листинг файлов:directed_graph.py

Динамические отношения как словари

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

Листинг файлов:dynamic_dict.py

Общие ассоциации

Иллюстрирует различные методы связывания нескольких типов родителей с определенным дочерним объектом.

Все примеры используют декларативное расширение вместе с декларативными миксинами. В конце каждого из них представлен идентичный пример использования - два класса, Customer и Supplier, оба подкласса HasAddresses миксина, который гарантирует, что родительскому классу будет предоставлена коллекция addresses, содержащая объекты Address.

Скрипты discriminator_on_association.py и generic_fk.py представляют собой модернизированные версии рецептов, представленных в 2007 году в записи блога Polymorphic Associations with SQLAlchemy.

Listing of files:

  • table_per_related.py - Illustrates a generic association which persists association objects within individual tables, each one generated to persist those objects on behalf of a particular parent class.

  • table_per_association.py - Illustrates a mixin which provides a generic association via a individually generated association tables for each parent class. The associated objects themselves are persisted in a single table shared among all parents.

  • generic_fk.py - Illustrates a so-called «generic foreign key», in a similar fashion to that of popular frameworks such as Django, ROR, etc. This approach bypasses standard referential integrity practices, in that the «foreign key» column is not actually constrained to refer to any particular table; instead, in-application logic is used to determine which table is referenced.

  • discriminator_on_association.py - Illustrates a mixin which provides a generic association using a single target table and a single association table, referred to by all parent tables. The association table contains a «discriminator» column which determines what type of parent object associates to each particular row in the association table.

Материализованные пути

Иллюстрирует шаблон «материализованные пути» для иерархических данных с использованием SQLAlchemy ORM.

Listing of files:

Вложенные наборы

Иллюстрирует элементарный способ реализации паттерна «вложенные множества» для иерархических данных с помощью SQLAlchemy ORM.

Listing of files:

Производительность

Пакет профилирования производительности для различных случаев использования SQLAlchemy.

Каждый пакет ориентирован на конкретный случай использования с определенным профилем производительности и соответствующими последствиями:

  • объёмные вставки

  • отдельные вкладыши, с транзакциями или без них

  • получение большого количества строк

  • выполнение большого количества коротких запросов

Все наборы включают в себя различные модели использования, иллюстрирующие использование как Core, так и ORM, и в целом отсортированы в порядке убывания производительности от худшего к лучшему, и наоборот, в зависимости от объема функциональности, предоставляемой SQLAlchemy, от наибольшего к наименьшему (эти две вещи обычно идеально соответствуют друг другу).

На уровне пакета представлен инструмент командной строки, который позволяет запускать отдельные наборы:

$ python -m examples.performance --help
usage: python -m examples.performance [-h] [--test TEST] [--dburl DBURL]
                                      [--num NUM] [--profile] [--dump]
                                      [--echo]

                                      {bulk_inserts,large_resultsets,single_inserts}

positional arguments:
  {bulk_inserts,large_resultsets,single_inserts}
                        suite to run

optional arguments:
  -h, --help            show this help message and exit
  --test TEST           run specific test name
  --dburl DBURL         database URL, default sqlite:///profile.db
  --num NUM             Number of iterations/items/etc for tests;
                        default is module-specific
  --profile             run profiling and dump call counts
  --dump                dump full call profile (implies --profile)
  --echo                Echo SQL output

Пример выполнения выглядит так:

$ python -m examples.performance bulk_inserts

Или с опциями:

$ python -m examples.performance bulk_inserts \
    --dburl mysql+mysqldb://scott:tiger@localhost/test \
    --profile --num 1000

Листинг файлов

Listing of files:

  • bulk_inserts.py - This series of tests illustrates different ways to INSERT a large number of rows in bulk.

  • bulk_updates.py - This series of tests will illustrate different ways to UPDATE a large number of rows in bulk (under construction! there’s just one test at the moment)

  • short_selects.py - This series of tests illustrates different ways to SELECT a single record by primary key

  • __main__.py - Allows the examples/performance package to be run as a script.

  • single_inserts.py - In this series of tests, we’re looking at a method that inserts a row within a distinct transaction, and afterwards returns to essentially a «closed» state. This would be analogous to an API call that starts up a database connection, inserts the row, commits and closes.

  • large_resultsets.py - In this series of tests, we are looking at time to load a large number of very small and simple rows.

Выполнение всех тестов со временем

Это форма запуска по умолчанию:

$ python -m examples.performance single_inserts
Tests to run: test_orm_commit, test_bulk_save,
              test_bulk_insert_dictionaries, test_core,
              test_core_query_caching, test_dbapi_raw_w_connect,
              test_dbapi_raw_w_pool

test_orm_commit : Individual INSERT/COMMIT pairs via the
    ORM (10000 iterations); total time 13.690218 sec
test_bulk_save : Individual INSERT/COMMIT pairs using
    the "bulk" API  (10000 iterations); total time 11.290371 sec
test_bulk_insert_dictionaries : Individual INSERT/COMMIT pairs using
    the "bulk" API with dictionaries (10000 iterations);
    total time 10.814626 sec
test_core : Individual INSERT/COMMIT pairs using Core.
    (10000 iterations); total time 9.665620 sec
test_core_query_caching : Individual INSERT/COMMIT pairs using Core
    with query caching (10000 iterations); total time 9.209010 sec
test_dbapi_raw_w_connect : Individual INSERT/COMMIT pairs w/ DBAPI +
    connection each time (10000 iterations); total time 9.551103 sec
test_dbapi_raw_w_pool : Individual INSERT/COMMIT pairs w/ DBAPI +
    connection pool (10000 iterations); total time 8.001813 sec

Выгрузка профилей для отдельных тестов

Вывод профиля Python может быть сделан для всех тестов или, чаще всего, для отдельных тестов:

$ python -m examples.performance single_inserts --test test_core --num 1000 --dump
Tests to run: test_core
test_core : Individual INSERT/COMMIT pairs using Core. (1000 iterations); total fn calls 186109
         186109 function calls (186102 primitive calls) in 1.089 seconds

   Ordered by: internal time, call count

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1000    0.634    0.001    0.634    0.001 {method 'commit' of 'sqlite3.Connection' objects}
     1000    0.154    0.000    0.154    0.000 {method 'execute' of 'sqlite3.Cursor' objects}
     1000    0.021    0.000    0.074    0.000 /Users/classic/dev/sqlalchemy/lib/sqlalchemy/sql/compiler.py:1950(_get_colparams)
     1000    0.015    0.000    0.034    0.000 /Users/classic/dev/sqlalchemy/lib/sqlalchemy/engine/default.py:503(_init_compiled)
        1    0.012    0.012    1.091    1.091 examples/performance/single_inserts.py:79(test_core)

    ...

Написание собственных сюит

Система наборов профилировщиков является расширяемой и может быть применена к вашему собственному набору тестов. Это ценная техника, которую можно использовать при выборе правильного подхода для какого-то критически важного для производительности набора процедур. Например, если мы хотим определить разницу между несколькими видами загрузки, мы можем создать файл test_loads.py со следующим содержимым:

from examples.performance import Profiler
from sqlalchemy import Integer, Column, create_engine, ForeignKey
from sqlalchemy.orm import relationship, joinedload, subqueryload, Session
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()
engine = None
session = None


class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    children = relationship("Child")


class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'))


# Init with name of file, default number of items
Profiler.init("test_loads", 1000)


@Profiler.setup_once
def setup_once(dburl, echo, num):
    "setup once.  create an engine, insert fixture data"
    global engine
    engine = create_engine(dburl, echo=echo)
    Base.metadata.drop_all(engine)
    Base.metadata.create_all(engine)
    sess = Session(engine)
    sess.add_all([
        Parent(children=[Child() for j in range(100)])
        for i in range(num)
    ])
    sess.commit()


@Profiler.setup
def setup(dburl, echo, num):
    "setup per test.  create a new Session."
    global session
    session = Session(engine)
    # pre-connect so this part isn't profiled (if we choose)
    session.connection()


@Profiler.profile
def test_lazyload(n):
    "load everything, no eager loading."

    for parent in session.query(Parent):
        parent.children


@Profiler.profile
def test_joinedload(n):
    "load everything, joined eager loading."

    for parent in session.query(Parent).options(joinedload("children")):
        parent.children


@Profiler.profile
def test_subqueryload(n):
    "load everything, subquery eager loading."

    for parent in session.query(Parent).options(subqueryload("children")):
        parent.children

if __name__ == '__main__':
    Profiler.main()

Мы можем запустить наш новый сценарий напрямую:

$ python test_loads.py  --dburl postgresql+psycopg2://scott:tiger@localhost/test
Running setup once...
Tests to run: test_lazyload, test_joinedload, test_subqueryload
test_lazyload : load everything, no eager loading. (1000 iterations); total time 11.971159 sec
test_joinedload : load everything, joined eager loading. (1000 iterations); total time 2.754592 sec
test_subqueryload : load everything, subquery eager loading. (1000 iterations); total time 2.977696 sec

Космические захватчики

Игра Space Invaders, использующая SQLite в качестве машины состояний.

Первоначально разработан в 2012 году. Адаптирован для работы в Python 3.

Запускается в текстовой консоли с использованием ASCII-искусства.

../../_images/space_invaders.jpg

Для запуска:

python -m examples.space_invaders.space_invaders

Во время его выполнения следите за выводом SQL в журнале:

tail -f space_invaders.log

наслаждайтесь!

Листинг файлов:space_invaders.py

Версионирование объектов

Версионирование с помощью таблицы истории

Иллюстрирует расширение, которое создает таблицы версий для объектов и хранит записи для каждого изменения. Данное расширение генерирует анонимный класс «history», который представляет исторические версии целевого объекта.

Сравните с примерами Версионирование с использованием временных рядов, в которых обновления записываются как новые строки в той же таблице, без использования отдельной таблицы истории.

Использование показано на примере модуля модульного тестирования test_versioning.py, который может быть запущен как любой другой модуль, используя unittest внутренне:

python -m examples.versioned_history.test_versioning

Фрагмент примера использования, с использованием декларативного:

from history_meta import Versioned, versioned_session

class Base(DeclarativeBase):
    pass

class SomeClass(Versioned, Base):
    __tablename__ = 'sometable'

    id = Column(Integer, primary_key=True)
    name = Column(String(50))

    def __eq__(self, other):
        assert type(other) is SomeClass and other.id == self.id

Session = sessionmaker(bind=engine)
versioned_session(Session)

sess = Session()
sc = SomeClass(name='sc1')
sess.add(sc)
sess.commit()

sc.name = 'sc1modified'
sess.commit()

assert sc.version == 2

SomeClassHistory = SomeClass.__history_mapper__.class_

assert sess.query(SomeClassHistory).\
            filter(SomeClassHistory.version == 1).\
            all() \
            == [SomeClassHistory(version=1, name='sc1')]

Миксин Versioned предназначен для работы с декларативными. Чтобы использовать расширение с классическими мапперами, можно применить функцию _history_mapper:

from history_meta import _history_mapper

m = mapper(SomeClass, sometable)
_history_mapper(m)

SomeHistoryClass = SomeClass.__history_mapper__.class_

Пример версионирования также интегрируется с функцией оптимистического параллелизма ORM, документированной в Настройка счетчика версий. Чтобы включить эту возможность, установите флаг Versioned.use_mapper_versioning в True:

class SomeClass(Versioned, Base):
    __tablename__ = 'sometable'

    use_mapper_versioning = True

    id = Column(Integer, primary_key=True)
    name = Column(String(50))

    def __eq__(self, other):
        assert type(other) is SomeClass and other.id == self.id

Выше, если два экземпляра SomeClass с одинаковым идентификатором версии обновляются и отправляются в базу данных для UPDATE одновременно, если уровень изоляции базы данных позволяет двум операторам UPDATE продолжить работу, один из них будет неудачным, потому что он больше не соответствует последнему известному идентификатору версии.

Listing of files:

Версионирование с использованием временных рядов

Несколько примеров, иллюстрирующих технику перехвата изменений, которые сначала были бы интерпретированы как UPDATE ряда, и превращения их в INSERT нового ряда, оставляя предыдущий ряд нетронутым в качестве исторической версии.

Сравните с примером Версионирование с помощью таблицы истории, который записывает строку истории в отдельную таблицу истории.

Listing of files:

  • versioned_rows_w_versionid.py - Illustrates a method to intercept changes on objects, turning an UPDATE statement on a single row into an INSERT statement, so that a new row is inserted with the new data, keeping the old row intact.

  • versioned_update_old_row.py - Illustrates the same UPDATE into INSERT technique of versioned_rows.py, but also emits an UPDATE on the old row to affect a change in timestamp. Also includes a SessionEvents.do_orm_execute() hook to limit queries to only the most recent version.

  • versioned_rows.py - Illustrates a method to intercept changes on objects, turning an UPDATE statement on a single row into an INSERT statement, so that a new row is inserted with the new data, keeping the old row intact.

  • versioned_map.py - A variant of the versioned_rows example built around the concept of a «vertical table» structure, like those illustrated in Вертикальное отображение атрибутов examples.

Вертикальное отображение атрибутов

Иллюстрирует отображение «вертикальной таблицы».

Вертикальная таблица» - это метод, при котором отдельные атрибуты объекта хранятся в виде отдельных строк в таблице. Техника «вертикальной таблицы» используется для хранения объектов, которые могут иметь разнообразный набор атрибутов, за счет простого управления запросами и краткости. Она часто встречается в системах управления контентом/документами, чтобы гибко представлять созданные пользователем структуры.

Приведены два варианта этого подхода. Во втором каждый ряд ссылается на «тип данных», который содержит информацию о типе информации, хранящейся в атрибуте, например, целое число, строка или дата.

Пример:

shrew = Animal(u'shrew')
shrew[u'cuteness'] = 5
shrew[u'weasel-like'] = False
shrew[u'poisonous'] = True

session.add(shrew)
session.flush()

q = (session.query(Animal).
     filter(Animal.facts.any(
       and_(AnimalFact.key == u'weasel-like',
            AnimalFact.value == True))))
print('weasel-like animals', q.all())

Listing of files:

Рецепты отображения наследования

Основные отображения наследования

Рабочие примеры наследования по одной таблице, объединенной таблице и конкретной таблице, как описано в Отображение иерархий наследования классов.

Listing of files:

  • joined.py - Joined-table (table-per-subclass) inheritance example.

  • single.py - Single-table (table-per-hierarchy) inheritance example.

  • concrete.py - Concrete-table (table-per-class) inheritance example.

Специальные API

Инструментарий атрибутов

Примеры, иллюстрирующие модификации системы управления атрибутами SQLAlchemy.

Listing of files:

Горизонтальное разделение

Базовый пример использования SQLAlchemy Sharding API. Под шардингом понимается горизонтальное масштабирование данных по нескольким базам данных.

Основными компонентами «шардированного» отображения являются:

  • несколько экземпляров Engine, каждому из которых присвоен «идентификатор осколка». Эти Engine экземпляры могут относиться к разным базам данных, или к разным схемам / учетным записям в одной базе данных, или они могут даже различаться только опциями, которые заставят их обращаться к разным схемам или таблицам при использовании.

  • функция, которая может вернуть идентификатор одного осколка, учитывая экземпляр для сохранения; эта функция называется «shard_chooser»

  • функция, которая может вернуть список идентификаторов осколков, относящихся к определенному идентификатору экземпляра; эта функция называется «id_chooser». Если она возвращает все идентификаторы осколков, поиск будет произведен во всех осколках.

  • функция, которая может вернуть список идентификаторов осколков, которые нужно попробовать, задав определенный запрос («query_chooser»). Если функция возвращает идентификаторы всех осколков, то будут запрошены все осколки, а результаты будут объединены.

В этих примерах различные виды шардов используются в одном и том же базовом примере, в котором хранятся данные о погоде по каждому континенту. Мы приводим примеры функций shard_chooser, id_chooser и query_chooser. Функция query_chooser иллюстрирует проверку элемента выражения SQL, чтобы попытаться определить один запрашиваемый осколок.

Создание общих процедур шардинга - это амбициозный подход к проблеме организации экземпляров между несколькими базами данных. В качестве более понятной альтернативы можно использовать подход «distinct entity» - простой метод явного назначения объектов различным таблицам (и потенциально узлам базы данных), описанный в вики по адресу EntityName.

Listing of files:

  • separate_tables.py - Illustrates sharding using a single SQLite database, that will however have multiple tables using a naming convention.

  • separate_schema_translates.py - Illustrates sharding using a single database with multiple schemas, where a different «schema_translates_map» can be used for each shard.

  • asyncio.py - Illustrates sharding API used with asyncio.

  • separate_databases.py - Illustrates sharding using distinct SQLite databases.

Расширение ORM

События запросов ORM

Рецепты, иллюстрирующие расширение поведения ORM SELECT, используемого Session.execute(), с помощью 2.0 style использования select(), а также объекта 1.x style Query.

Примеры включают демонстрацию опции with_loader_criteria(), а также крючка SessionEvents.do_orm_execute().

Начиная с версии SQLAlchemy 1.4, конструкция Query объединена с конструкцией Select, так что эти два объекта в основном одинаковы.

Listing of files:

  • temporal_range.py - Illustrates a custom per-query criteria that will be applied to selected entities.

  • filter_public.py - Illustrates a global criteria applied to entities of a particular type.

Кэширование Dogpile

Иллюстрирует, как встроить функциональность dogpile.cache в ORM-запросы, позволяя полностью контролировать кэш, а также извлекать атрибуты «ленивой загрузки» из долговременного кэша.

В этой демонстрации показаны следующие техники:

  • Использование крючка события SessionEvents.do_orm_execute()

  • Базовая техника обхода Session.execute() для извлечения из пользовательского источника кэша вместо базы данных.

  • Рудиментарное кэширование с помощью dogpile.cache, использование «регионов», которые позволяют глобально управлять фиксированным набором конфигураций.

  • Использование пользовательских объектов UserDefinedOption для настройки опций в объекте statement.

См.также

Повторное выполнение заявлений - включает общий пример представленной здесь техники.

Например:

# query for Person objects, specifying cache
stmt = select(Person).options(FromCache("default"))

# specify that each Person's "addresses" collection comes from
# cache too
stmt = stmt.options(RelationshipCache(Person.addresses, "default"))

# execute and results
result = session.execute(stmt)

print(result.scalars().all())

Для запуска SQLAlchemy и dogpile.cache должны быть установлены или находиться в текущем PYTHONPATH. Демонстрация создаст локальный каталог для файлов данных, вставит начальные данные и запустится. При повторном запуске демонстрации будут использованы уже имеющиеся файлы кэша, и будет выдан ровно один SQL-запрос к двум таблицам - однако отображаемый результат будет использовать десятки ленивых загрузок, которые все будут брать данные из кэша.

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

python -m examples.dogpile_caching.helloworld

python -m examples.dogpile_caching.relationship_caching

python -m examples.dogpile_caching.advanced

python -m examples.dogpile_caching.local_session_caching

Listing of files:

  • environment.py - Establish data / cache file paths, and configurations, bootstrap fixture data if necessary.

  • caching_query.py - Represent functions and classes which allow the usage of Dogpile caching with SQLAlchemy. Introduces a query option called FromCache.

  • model.py - The datamodel, which represents Person that has multiple Address objects, each with PostalCode, City, Country.

  • fixture_data.py - Installs some sample data. Here we have a handful of postal codes for a few US/Canadian cities. Then, 100 Person records are installed, each with a randomly selected postal code.

  • helloworld.py - Illustrate how to load some data, and cache the results.

  • relationship_caching.py - Illustrates how to add cache options on relationship endpoints, so that lazyloads load from cache.

  • advanced.py - Illustrate usage of Query combined with the FromCache option, including front-end loading, cache invalidation and collection caching.

  • local_session_caching.py - This example creates a new dogpile.cache backend that will persist data in a dictionary which is local to the current session. remove() the session and the cache is gone.

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