【问题标题】:Django: Nested Queries from Non-Nested ModelsDjango:来自非嵌套模型的嵌套查询
【发布时间】:2009-09-03 14:32:54
【问题描述】:

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

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 上。然后,区域和国家/地区将始终由饮料的子区域知道。不幸的是,有太多现实世界的例外情况无法实现这一点 - 葡萄酒有一个区域但没有一个子区域,等等。所以我将布局按上述方式展平。

现在的问题是如何从这个模型布局中导出菜单。看起来深度嵌套的查询集列表将成为可行的方法,但这似乎计算成本高且代码复杂。有没有更清洁的方法?

【问题讨论】:

    标签: django django-models


    【解决方案1】:

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

    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 并开始一个新的。最后五行用于处理最后一个饮料,因为您总是在当前迭代期间保存前一个项目(并且最后一个项目没有下一个迭代)。这真的很粗糙,但这是我一直使用的过程,只需要一个查询。

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

    【讨论】:

    • Adam - 看起来它可以解决问题,但不幸的是,这正是我希望避免的那种复杂性。必须有一种更清洁、更像 Django 的方式。如果可能的话,我希望框架为我完成大部分工作。非常感谢您为此付出的所有努力!
    【解决方案2】:

    经过一番折腾,我相信我通过构建一组嵌套字典和列表找到了一个使用很少 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)
    

    字典完全符合需要,只是你失去了排序的能力,所以以后我得想办法按字母顺序排列。

    遍历模板中的字典:

    <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 等(在本示例中已删除)。

    【讨论】:

      【解决方案3】:

      两个想法

      • 要使您的第一种方法有效,您可以将 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')')))
      
      • 或者我的第二个想法:首先不要优化数据库查询——使用一些缓存。

        你可以先只查询国家,循环这个集合并查询这个国家的地区,然后在不同的循环中编写菜单。

        这会导致很多数据库命中,但代码会很简单。

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

        但请注意:信号和全局变量仅在进程范围内有效。但也许网络服务器跨越多个进程。在这里,您可以将菜单写入数据库或文件并保留时间戳以检查是否需要重新加载。

      当然可以结合这些想法

      【讨论】:

      • vikingsegundo - 是的,这就是我想到的方法。我现在正在考虑将方法放在模型上,这些模型在给定父实例的情况下派生所有填充的子代,然后使用模板标签或标签来点击这些方法。我可以在包含菜单的模板上使用缓存,然后将菜单模板的结果包含在主模板中。因此,这与您在此处提出的建议非常相似。当我得到它的工作时,我会发布结果。谢谢!
      猜你喜欢
      • 1970-01-01
      • 2021-02-02
      • 2019-01-05
      • 2019-05-05
      • 1970-01-01
      • 2013-11-12
      • 1970-01-01
      • 2011-06-19
      • 2017-11-29
      相关资源
      最近更新 更多