Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

SQLAlchemy

Глава 20 из книги "Архитектура приложений с открытым исходным кодом", том 2.

Оригинал: SQLAlchemy
Автор: Michael Bayer
Перевод: А. Панин

20.6. Отображение классов при использовании объектно-реляционного отображения

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

В SQLAlchemy такая связь называется "отображением", что соответствует широко известному шаблону проектирования с названием "DataMapper", описанному в книге Martin Flower с названием "Patterns of Enterprise Architecture". В целом, система объектно-реляционного отображения SQLAlchemy была разработана с применением большого количества приемов, которые описал в своей книге Martin Flower. Она также подверглась значительному влиянию со стороны известной системы реляционного отображения Hibernate для языка программирования Java и продукта SQLObject для языка программирования Python от Ian Bicking.

Классическое отображение против декларативного отображения

Мы используем термин "классическое отображение" для указания на систему объектно-реляционного отображения данных для существующего пользовательского класса в рамках SQLAlchemy. Эта форма отображения использует объект Table и заданный пользователем класс для формирования двух связанных с помощью функции с именем mapper, но при этом отдельных, примитивов. Как только функция mapper применяется по отношению к заданному пользователем классу, класс приобретает новые атрибуты, соответствующие столбцам таблицы:
class User(object):
    pass

mapper(User, user_table)

# теперь у объекта User есть атрибут ".id"
User.id

Функция mapper также может добавлять другие атрибуты классу, включая атрибуты, соответствующие как ссылкам на другие типы объектов, так и на произвольные SQL-запросы. Процесс добавления произвольных атрибутов классу в мире Python известен под названием "monkeypatching"; однако, так как мы выполняем его на основе данных и не привольным образом, суть процесса гораздо лучше может быть обозначена термином "доработка класса" ("class instrumentation").

Современный подход к работе с SQLAlchemy базируется на использовании декларативного расширения, которое представляет собой систему конфигурации, имеющую сходство с известной, похожей на active record системой декларативного описания классов, используемой во множестве других инструментов объектно-реляционного отображения. В этой системе конечный пользователь явно задает описание атрибута в процессе создания описания класса, каждое из которых представляет атрибут класса, который должен быть использован при создании отображения. Объект Table в большинстве случаев не упоминается ни явно, ни при использовании функции mapper; явно упоминается только пользовательский класс, объекты Column и другие относящиеся к отображению атрибуты:
class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)

При рассмотрении приведенного выше фрагмента кода может показаться, что добавление атрибутов в класс осуществляется непосредственно в строке id = Column(), но на самом деле это не так. Декларативное расширение использует метакласс Python, с помощью которого очень удобно выполнять серии операций каждый раз, когда новый класс впервые декларируется с целью генерации нового объекта Table на основе декларации и передачи его функции mapper вместе с классом. После этого функция mapper выполняет работу точно таким же образом, добавляя набор атрибутов в класс, причем в данном случае используется атрибут id, а также заменяя атрибуты, которые были добавлены ранее. Ко времени окончания процесса инициализации метакласса (т.е., к моменту, когда поток выполнения покидает блок описания класса User), объект Column с атрибутом id перемещается в новый объект Table и User.id заменяется на новый атрибут, специфичный для отображения.

Всегда считалось, что система SQLAlchemy должна иметь четкую декларативную форму конфигурации. Однако, создание декларативного расширения задерживалось из-за продолжающейся работы по стабилизации механизмов классического отображения. Ранее существовало временное расширение под названием ActiveMapper, которое впоследствии было преобразовано в проект Elixir. Оно позволяло повторно объявлять конструкции отражения в рамках декларативной системы более высокого уровня. Задача декларативного расширения была противоположной задаче проекта Elixir и заключалась в создании мощной абстракции путем создания системы, которая практически полностью сохраняет классические концепции SQLAlchemy в области отображения данных, допуская реорганизацию метода использования для сокращения объема отладочной информации и повышения совместимости с расширениями уровня классов по сравнению с возможностями классических систем отображения.

Вне зависимости от того, используется ли классическое или декларативное отображение, используемый для отображения класс получает новые возможности, которые позволяют ему работать с синтаксическими конструкциями SQL, представленными в форме атрибутов. Изначально принцип использования специального атрибута в качестве источника SQL-запросов для столбцов в SQLAlchemy соответствовал принципу использования такового в SQLObject и заключался в использовании при работе с SQLAlchemy атрибута .c, как показано в этом примере:
result = session.query(User).filter(User.c.username == 'ed').all()
Однако, в версии 0.4 SQLAlchemy эти функции были возложены непосредственно на отображаемые атрибуты:
result = session.query(User).filter(User.username == 'ed').all()

Это изменение метода доступа к атрибутам оказалось значительным усовершенствованием, так как оно позволило аналогичным объектам столбцов объектам находиться в классе для достижения дополнительных, специфичных для класса возможностей, которые не доступны для классов, наследуемых напрямую от находящегося уровнем ниже класса Table. Оно также позволило интегрировать различные типы атрибутов классов, такие, как атрибуты, ссылающиеся непосредственно на столбцы таблицы, атрибуты, ссылающиеся на SQL-запросы, созданные на основе этих столбцов, а также атрибуты, ссылающиеся на соответствующий класс. Наконец, оно позволило достичь соответствия между классом для отображения и экземпляром этого класса для отображения, в котором тот же атрибут может быть предназначен для работы с другой информацией в зависимости от родительского класса. Атрибуты классов возвращают SQL-запросы, в то время, как атрибуты экземпляров классов возвращают реальные данные.

