Как я могу имитировать 'select_related', используя google-appengine и django-nonrel? - PullRequest
2 голосов
/ 22 августа 2011

Документация django nonrel гласит: «Вы должны вручную написать код для объединения результатов нескольких запросов (JOINs, select_related () и т. Д.)».

Может ли кто-нибудь указать мне на фрагменты, которые вручную добавляют связанные данные? У @nickjohnson есть отличный пост , показывающий, как это сделать с прямыми моделями AppEngine, но я использую django-nonrel.

Для моего конкретного использования я пытаюсь получить UserProfiles со связанными с ними моделями пользователей. Это должны быть просто два простых запроса, а затем сопоставить данные.

Однако, используя django-nonrel, новый запрос запускается для каждого результата в наборе запросов. Как я могу получить доступ к связанным элементам в виде «select_related»?

Я пробовал это, но, похоже, не работает, как я ожидал. Глядя на статистику rpc, кажется, что он запускает запрос для каждого отображаемого элемента.

all_profiles = UserProfile.objects.all()
user_pks = set()
for profile in all_profiles: 
    user_pks.add(profile.user_id)  # a way to access the pk without triggering the query

users = User.objects.filter(pk__in=user_pks)
for profile in all_profiles:
    profile.user = get_matching_model(profile.user_id, users)


def get_matching_model(key, queryset):
    """Generator expression to get the next match for a given key"""
    try:
        return (model for model in queryset if model.pk == key).next()
    except StopIteration:
        return None

UPDATE: Я ... я понял, в чем моя проблема.

Я пытался улучшить эффективность changelist_view в администраторе django. Казалось, что приведенная выше логика select_related все еще производит дополнительные запросы для каждой строки в наборе результатов, когда внешний ключ был в моем «display_list». Однако я проследил это до чего-то другого. Вышеприведенная логика не создает несколько запросов (но если вы более точно подражаете способу Ника Джонсона, это будет выглядеть намного красивее).

Проблема в том, что в django.contrib.admin.views.main на строке 117 внутри метода ChangeList есть следующий код: result_list = self.query_set._clone(). Таким образом, хотя я правильно переопределил набор запросов в админке и выбрал связанный материал, этот метод вызывал клон набора запросов, который НЕ сохраняет атрибуты в модели, которые я добавил для моей «select related», в результате чего даже более неэффективная загрузка страницы, чем когда я начинал.

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

1 Ответ

1 голос
/ 27 августа 2011

Мне не нравится отвечать на мой собственный вопрос, но ответ может помочь другим.

Вот мое решение, которое получит связанные элементы в наборе запросов полностью на основе решения Ника Джонсона, связанного выше.


from collections import defaultdict

def get_with_related(queryset, *attrs):
    """
    Adds related attributes to a queryset in a more efficient way
    than simply triggering the new query on access at runtime.

    attrs must be valid either foreign keys or one to one fields on the queryset model
    """
    # Makes a list of the entity and related attribute to grab for all possibilities
    fields = [(model, attr) for model in queryset for attr in attrs]

    # we'll need to make one query for each related attribute because
    # I don't know how to get everything at once. So, we make a list
    # of the attribute to fetch and pks to fetch.
    ref_keys = defaultdict(list)
    for model, attr in fields:
        ref_keys[attr].append(get_value_for_datastore(model, attr))

    # now make the actual queries for each attribute and store the results
    # in a dict of {pk: model} for easy matching later
    ref_models = {}
    for attr, pk_vals in ref_keys.items():
        related_queryset = queryset.model._meta.get_field(attr).rel.to.objects.filter(pk__in=set(pk_vals))
        ref_models[attr] = dict((x.pk, x) for x in related_queryset)

    # Finally put related items on their models
    for model, attr in fields:
        setattr(model, attr, ref_models[attr].get(get_value_for_datastore(model, attr)))

    return queryset

def get_value_for_datastore(model, attr):
    """
    Django's foreign key fields all have attributes 'field_id' where
    you can access the pk of the related field without grabbing the
    actual value.
    """
    return getattr(model, attr + '_id')

Чтобы иметь возможность изменять набор запросов в админке, чтобы использовать связанное с выбором, мы должны перепрыгнуть через пару обручей.Вот что я сделал.Единственное, что изменилось в методе get_results в AppEngineRelatedChangeList, это то, что я удалил self.query_set._clone () и просто использовал self.query_set.


class UserProfileAdmin(admin.ModelAdmin):
    list_display = ('username', 'user', 'paid')
    select_related_fields = ['user']

    def get_changelist(self, request, **kwargs):
        return AppEngineRelatedChangeList

class AppEngineRelatedChangeList(ChangeList):

    def get_query_set(self):
        qs = super(AppEngineRelatedChangeList, self).get_query_set()
        related_fields = getattr(self.model_admin, 'select_related_fields', [])
        return get_with_related(qs, *related_fields)

    def get_results(self, request):
        paginator = self.model_admin.get_paginator(request, self.query_set, self.list_per_page)
        # Get the number of objects, with admin filters applied.
        result_count = paginator.count

        # Get the total number of objects, with no admin filters applied.
        # Perform a slight optimization: Check to see whether any filters were
        # given. If not, use paginator.hits to calculate the number of objects,
        # because we've already done paginator.hits and the value is cached.
        if not self.query_set.query.where:
            full_result_count = result_count
        else:
            full_result_count = self.root_query_set.count()

        can_show_all = result_count  self.list_per_page

        # Get the list of objects to display on this page.
        if (self.show_all and can_show_all) or not multi_page:
            result_list = self.query_set
        else:
            try:
                result_list = paginator.page(self.page_num+1).object_list
            except InvalidPage:
                raise IncorrectLookupParameters

        self.result_count = result_count
        self.full_result_count = full_result_count
        self.result_list = result_list
        self.can_show_all = can_show_all
        self.multi_page = multi_page
        self.paginator = paginator
...