如何有效的遍历django的QuerySet

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何有效的遍历django的QuerySet相关的知识,希望对你有一定的参考价值。

参考技术A 开始的阶段没有遇到什么问题,我们举例,在models有一张员工表employee,对应的表结构中,postion列表示员工职位,前台post过来的参数赋给position,加上入职时间、离职时间,查询操作通过models.filter(position=params)完成,获取的员工信息内容由QuerySet和当前展示页与每页展示的记录数进行简单的计算,返回给前台页面进行渲染展示。编码如下:
def get_employees(position, start, end):
return employee.objects.filter(alert_time__lt=end,alert_time__gt=start).filter(position__in=position)
@login_required
def show(request):
if not validate(request):
return render_to_response('none.html',
context_instance=RequestContext(request, 'msg':'params error')
)
position = request.REQUEST.get('position')
time_range = request.REQUEST.get('time')
start, end = time_range[0], time_range[1]
num_per_page, page_num = get_num(request)
all_employees = get_employees(position, start, end)
  # 根据当前页与每页展示的记录数,取到正确的记录
employees = employees_events[(page_num-1)*num_per_page:page_num*num_per_page]
return render_to_response('show_employees.html',
context_instance=RequestContext(
request,
'employees': employees,
'num_per_page': num_per_page,
'page_num':page_num,
'page_options' : [50, 100, 200]
)
)
  运行之后可以正确的对所查询的员工信息进行展示,并且查询速度很快。employee表中存放着不同职位的员工信息,不同类型的详细内容也不相同,假设employees有一列名为infomation,存储的是员工的详细信息,infomation = 'age': 33, 'gender': 'male', 'nationality': 'German', 'degree': 'doctor', 'motto': 'just do it',现在的需求是要展示出分类更细的员工信息,前台页面除了post职位、入职离职时间外,还会对infomation中的内容进行筛选,这里以查询中国籍的设计师为例,在之前的代码基础上,需要做一些修改。员工信息表employee存放于mysql中,而MySQL为ORM数据库,它并未提供类似mongodb一样更为强大的聚合函数,所以这里不能通过objects提供的方法进行filter,一次性将所需的数据获取出来,那么需要对type进行过滤后的数据,进行二次遍历,通过information来确定当前记录是否需要返回展示,在展示过程中,需要根据num_per_page和page_num计算出需要展示数据起始以及终止位置。
def get_employees(position, start, end):
return employee.objects.filter(alert_time__lt=end,alert_time__gt=start).filter(position__in=position)
def filter_with_nation(all_employees, nationality, num_per_page, page_num):
result = []
pos = (page_num-1)*num_per_page
cnt = 0
start = False
for employee in all_employees:
info = json.loads(employee.information)
if info.nationality != nationality:
continue
# 获取的数据可能并不是首页,所以需要先跳过前n-1页
if cnt == pos:
if start:
break
cnt = 0
pos = num_per_page
start = True
if start:
result.append(employee)
return employee
@login_required
def show(request):
if not validate(request):
return render_to_response('none.html',
context_instance=RequestContext(request, 'msg':'params error')
)
position = request.REQUEST.get('position')
time_range = request.REQUEST.get('time')
start, end = time_range[0], time_range[1]
num_per_page, page_num = get_num(request)
all_employees = get_employees(position, start, end)
nationality = request.REQUEST.get('nationality')
employees = filter_with_nation(all_employees, num_per_page, page_num)
return render_to_response('show_employees.html',
context_instance=RequestContext(
request,
'employees': employees,
'num_per_page': num_per_page,
'page_num':page_num,
'page_options' : [50, 100, 200]
)
)
  当编码完成之后,在数据employee表数据很小的情况下测试并未发现问题,而当数据量非常大,并且查询的数据很少时,代码运行非常耗时。我们设想,这是一家规模很大的跨国公司,同时人员的流动量也很大,所以employee表的数据量很庞大,而这里一些来自于小国家的员工并不多,比如需要查询国籍为梵蒂冈的员工时,前台页面进入了无尽的等待状态。同时,监控进程的内存信息,发现进程的内存一直在增长。毫无疑问,问题出现在filter_with_nation这个函数中,这里逐条遍历了employee中的数据,并且对每条数据进行了解析,这并不是高效的做法。
  在网上查阅了相关资料,了解到:
