Django:来自非嵌套模型的嵌套查询

时间:2009-09-03 14:32:54

标签: django django-models

我正在尝试找出从一组非嵌套模型派生嵌套菜单的最佳方法。给定这样的布局:

class Beverage(models.Model):
   country = models.ForeignKey(Country,null=True,blank=True)    
   region = models.ForeignKey(Region,null=True,blank=True)
   subregion = models.ForeignKey(SubRegion,null=True,blank=True) 
   in_stock = models.BooleanField()
   ...

结果菜单将类似于:

France
    Region 1
        Subregion 1
        Subregion 2
    Region 2
        Subregion 3
        Subregion 4
Spain
    ....

如果没有没有库存的饮料,菜单中不应出现任何国家,地区或次区域。由于子区域始终属于某个区域,而某个区域始终属于某个国家/地区,因此我最初的方法是将模型本身嵌套,并仅将SubRegion放在Beverage上。然后区域和国家将始终为饮料的子区域所知。不幸的是,有太多现实世界的例外情况使这个可行 - 葡萄酒有一个地区而不是一个分区域等等。所以我将布局压平如上。

现在的问题是如何从这个模型布局中导出菜单。它看起来像一个深度嵌套的查询集的列表将是要走的路,但这似乎是计算上昂贵和复杂的代码。有更干净的方式吗?

3 个答案:

答案 0 :(得分:1)

我过去用来解决类似问题的一个过程是选择所有具有单个查询的项目,基于国家/地区,然后是区域,然后是子区域。然后循环查询结果并维护指向您在国家和地区看到的最后一个ID的变量。如果饮料上的下一个国家/地区ID与上一个ID不匹配,则保存旧列表并开始新列表。这里有一些非常粗糙/凌乱的pythoncode来解释这个想法:

beverages = Beverage.objects.order_by('country', 'region', 'subregion')
last_country = -1
menu = []
country_obj = None
for beverage in beverages:
    if beverage.country_id != last_country:
        if country_obj is not None:
            if region_obj is not None:
                if subregion_obj is not None:
                    region_obj['children'].append(subregion_obj)
                country_obj['children'].append(region_obj)
            menu.append(country_obj)
        country_obj = {'name': beverage.country.name, 'children': []}
        last_country = beverage.country_id
        last_region = -1
        region_obj = None
        last_subregion = -1
        subregion_obj = None
    if beverage.region is None:
        country_obj['children'].append(beverage)    
    else:
        if beverage.region_id != last_region:
            if region_obj is not None:
                if subregion_obj is not None:
                    region_obj['children'].append(subregion_obj)
                country_obj['children'].append(region_obj)
            region_obj = {'name': beverage.region.name, 'children': []}
            last_region = beverage.region_id
            last_subregion = -1
            subregion_obj = None
        if beverage.subregion is None:
            region_obj['children'].append(beverage)
        else:
            if beverage.subregion_id != last_subregion:
                if subregion_obj is not None:
                    region_obj['children'].append(subregion_obj)
                subregion_obj = {'name': beverage.subregion.name, 'children': []}
                last_subregion = beverage.subregion_id
            subregion_obj['children'].append(beverage)
if beverage.subregion is not None:
    region_obj['children'].append(subregion_obj)
if beverage.region is not None:
    country_obj['children'].append(region_obj)
menu.append(country_obj)

正如您可能知道的那样,每个级别都具有相同的逻辑:检查id是否已更改,是否已附加旧x_obj并启动新的x_obj。最后五行是处理最后一个饮料,因为您总是在当前迭代期间保存前一个项目(并且最后一个项目没有下一次迭代)。这是非常粗糙的边缘,但这是我一直使用的过程只需要一个查询。

我编辑修复了当我最终运行它时发现的一些错误。它似乎适用于我的简单测试用例。

答案 1 :(得分:1)

两个想法

  • 要使您的第一个方法有效,您可以将GenericForeignKeys保存到Country,Region或SubRegion。或者什么都可以确定原点。使用limit_choices_toQ objects来控制,可以添加哪些类型。

代码:

content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
origin = generic.GenericForeignKey('content_type', 'object_id', 
                                   limit_choices_to = \
                              Q(name='contry', app_label='what ever is the name of the app')| \
                              Q(name='region', app_label='what ever is the name of the app')| \
                              Q(name='subregion', app_label='what ever is the name of the app')')))
  • 或者我的第二个想法:首先不要优化db-query - 使用一些缓存。

    您可以先查询国家/地区,遍历此集并查询此国家/地区的区域,然后在不同的循环中编写菜单。

    这会导致许多db命中,但代码会非常简单。

    由于您不会对每个站点请求执行此计算,因此您应该将菜单写入全局变量。此计算可以在形成菜单的模型中进行任何保存或删除操作。所以你可以通过signaling控制它。

    但请注意:信号和全局变量仅适用于流程范围。但也许网络服务器跨越了几个过程。在这里,您可以将菜单写入数据库或文件,并保留时间戳以进行检查,如果有必要重新加载它。

当然这个想法可以合并

答案 2 :(得分:1)

经过多次摆弄后,我相信通过构建一组嵌套的词典和列表,我找到了一个使用极少LOC的工作解决方案。我想将真实对象发送到模板,而不仅仅是字符串(基本上尽量保持尽可能接近通用查询集方法)。生成的字典的形式是:

{
    Country1:{
        region1:[subregion1,subregion2],
        region2:[subregion3,subregion4]
        },
    Country2: {
        region3:[subregion5,subregion6],
        region4:[subregion7,subregion8]    
    },
}

其中每个国家,地区和子区域都是真实对象,而不是字符串。这是业务结束(这是在模板标签中)。请注意,我们会在每次迭代中检查可用的库存,只有在库存中有东西时才设置字典或列表项。

regionmenu = {}
for c in Country.objects.all() :
    if Wine.objects.filter(country=c,inventory__gt=0).count() > 0 :
        regionmenu[c] = {}

    for r in c.region_set.all(): 
        if Wine.objects.filter(country=c,region=r,inventory__gt=0).count() > 0 :
            regionmenu[c][r] = []           

        for s in r.subregion_set.all():
            if Wine.objects.filter(country=c,region=r,subregion=s,inventory__gt=0).count() > 0 :
                regionmenu[c][r].append(s)

字典完全符合需求,除非你失去了排序的能力,所以我必须在以后找出一些字母化的东西。

在模板中迭代dicts:

<ul>
{% for country, regions in regionmenu.items  %}
    <li>{{ country }} 
        <ul>
        {% for region, subregions in regions.items %}
        <li>{{ region }}
            <ul>
            {% for subregion in subregions %}
                <li>{{ subregion }}</li>
            {% endfor %}
            </ul>
        </li>
        {% endfor %}
        </ul>
    </li>
{% endfor %}
</ul>   

由于我们传入的是对象而不是字符串,我现在可以为每个级别的每个项目执行URL反转,获取slug等等(在此示例中删除)。