#python #sqlalchemy #eager-loading
#python #sqlalchemy #нетерпеливая загрузка
Вопрос:
У меня есть некоторая структура базы данных; поскольку большая ее часть для нас не имеет значения, я опишу только некоторые релевантные фрагменты. Давайте рассмотрим объект Item в качестве примера:
items_table = Таблица("invtypes", gdata_meta, Столбец ("typeID", целое число, primary_key = True), Столбец ("typeName", строка, индекс= True), Столбец ("marketGroupID", целое число, ForeignKey("invmarketgroups.marketGroupID")), Столбец ("groupId", целое число, ForeignKey("invgroups.groupId"), индекс = True)) mapper(элемент, items_table, свойства = {"группа": отношение (группа, обратная ссылка = "элементы"), "_Item__attributes" : отношение (атрибут, collection_class = attribute_mapped_collection('name')), "эффекты": отношение (эффект, collection_class = attribute_mapped_collection('name')), "Метагруппа": отношение (метатип, primaryjoin = metatypes_table.c.typeID == items_table.c.typeID, uselist = False), "ID": синоним ("typeID"), "name" : синоним("typeName")})
Я хочу добиться некоторых улучшений производительности на уровне sqlalchemy / базы данных, и у меня есть пара идей: 1) Запрашивать один и тот же элемент дважды:
элемент = сессия.запрос (Item).get(11184) элемент = Отсутствует (ссылка на элемент потеряна, объект собран как мусор) элемент = сессия.запрос (Item).get(11184)
Каждый запрос генерирует и выдает SQL-запрос. Чтобы избежать этого, я использую 2 пользовательские карты для объекта item:
itemMapId = {} Имя_элемента = {} @cachedQuery(1, "поиск") определить GetItem(поиск, нетерпеливый = Нет): if isinstance(lookfor, (int, float)): id = int (поиск) если eager равен None и id в itemMapId: item = itemMapId[идентификатор] ещё: элемент = сессия.запрос (Item).options(*processEager(нетерпеливый)).get(идентификатор) itemMapId[элемент.ID] = item itemMapName[item.name ] = элемент elif isinstance(поиск, базовая строка): если eager равен None и ищите в itemMapName: item = имя_элемента[поиск] ещё: # Элементы имеют уникальные имена, поэтому мы можем получить только первый результат без обеспечения его уникальности элемент = сессия.запрос (Item).options(*processEager (нетерпеливый)).фильтр(Item.name == поиск).first() itemMapId[элемент.ID] = item itemMapName[item.name ] = элемент возвращаемый элемент
Я полагаю, что sqlalchemy выполняет аналогичное отслеживание объектов, по крайней мере, по первичному ключу (item.ID ). Если это произойдет, я могу стереть обе карты (хотя удаление карты имен потребует незначительных изменений в приложении, которое использует эти запросы), чтобы не дублировать функциональность и использовать стандартные методы. Актуальный вопрос: если в sqlalchemy есть такая функциональность, как получить к ней доступ?
2) Быстрая загрузка отношений часто помогает сэкономить много запросов к базе данных. Скажем, мне определенно понадобится следующий набор свойств item=Item ():
item.group (Объект группы, согласно groupId нашего элемента) item.group.items (извлекает все элементы из списка элементов нашей группы) item.group.items.metaGroup (объект метагруппы / отношение для каждого элемента в списке)
Если у меня есть некоторый идентификатор элемента, но ни один элемент еще не загружен, я могу запросить его из базы данных, охотно загружая все, что мне нужно: sqlalchemy объединит group, ее элементы и соответствующие метагруппы в рамках одного запроса. Если бы я обращался к ним с отложенной загрузкой по умолчанию, sqlalchemy нужно было бы выполнить 1 запрос, чтобы захватить элемент 1, чтобы получить группу 1 * # элементы для всех элементов в списке 1 * # элементы, чтобы получить метагруппу каждого элемента, что является расточительным.
2.1) Но что, если у меня уже есть извлеченный объект Item, и некоторые свойства, которые я хочу загрузить, уже загружены? Насколько я понимаю, когда я повторно извлекаю некоторый объект из базы данных — его уже загруженные отношения не становятся выгруженными, я прав?
2.2) Если у меня есть объект Item, и я хочу получить доступ к его группе, я могу просто getGroup используя item.groupId , применяя любые необходимые мне инструкции («items» и «items.metaGroup»). Он должен правильно загружать group и ее запрошенные отношения, не касаясь содержимого элемента. Будет ли sqlalchemy правильно сопоставлять эту выбранную группу с item.group, чтобы при доступе к item.group она ничего не извлекала из базовой базы данных?
2.3) Если у меня есть следующие вещи, извлеченные из базы данных: исходный элемент, item.group и некоторая часть элементов из списка item.group.items, некоторые из которых могут иметь загруженную метагруппу, какая была бы наилучшая стратегия для завершения структуры данных так же, как в списке eagle выше: повторно извлеките группу с помощью («items», «items.metaGroup»), которая требует загрузки, или проверьте каждый элемент из списка items по отдельности, и если элемент или его метагруппа не загружены — загрузите их? Похоже, это зависит от ситуации, потому что, если все уже было загружено некоторое время назад — выдача такого сложного запроса бессмысленна. Предоставляет ли sqlalchemy способ отслеживать, загружено ли какое-либо объектное отношение, с возможностью просмотра глубже, чем только на один уровень?
В качестве иллюстрации к 2.3 — я могу извлекать группу с идентификатором 83, охотно извлекая «элементы» и «items.metaGroup». Есть ли способ определить по элементу (у которого groupId равен 83), загружены ли у него «group», «group.items» и «group.items.metaGroup», используя инструменты sqlalchemy (в этом случае все они должны быть загружены)?
Комментарии:
1. Начиная с элемента. ID — это основной ключ, который вы должны иметь возможность заменить .filter (Элемент. ID == int(lookfor)).one() с помощью .get(int(lookfor))
2. Спасибо за совет, я попробую такую замену. Причина использования такой конструкции проста — я вырезал много нерелевантного кода из примера, в полной версии выборка объекта может быть выполнена не только через первичный ключ.
Ответ №1:
Чтобы принудительно загрузить отложенные атрибуты, просто обратитесь к ним. Это самый простой способ, и он отлично работает для отношений, но не так эффективен для Column
s (вы получите отдельный SQL-запрос для каждого столбца в той же таблице). Вы можете получить список всех незагруженных свойств (как отношений, так и столбцов) из sqlalchemy.orm.attributes.instance_state(obj).unloaded
.
В вашем примере вы не используете отложенные столбцы, но я опишу их здесь для полноты картины. Типичный сценарий для обработки отложенных столбцов следующий:
- Украсьте выбранные столбцы
deferred()
. Объедините их в одну или несколько групп, используяgroup
параметр todeferred()
. - При желании используйте опции
undefer()
иundefer_group()
в запросе. - Доступ к отложенному столбцу, помещенному в группу, загрузит все столбцы в этой группе.
К сожалению, это не работает наоборот: вы можете объединять столбцы в группы, не откладывая их загрузку по умолчанию с помощью column_property(Column(…), group=…)
, но defer()
опция не повлияет на них (она работает только для Column
s, а не свойств столбца, по крайней мере, в 0.6.7).
Принудительная загрузка отложенных свойств столбца session.refresh(obj, attribute_names=…)
, предложенная Натаном Виллаескузой, вероятно, является лучшим решением. Единственный недостаток, который я вижу, заключается в том, что сначала истекает срок действия атрибутов, поэтому вы должны убедиться, что среди переданных в качестве attribute_names
аргумента атрибутов нет загруженных (например, с помощью пересечения с state.unloaded
).
Обновить
1) SQLAlchemy отслеживает загруженные объекты. Вот как работает ORM: в сеансе должен быть единственный объект для каждого идентификатора. Его внутренний кэш по умолчанию слаб (используйте weak_identity_map=False
, чтобы изменить это), поэтому объект удаляется из кэша, как только в вашем коде нет ссылки на него. SQLAlchemy не будет выполнять SQL-запрос для query.get(pk)
, когда объект уже находится в сеансе. Но это работает только для get()
метода, поэтому query.filter_by(id=pk).first()
будет выполнен SQL-запрос и обновлен объект в сеансе с загруженными данными.
2) Быстрая загрузка отношений приведет к меньшему количеству запросов, но это не всегда быстрее. Вы должны проверить это для вашей базы данных и данных.
2.1) Повторная выборка данных из базы данных не приведет к выгрузке объектов, связанных через отношения.
2.2) item.group
загружается с помощью query.get()
метода, поэтому не приведет к SQL-запросу, если объект уже находится в сеансе.
2.3) Да, это зависит от ситуации. В большинстве случаев лучше всего надеяться, что SQLAlchemy будет использовать правильную стратегию :). Для уже загруженного отношения вы можете проверить, загружены ли отношения связанных объектов через state.unloaded
и так рекурсивно на любую глубину. Но когда отношение еще не загружено, вы не можете узнать, загружены ли уже связанные объекты и их отношения: даже когда отношение еще не загружено, связанные объекты могут быть уже в сеансе (просто представьте, что вы запрашиваете первый элемент, загружаете его группу, а затем запрашиваете другой элемент, имеющий ту же группу). Для вашего конкретного примера я не вижу проблемы просто проверить state.unloaded
рекурсивно.
Комментарии:
1. Как я упоминал в комментарии к ответу Натана Виллаескузы, доступ к атрибуту подойдет, если мне нужно загрузить только отношения верхнего уровня без углубления. Что касается незагруженного набора атрибутов — спасибо. Существуют ли документы sqlalchemy InstanceState, в которых перечислены такие атрибуты, как ‘незагруженный’? Я не нашел таких документов на официальном сайте, но, похоже, мне нужны такие низкоуровневые функции для правильного завершения объекта и другой экономии производительности.
2. Не могли бы вы более подробно изложить свои требования, обновив вопрос? Простая рекурсивная загрузка без ограничения глубины не кажется разумной: всего одно свойство relation может привести к загрузке всей базы данных в память в некоторых приложениях.
3. Я только что провел несколько экспериментов: 2.1 и 2.2 кажутся верными (т. Е. в обоих случаях повторной выборки не происходит). Хотя все еще нужен ответ для 2.3 и 1.
4. @DarkPhoenix Я обновил ответ, надеюсь, достаточно подробный.
5. Спасибо, потрясающе — это охватывает все, что мне нужно.
Ответ №2:
1) Из документации к сеансу:
[Сеанс] в некоторой степени используется в качестве кэша, поскольку он реализует шаблон identity map и хранит объекты, привязанные к их первичному ключу. Однако он не выполняет никакого кэширования запросов. … Только когда вы говорите query.get({некоторый первичный ключ}), сеанс не должен выдавать запрос.
2.1) Вы правы, отношения не изменяются при обновлении объекта.
2.2) Да, группа будет в карте идентификаторов.
2.3) Я полагаю, что лучшим вариантом будет попытаться перезагрузить всю group.items в одном запросе. По моему опыту, обычно намного быстрее выполнить один большой запрос, чем несколько меньших. Единственный раз, когда имело бы смысл перезагрузить только определенную group.item, — это когда нужно было загрузить именно один из них. Хотя в этом случае вы выполняете один большой запрос вместо одного маленького, поэтому вы фактически не уменьшаете количество запросов.
Я не пробовал это, но я считаю, что вы должны иметь возможность использовать метод sqlalchemy.orm.util.identity_key, чтобы определить, находится ли объект в карте идентификаторов sqlalchemy. Мне было бы интересно узнать, что возвращает вызов identiy_key(Group, 83).
Первоначальный вопрос) Если я правильно понимаю, у вас есть объект, который вы извлекли из базы данных, где были загружены некоторые из его отношений, и вы хотели бы получить остальные отношения с помощью одного запроса? Я полагаю, вы сможете использовать метод Session.refresh(), передающий имена отношений, которые вы хотите загрузить.
Комментарии:
1. Да, это правильно — я уже извлек объект из базы данных и хочу получить его отношения в одном запросе (или 1 запрос на отношение, это не имеет значения, пока sqlalchemy может присваивать извлеченные свойства объекту, с которым я работаю). Я попытался обновить, как вы предложили, sqlalchemy позволяет мне делать это только для столбцов в базе данных, а не для отношений (я могу обновить groupId объекта item, но я не могу обновить групповую связь). InvalidRequestError: для операции обновления не указаны свойства на основе столбцов. Используйте session.expire() для перезагрузки коллекций и связанных элементов.
2. Я думаю, что могу попробовать запустить отложенную загрузку требуемых свойств и отношений (это означало бы, что будет выдан 1 запрос на объект), но что, если я захочу продвинуться на несколько уровней глубже в уровне отношений? Давайте возьмем элемент в качестве примера, я могу запустить отложенную загрузку, обратившись к item.group <1-й запрос>, затем я хочу, чтобы sqlalchemy извлекал все элементы из списка объектов item.group.items < 1> и метагруппировал для каждого элемента < # элементов> (это выглядело бы как («group», «group.items», «group.items.metaGroup») для быстрой загрузки) — есть ли способ сделать это, используя 1 запрос для «group» свойство элемента?
3. Вы могли бы сделать group = Session. запрос(item.group).options(eagerload(…)) и полная перезагрузка группы, частью которой является элемент. Мне любопытно, будет ли в этот момент item.group == group.
4. Я только что проверил — да, внутренние сопоставления sqlalchemy правильно отражают загруженный объект (то есть item.group == group). Есть ли какой-либо способ определить, извлечено ли какое-либо отношение (скажем, item.group) уже из базы данных (чтобы избежать повторной выборки данных)?
5. Я считаю, что это возможно. Смотрите мой пересмотренный вопрос.