Django的queryset是惰性的,使用filter语句进行查询,实际上并没有运行任何的要真正从数据库获得数据
只要你查询的时候才真正的操作数据库。会导致执行查询的操作有:对QuerySet进行遍历queryset,切片,序列化,对 QuerySet 应用 list()、len()方法,还有if语句
当第一次进入循环并且对QuerySet进行遍历时,Django从数据库中获取数据,在它返回任何可遍历的数据之前,会在内存中为每一条数据创建实例,而这有可能会导致内存溢出。
  上面的原来很好的解释了代码所造成的现象。那么如何进行优化是个问题,网上有说到当QuerySet非常巨大时,为避免将它们一次装入内存,可以使用迭代器iterator()来处理,但对上面的代码进行修改,遍历时使用employee.iterator(),而结果和之前一样,内存持续增长,前台页面等待,对此的解释是:using iterator() will save you some memory by not storing the result of the cache internally (though not necessarily on PostgreSQL!); but will still retrieve the whole objects from the database。
  这里我们知道不能一次性对QuerySet中所有的记录进行遍历,那么只能对QuerySet进行切片,每次取一个chunk_size的大小,遍历这部分数据,然后进行累加,当达到需要的数目时,返回满足的对象列表,这里修改下filter_with_nation函数:
def filter_with_nation(all_employees, nationality, num_per_page, page_num):
result = []
pos = (page_num-1)*num_per_page
cnt = 0
start_pos = 0
start = False
while True:
employees = all_employees[start_pos:start_pos+num_per_page]
start_pos += num_per_page
for employee in employees:
info = json.loads(employee.infomation)
if info.nationality != nationality:
continue
if cnt == pos:
if start:
break
cnt = 0
pos = num_per_page
start = True
if start:
result.append(opt)
cnt += 1
if cnt == num_per_page or not events:
break
return result
  运行上述代码时,查询的速度更快,内存也没有明显的增长,得到效果不错的优化。这篇文章初衷在于记录自己对django中queryset的理解和使用,而对于文中的例子,其实正常业务中,如果需要记录员工详细的信息,最好对employee表进行扩充,或者建立一个字表,存放详细信息,而不是将所有信息存放入一个字段中,避免在查询时的二次解析。

具有复杂外键遍历的棘手 Django QuerySet

【中文标题】具有复杂外键遍历的棘手 Django QuerySet【英文标题】:Tricky Django QuerySet with Complicated ForeignKey Traversals 【发布时间】:2015-01-16 22:09:00 【问题描述】:

我正在改编 Jim McGaw 的电子商务网站,以供我的客户使用。我的客户销售由一组定制部件组成的计算机版本。进入系统的部分将发生变化。例如,两年后售出的超级武士系统的零件将与我们明天售出的零件不同。因此,当我们销售每个 Super Samurai 系统时,我们需要了解销售时进入 Super Samurai 的部件的快照。

我遇到了 QuerySet 的问题,它从将部件映射到构建的表中复制了所有部件(即进入超级武士的部件)......

class Build(models.Model):
    build = models.ForeignKey(PartModel, related_name='+')
    part = models.ForeignKey(PartModel, related_name='+')
    quantity = models.PositiveSmallIntegerField(default=1)

    class Meta:
        abstract = True
        unique_together = ('build', 'part')

    def __unicode__(self):
        return self.build.name + ' with ' + str(self.quantity) + ' * ' + \
               self.part.family.make.name + ' ' + self.part.name

class BuildPart(Build):
    pass

    class Meta:
        verbose_name = "Build Part"

我需要将 BuildPart 表中的构建部分复制到 OrderBuildPart 表中...

class OrderBuildPart(Build):
    orderItem = models.ForeignKey(OrderItem, unique=False)

    class Meta:
        verbose_name = "Ordered Build Part"

...这样将来我们就可以知道哪些部分进入了某某的构建中。

McGaw 的电子商务网站不允许商品与其他商品捆绑在一起。因此,与其为构建及其部件创建两个不同表(和两个系列 SKU)的噩梦般的场景,我希望构建与任何其他部分一样......