Анатомия отображения

Атрибут id, который был привязан к нашему классу User, является объектом, тип которого известен в рамках языка программирования Python как дескриптор (descriptor), причем этот объект поддерживает методы __get__, __set__ и __del__, которые интерпретатор Python позволяет использовать во всех операциях, связанных с этим классом или его экземпляром. Реализация в рамках SQLAlchemy известна под именем InstrumentedAttribute и мы продемонстрируем механизмы, скрытые за этим фасадным классом, в ходе рассмотрения следующего примера. Начиная работу с описания класса Table, а также пользовательского класса, мы начинаем формирование отображения только для одного столбца, а также используем функцию relationship, которая задает ссылку на связанный класс:
user_table = Table("user", metadata,
    Column('id', Integer, primary_key=True),
)

class User(object):
    pass

mapper(User, user_table, properties={
    'related':relationship(Address)
})

После создания отображения структура относящихся к классу объектов будет соответствовать представленной на Рисунке 20.11.

Анатомия отображения
Рисунок 20.11: Анатомия отображения

На рисунке проиллюстрировано созданное с помощью SQLAlchemy отображение, использующее два отдельных уровня для осуществления взаимодействия заданного пользователем класса и метаданных таблицы, с которой он связан. Инструментарий, предназначенный для реализации функций класса представлен в левой части рисунка, в то время, как функции для работы с базой данных и SQL-запросами - в правой части. Основной используемый шаблон проектирования подразумевает то, что композиция объектов используется для разделения моделей повеления объектов, а наследование объектов используется для разделения вариаций поведения объектов в рамках определенной роли.

В области инструментария класса следует выделить класс ClassManager, который связан с используемым для отображения классом, причем каждый объект InstrumentedAttribute из его коллекции связан с каждым отображаемым атрибутом класса. InstrumentedAttribute также является упомянутым ранее общедоступным дескриптором языка Python и позволяет формировать SQL-запросы при использовании совместно с запросами на основе классов (т.е., User.id==5). При разговоре об экземпляре класса User, следует упомянуть о том, что объект InstrumentedAttribute делегирует поддержку атрибута объекту AttributeImpl, который является одним из нескольких аналогичных объектов, выбранным в соответствии с типом представляемых данных.

Со стороны отображения объект Mapper представляет связь заданного пользователем класса и выбираемого элемента базы данных, чаще всего объекта таблицы Table. Объект Mapper поддерживает коллекцию объектов, известных как MapperProperty и соответствующих каждому из атрибутов, которые отвечают за представление определенного атрибута в SQL-запросе. Наиболее часто встречающимися вариантами объектов MapperProperty являются объекты ColumnProperty, представляющие столбцы в SQL-запросе, а также объекты RelationshipProperty, представляющие связь с другим классом отображения.

Объект MapperProperty делегирует функции загрузки атрибутов, включая функции преобразования атрибутов в фрагменты SQL-запроса и функции их извлечения из строк результатов запросов, объекту LoaderStrategy, который также может быть представлен несколькими вариантами. Различные объекты LoaderStrategies устанавливают методы загрузки атрибута в соответствии с вариантами: deferred (отложенная загрузка), eager (быстрая загрузка) или immediate (немедленная загрузка). Стандартная версия поведения выбирается во время конфигурации отображения, при этом дополнительным вариантом является использование альтернативной стратегии в момент выполнения запроса. Объект RelationshipProperty также ссылается на объект DependencyProcessor, который устанавливает метод обработки зависимостей между отображениями и метод синхронизации атрибутов для применения во время сохранения данных. Выбор объекта DependencyProcessor основывается на параметрах отношения родительских и целевых элементов, связанных друг с другом.

Структура Mapper/RelationshipProperty формирует граф, в котором объекты Mapper являются узлами, а объекты RelationshipProperty - ориентированными ребрами. После того, как полный набор функций отображения декларируется приложением, наступает этап отложенной "инициализации", известный как процесс конфигурации (configuration). Данные, собираемые в ходе выполнения этого процесса, главным образом используются каждым объектом RelationshipProperty для уточнения параметров, используемых функциями отображения родительских (parent) и целевых (target) объектов, причем также производится выбор объекта AttributeImpl наряду с объектом DependencyProcessor. Этот граф является ключевой структурой данных, используемой при выполнении операции объектно-реляционного отображения. Он участвует в операциях, использующих так называемые "каскады", которые устанавливают последовательность выполнения функций на основе последовательностей объектов в операциях запросов, в которых связанные объекты и коллекции объектов "быстро" одновременно загружаются, а также в процессах сохранения данных объектов, когда граф зависимостей для всех объектов создается до того, как будут завершены этапы освобождения выделенных для хранения данных ресурсов.


Продолжение статьи: Методы выполнения запросов и загрузки данных