一、queryset

  Queryset是django中构建的一种数据结构,ORM查询集往往是queryset数据类型,我们来进一步了解一下queryset的特点。

1、可切片

  使用Python 的切片语法来限制查询集记录的数目。它等同于SQL 的LIMIT 和OFFSET 子句。

  >>> Entry.objects.all()[:5]      # (LIMIT 5)
  >>> Entry.objects.all()[5:10]    # (OFFSET 5 LIMIT 5)

  不支持负的索引(例如Entry.objects.all()[-1])。通常,查询集的切片返回一个新的查询集(它不会再执行sql查询语句)。

2、可迭代

  articleList = models.Article.objects.all()
  for article in articleList:
      print(article.title)

3、惰性查询

  查询集是惰性执行的 —— 创建查询集不会带来任何数据库的访问。你可以将过滤器保持一整天,直到查询集需要求值时,Django 才会真正运行这个查询。

  queryResult = models.Article.objects.all()     # not hits database
  print(queryResult)      # hits database
  for article in queryResult:
      print(article.title)       # hits database

  一般来说,只有在“请求”查询集的结果时才会到数据库中去获取它们。当你确实需要结果时,查询集通过访问数据库来求值。

4、缓存机制

  每个查询集都包含一个缓存来最小化对数据库的访问。理解它是如何工作的将让你编写最高效的代码。

  在一个新创建的查询集中,缓存为空。首次对查询集进行求值 —— 同时发生数据库查询 ——Django 将保存查询的结果到查询集的缓存中并返回明确请求的结果(例如,如果正在迭代查询集,则返回下一个结果)。接下来对该查询集的求值将重用缓存的结果。

  请牢记这个缓存行为,因为对查询集使用不当的话,它会坑你的。例如,下面的语句创建两个查询集,对它们求值,然后扔掉它们:

  print([a.title for a in models.Article.objects.all()])
  print([a.create_time for a in models.Article.objects.all()])

  这意味着相同的数据库查询将执行两次,显然倍增了你的数据库负载。同时,还有可能两个结果列表并不包含相同的数据库记录,因为在两次请求期间有可能有Article被添加进来或删除掉。为了避免这个问题,只需保存查询集并重新使用它,如下:

  queryResult = models.Article.objects.all()
  print([a.title for a in queryResult])
  print([a.create_time for a in queryResult]

  何时查询集不会被缓存?

  查询集不会永远缓存它们的结果。当只对查询集的部分进行求值时会检查缓存,如果这个部分不在缓存中,那么接下来查询返回的记录都将不会被缓存。所以,这意味着使用切片或索引来限制查询集将不会填充缓存。例如,重复获取查询集对象中一个特定的索引将每次都查询数据库,如下:

  >>> queryset = Entry.objects.all()
  >>> print queryset[5]     # Queries the database
  >>> print queryset[5]     # Queries the database again

  然而,如果已经对全部查询集求值过,则将检查缓存,如下:

  >>> queryset = Entry.objects.all()
  >>> [entry for entry in queryset]    # Queries the database
  >>> print queryset[5]    # Uses cache
  >>> print queryset[5]    # Uses cache

  下面是一些其它例子,它们会使得全部的查询集被求值并填充到缓存中:

  >>> [entry for entry in queryset]
  >>> bool(queryset)
  >>> entry in queryset
  >>> list(queryset)

  注意:简单地打印查询集不会填充缓存,如下:

  queryResult = models.Article.objects.all()
  print(queryResult)    # hits database
  print(queryResult)    # hits database

5、exists()iterator()方法

(1)exists()

  简单的使用if语句进行判断也会完全执行整个queryset并且把数据放入cache,虽然你并不需要这些数据!为了避免这个,可以用exists()方法来检查是否有数据,如下:

  if queryResult.exists():
  # SELECT (1) AS "a" FROM "blog_article" LIMIT 1; args=()
    print("exists...")

(2)iterator()

  当queryset非常巨大时,cache会成为问题。

  处理成千上万的记录时,将它们一次装入内存是很浪费的。更糟糕的是,巨大的queryset可能会锁住系统进程,让你的程序濒临崩溃。要避免在遍历数据的同时产生queryset cache,可以使用iterator()方法来获取数据,处理完数据就将其丢弃。如下:

  objs = Book.objects.all().iterator()
  # iterator()可以一次只从数据库获取少量数据,这样可以节省内存
  for obj in objs:
      print(obj.title)
  # BUT,再次遍历没有打印,因为迭代器已经在上一次遍历(next)到最后一次了,没得遍历了
  for obj in objs:
      print(obj.title)

  当然,使用iterator()方法来防止生成cache,意味着遍历同一个queryset时会重复执行查询。所以使用iterator()的时候要当心,确保你的代码在操作一个大的queryset时没有重复执行查询。

总结:queryset的cache是用于减少程序对数据库的查询,在通常的使用下会保证只有在需要的时候才会查询数据库。使用exists()和iterator()方法可以优化程序对内存的使用。不过,由于它们并不会生成queryset cache,可能会造成额外的数据库查询。因此,要根据场景灵活使用。 

二、二级菜单的默认显示

1、修改my_tags.py文件,代码如下:

@register.inclusion_tag("menu.html")
def get_menu_styles(request):
    permission_menu_dict = request.session.get("permission_menu_dict")
    print("permission_menu_dict", permission_menu_dict)

    for val in permission_menu_dict.values():
        for item in val["children"]:
            val["class"] = "hide"
            ret = re.search("^{}$".format(item["url"]), request.path)
            if ret:
                val["class"] = ""

    return {"permission_menu_dict": permission_menu_dict}

2、修改menu.html文件,代码如下:

<div class="multi-menu">
    {% for item in permission_menu_dict.values %}
        <div class="item">
            <div class="title">
                <i class="{{ item.menu_icon }}"></i>{{ item.menu_title }}
            </div>
            <div class="body {{ item.class }}">
                {% for foo in item.children %}
                    <a href="{{ foo.url }}">{{ foo.title }}</a>
                {% endfor %}
            </div>
        </div>
    {% endfor %}
</div>

动态显示按钮权限

  除了菜单权限,还有按钮权限,比如添加用户(账单),编辑用户(账单),删除用户(账单),这些不是菜单选项,而是以按钮的形式在页面中显示,但不是所有的用户都有所有的按钮权限,我们需要在用户不拥有这个按钮权限时就不要显示这个按钮,下面介绍一下思路和关键代码。

1、在permission表中增加一个字段name,permission模型类如下:

class Permission(models.Model):
    """
    权限表
    """
   url = models.CharField(verbose_name='含正则的URL', max_length=32)
    title = models.CharField(verbose_name='标题', max_length=32)
    menu = models.ForeignKey(verbose_name='标题', to="Menu", on_delete=models.CASCADE, null=True)
    name = models.CharField(verbose_name='url别名', max_length=32, default="")

   def __str__(self):
            return self.title

2、将权限别名列表注入session,setsession.py中代码如下:

def initial_session(user_obj, request):
    """
    将当前登录人的所有权限url列表和
    自己构建的所有菜单权限字典和
    权限表name字段列表注入session
    :param user_obj: 当前登录用户对象
    :param request: 请求对象HttpRequest
    """
    # 查询当前登录人的所有权限列表
    ret = Role.objects.filter(user=user_obj).values('permissions__url',
                                                    'permissions__title',
                                                    'permissions__name',
                              'permissions__menu__title',
                                      'permissions__menu__icon',
                                  'permissions__menu__id').distinct()
    permission_list = []
    permission_names = []
    permission_menu_dict = {}
    for item in ret:
        # 获取用户权限列表用于中间件中权限校验
        permission_list.append(item['permissions__url'])
        # 获取权限表name字段用于动态显示权限按钮
        permission_names.append(item['permissions__name'])
        
        menu_pk = item['permissions__menu__id']
        if menu_pk:
            if menu_pk not in permission_menu_dict:
                permission_menu_dict[menu_pk] = {
                    "menu_title": item["permissions__menu__title"],
                    "menu_icon": item["permissions__menu__icon"],
                    "children": [
                        {
                            "title": item["permissions__title"],
                            "url": item["permissions__url"],
                        }
                    ],
                }
            else:
                permission_menu_dict[menu_pk]["children"].append({
                    "title": item["permissions__title"],
                    "url": item["permissions__url"],
                })
    print('权限列表', permission_list)
    print('菜单权限', permission_menu_dict)
    # 将当前登录人的权限列表注入session中
    request.session['permission_list'] = permission_list
    # 将权限表name字段列表注入session中
    request.session['permission_names'] = permission_names
    # 将当前登录人的菜单权限字典注入session中
    request.session['permission_menu_dict'] = permission_menu_dict

3、自定义过滤器,my_tags.py中部分代码如下:

@register.filter
def has_permission(btn_url, request):
    permission_names = request.session.get("permission_names")
    return btn_url in permission_names

4、使用过滤器,模板(如customer_list.html)中部分权限按钮代码如下:

<div class="btn-group">
    {% load my_tags %}
    {% if "customer_add"|has_permission:request %}
         <a class="btn btn-default" href="/customer/add/">
            <i class="fa fa-plus-square" aria-hidden="true"></i> 添加客户
         </a>
    {% endif %}
</div>

注意:permission表中新增name字段与url的别名没有关系,当然也可以起一样的名字,心里明白他们其实并无关系即可。