class PartModel(models.Model):
    family = models.ForeignKey(PartFamily)
    name = models.CharField("Model Name", max_length=50, unique=True)
    slug = models.SlugField(help_text="http://www.Knowele.com/<b>*slug*</b>",
                            unique=True)
    *** = models.CharField("***", help_text="Vendor's Part Number",
                               max_length=30, blank=True, null=True)
    url = models.URLField("URL", blank=True, null=True)
    costurl = models.URLField("Cost URL", blank=True, null=True)
    cost = models.DecimalField(help_text="How much knowele.com pays", max_digits=9, decimal_places=2, blank=True, null=True)
    price = models.DecimalField(help_text="How much a customer pays", max_digits=9, decimal_places=2, blank=True, null=True)
    isActive = models.BooleanField(default=True)
    isBestseller = models.BooleanField(default=False)
    isFeatured = models.BooleanField(default=False)
    isBuild = models.BooleanField(default=False)
    description = models.TextField(blank=True, null=True)
    updated = models.DateTimeField(auto_now=True)
    created = models.DateTimeField(auto_now_add=True)
    buildpart = models.ManyToManyField('self', through='BuildPart',
                                symmetrical=False, related_name='+')

    class Meta:
        ordering = ['name']
        verbose_name = "Product Model"

    def __unicode__(self):
        return self.name

    def get_absolute_url(self):
        from django.core.urlresolvers import reverse
        return reverse('productdetail', args=[self.slug])

buildpart 字段引用 ManyToMany BuildPart 表,该表允许构建具有多个部分,并且允许一个部分与多个构建相关联。

通过调整 McGaw 的代码,我几乎得到了我需要的东西,直到我最终完成 PayPal 付款并尝试记录在销售的确切时刻哪些部件进入了已售出的版本......

def payment(request):
    token = request.POST['token']
    payer = request.POST['payer']

    result = paypal.do_express_checkout_payment(request, token, payer)

    if result['ACK'][0] in ['Success', 'SuccessWithWarning']:
        cart = Cart.objects.get(cart_id=get_cart_id(request))
        finalOrder = Order()
        finalOrder.cart_id = get_cart_id(request)
        finalOrder.token = token
        finalOrder.corID = result['CORRELATIONID'][0]
        finalOrder.payerID = payer
        finalOrder.ipAddress = request.META['REMOTE_ADDR']
        finalOrder.first = cart.first
        finalOrder.last = cart.last
        finalOrder.address = cart.address
        finalOrder.email = cart.email
        finalOrder.transactionID = result['PAYMENTINFO_0_TRANSACTIONID'][0]
        finalOrder.status = 'f'
        finalOrder.save()

        for item in get_cart_items(request):
            oi = OrderItem()
            oi.cart_id = item.cart_id
            oi.quantity = item.quantity
            oi.product = item.product
            oi.price = item.price()
            oi.save()

            if item.product.isBuild:
                for part in get_build_parts(request, item):
                    bp = OrderBuildPart()
                    bp.build = part.build
                    bp.part = part.part
                    bp.quantity = part.quantity
                    bp.orderItem = oi
                    bp.save()

        empty_cart(request)

        return render(request, 'payment.html', locals())

在我们点击 get_build_parts 函数之前,一切似乎都很好......

def get_build_parts(request, part):
    return BuildPart.objects.filter(build__id=part__product__pk)

...Django 的验尸报告“NameError at /payment/ global name 'part__product__pk' is not defined”

我如何遍历这些复杂的关系,以便我的老板可以查看每个客户的构建中包含哪些部分?

【问题讨论】:

【参考方案1】:

查找的价值方面并不像您想象的那样工作。双下划线仅适用于左侧:实际上,这是一种绕过 Python 语法要求的技巧。在右侧,您传递了一个普通表达式,它可以使用标准点语法遵循对象关系:

return BuildPart.objects.filter(build__id=part.product.pk)

【讨论】:

【参考方案2】:

试试BuildPart.objects.filter(build=part__product),而不是你所拥有的。

此外,您的“事后分析”似乎来自实际使用的 web 应用程序。您应该通过单元测试失败而不是 HTTP 调用来了解此类问题。

【讨论】:

以上是关于如何有效的遍历django的QuerySet的主要内容,如果未能解决你的问题,请参考以下文章

如何遍历 Django 模板中的字典?

如何在 django 中遍历数字

如何使用 Django 模型中的函数遍历 SQL 数据库?

Django:如何遍历模板内的两个列表[重复]

如何在 Django 模板中多次遍历同一个字典?

如何遍历 Django 字典中的外键?