Django杂篇(1)


Django杂篇(1)

这里我们介绍以下Django常用的一些小工具,分别是:

  1. bulk_create,一种将数据批量插入数据库的方法,效率较高
  2. Pagination,自定义的一种分页器,重点在于其思路和使用
  3. 多对多表关系的创建方法,常见的有三种
  4. form校验组件的应用,其主要作用就是按照我们的要求校验数据的格式,并取到符合条件以及不符合条件的数据和报错

bulk_create

其实批量插入数据的原理非常简单,在日常来看,我们在向数据库插入数据的时候,通常都是一条一条的插入,如果有相似数据,我们通常会用一个循环,来持续插入,实际上这种方法如果在数据量比较大的情况下会非常耗时,所以我们才会引入这种插入方式,下面就用一个非常简单的例子来做一下对比,首先我们创建一个Django项目,命名第一个应用名为app01.

# 我们在models里面创建一个最简单的书籍表
# app01/models.py
class Book(models.Model):
    title=models.CharField(max_length=32)
# 然后在Terminal窗口,输入python manage.py makemigrations和python manage.py migrate之后就成功建立了表,然后在urls.py里面加路由

# urls.py
from django.conf.urls import url
from django.contrib import admin
from app01 import views
urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^index/', views.index),
]

# 然后在views.py里面写对应路由的函数,以下这种就是效率最低,也是最常用的插入方式,1000条数据甚至要用两分钟左右,而且对数据库的压力特别大,因为每次写入都要操作数据库
# views.py,常规插入方式
def index(request):
    # 往书籍表中插入数据 1000
    for i in range(1000):  # 这种插入方式 效率极低
         models.Book.objects.create(title='第%s本书'%i)
    book_queryset = models.Book.objects.all()
    return render(request,'index.html',locals())

# index.html
<body>
{% for book_obj in book_queryset %}
    <p>{{ book_obj.title }}</p>
{% endfor %}
</body>

换用bulk_create的话,views.py里面就要按以下这种方式写:

# views.py,bulk_create批量插入方式
def index(request):
    book_list = []
    for i in range(100000):
         book_list.append(models.Book(title='第%s本书'%i))
    models.Book.objects.bulk_create(book_list)  # 批量插入数据
    book_queryset = models.Book.objects.all()
    return render(request,'index.html',locals())

我们可以很明确的发现,用批量插入十万条数据仅仅用了不到二十秒,差距非常明显.其实原理非常简单

  • 无非就是平时是一条一条插入,用了bulk_create之后我们是把所有需要插入的数据先插入一个列表中,这个操作是在计算机内部完成的,耗时非常低,然后通过bulk_create把这个列表插入到数据库中,从而极大的提升了效率

Pagination

首先我们要知道分页器的概念,因为通常情况下一个网站其页面大小是有限的,不能展示其全部的信息,所以需要分页器来吧这些内容分成不同的页面,以便于我们浏览,那么我们首先用自己的思路来实现一个简单的分页器,我们继续上面的项目写,把插入数据的那几行注释掉就好

# views.py
# 自定义分页器
def index(request):
    # 1. 获取用户想要访问的页码数
    current_page = request.GET.get('page', 1)  # 获取用户输入,如果没有page参数,默认展示第一页
    current_page = int(current_page)
    # 2. 每页展示多少条数据,这里我们设置为每页10条
    per_page_num = 10
    # 3. 定义起始位置和终止位置
    start_page = (current_page - 1) * per_page_num
    end_page = current_page * per_page_num

    # 4. 统计数据的总条数
    book_queryset = models.Book.objects.all()
    all_count = book_queryset.count()

    # 5. 求数据到底需要多少个页面才能展示完
    page_num, more = divmod(all_count, per_page_num)
    if more:
        page_num += 1
    # page_num就决定了需要多少个页码
    page_html = ''
    xxx = current_page	# 这里取出来当前选中的页码赋给xxx,以便于后面判断当前页面
    if current_page < 6:	# 如果当前页面页码小于6,就不能再往前数五个页面,所以要为其重置为6
        current_page=6
    for i in range(current_page-5,current_page+6): # 这里我们希望分页器是以选中页面的页码为中心,然后左右各有五个页面的页码供选择
        if xxx == i:
            page_html+='<li class="active"><a href="?page=%s">%s</a></li>'   # 这里可以设置当前页面的页码为高亮的形式
        else:
            page_html+='<li><a href="?page=%s">%s</a></li>'
    book_queryset = models.Book.objects.all()[start_page:end_page]
    return render(request, 'index.html', locals())

# 然后,在index.html里面,我们如下写,这里分页的格式是来自于bootstrap,我们修改后即可使用,
<body>
<nav aria-label="Page navigation">
  <ul class="pagination">
    <li>
      <a href="#" aria-label="Previous">
        <span aria-hidden="true">&laquo;</span>
      </a>
    </li>
    {{ page_html|safe }}{#这里就是我们所写的最关键的一句,|safe的意思是取消转义,即后端传过来的符合前端标签要求的语句可以被表现出其应有的形式,而不只是字符串形式#}
    <li>
      <a href="#" aria-label="Next">
        <span aria-hidden="true">&raquo;</span>
      </a>
    </li>
  </ul>
</nav>
</body>

以上就是我们手动实现的一个简略的分页器,当然还有好多功能没有完善,不过我们了解分页器的思路即可,因为实际生产中我们并不需要手动去写分页器,这里有一个较完整的现成的分页器代码,我们将其记录下来,会调用即可

# 在app01下面新建一个utils文件夹,然后新建一个mypage.py文件,将以下代码复制粘贴即可
class Pagination(object):
    def __init__(self, current_page, all_count, per_page_num=10, pager_count=11):
        """
        封装分页相关数据
        :param current_page: 当前页
        :param all_count:    数据库中的数据总条数
        :param per_page_num: 每页显示的数据条数
        :param pager_count:  最多显示的页码个数

        用法:
        queryset = model.objects.all()
        page_obj = Pagination(current_page,all_count)
        page_data = queryset[page_obj.start:page_obj.end]
        获取数据用page_data而不再使用原始的queryset
        获取前端分页样式用page_obj.page_html
        """
        try:
            current_page = int(current_page)
        except Exception as e:
            current_page = 1

        if current_page < 1:
            current_page = 1

        self.current_page = current_page

        self.all_count = all_count
        self.per_page_num = per_page_num

        # 总页码
        all_pager, tmp = divmod(all_count, per_page_num)
        if tmp:
            all_pager += 1
        self.all_pager = all_pager

        self.pager_count = pager_count
        self.pager_count_half = int((pager_count - 1) / 2)

    @property
    def start(self):
        return (self.current_page - 1) * self.per_page_num

    @property
    def end(self):
        return self.current_page * self.per_page_num

    def page_html(self):
        # 如果总页码 < 11个:
        if self.all_pager <= self.pager_count:
            pager_start = 1
            pager_end = self.all_pager + 1
        # 总页码  > 11
        else:
            # 当前页如果<=页面上最多显示11/2个页码
            if self.current_page <= self.pager_count_half:
                pager_start = 1
                pager_end = self.pager_count + 1

            # 当前页大于5
            else:
                # 页码翻到最后
                if (self.current_page + self.pager_count_half) > self.all_pager:
                    pager_end = self.all_pager + 1
                    pager_start = self.all_pager - self.pager_count + 1
                else:
                    pager_start = self.current_page - self.pager_count_half
                    pager_end = self.current_page + self.pager_count_half + 1

        page_html_list = []
        # 添加前面的nav和ul标签
        page_html_list.append('''
                    <nav aria-label='Page navigation>'
                    <ul class='pagination'>
                ''')
        first_page = '<li><a href="?page=%s">首页</a></li>' % (1)
        page_html_list.append(first_page)

        if self.current_page <= 1:
            prev_page = '<li class="disabled"><a href="#">上一页</a></li>'
        else:
            prev_page = '<li><a href="?page=%s">上一页</a></li>' % (self.current_page - 1,)

        page_html_list.append(prev_page)

        for i in range(pager_start, pager_end):
            if i == self.current_page:
                temp = '<li class="active"><a href="?page=%s">%s</a></li>' % (i, i,)
            else:
                temp = '<li><a href="?page=%s">%s</a></li>' % (i, i,)
            page_html_list.append(temp)

        if self.current_page >= self.all_pager:
            next_page = '<li class="disabled"><a href="#">下一页</a></li>'
        else:
            next_page = '<li><a href="?page=%s">下一页</a></li>' % (self.current_page + 1,)
        page_html_list.append(next_page)

        last_page = '<li><a href="?page=%s">尾页</a></li>' % (self.all_pager,)
        page_html_list.append(last_page)
        # 尾部添加标签
        page_html_list.append('''
                                           </nav>
                                           </ul>
                                       ''')
        return ''.join(page_html_list)

下面我们介绍怎样去调用这个已经存在定义好的分页器

# urls.py,写对应路由
from django.conf.urls import url
from django.contrib import admin
from app01 import views
urlpatterns = [
    url(r'^login/', views.login),
]


# views.py
from django.shortcuts import render,HttpResponse,redirect
from app01 import models
from app01.utils.mypage import Pagination	# 这里导入刚才粘贴的已经定义好的分页器类
# 使用封装好的分页器代码
def login(request):
    book_queryset = models.Book.objects.all()	# 这里取到表内所有的数据,生成一个queryset对象
    current_page = request.GET.get('page',1)	# 从前端发来的get请求里面取出用户点击的page页面,如果没有默认就是1,也就是首页
    all_count = book_queryset.count()	#这里对queryset对象进行计数,得到一共多少条记录
    # 1.实例化产生对象
    page_obj = Pagination(current_page=current_page,all_count=all_count)# 我们只需要把记录的总数量和当前页面的page数传进函数里面即可
    # 2.对真实数据进行切片操作
    page_queryset = book_queryset[page_obj.start:page_obj.end]
    return render(request,'login.html',locals())

# login.html,前端的调用也非常简单
<body>
{% for book_obj in page_queryset %}	{#这里把后端传来的queryset对象直接循环,显示出来,即可以看到第*本书#}
    <p>{{ book_obj.title }}</p>
{% endfor %}
{{ page_obj.page_html|safe }}	{#这里是显示分页器的核心操作,加|safe是取消转义#}
</body>

创建多对多表关系的常用方法

  1. 第一种

    就是我们之前在建表的时候用到的,利用models.ManyToManyField方法来建立表和表之间的多对多关系,Django会自动生成第三张表.

    但是这种方法的弊端在于,第三张表只有主键和两张表的关系字段,且我们不能额外对其添加字段,所以其可扩展性较差,不利于项目后期的维护与开发.实例如下:

    class Book(models.Model):
        ...
        author=models.ManyToManyField(to='Author')
    class Author(models.Model):
        ...
    
  2. 第二种

    纯手动创建,即我们不依赖于ORM自动帮我们创建表关系,而是自己手动创建,这种方法所建立的表可扩展性好,可以添加很多我们想要额外增加的功能,比如表数据创建的时间等.但是弊端在于很多ORM的查询方法不能用,查询的效率会比较低,而且代码较复杂,开发效率也比较低,实例如下:

    class Book(models.Model):
        ...
                    
    class Author(models.Models):
        ...
                
    class Book2Author(models.Model):
    	book_id = models.ForeignKey(to='Book')
        author_id = models.ForeignKey(to='Author')
        create_time = models.DateField(auto_now_add=True)
        ...
    
  3. 第三种

    半自动的创建,即多对多的关系表还是我们自己手动创建,但是我们会告诉Django的ORM关系表的名字和位置,这样我们既保留了ORM查询的便利性,又保留了第三张关系表的可扩展性,可谓一举两得,但是唯一的缺点可能就是需要写的代码比较多,不过也无可厚非.

    半自动的创建表关系所需要额外加的两个参数是through='关系表的名字',through_fields=('表名(外键所在的表)','表名(外键不在的那个表)'),注意表名的顺序,不能反

    class Book(models.Model):
          ...
          authors = models.ManyToManyField(to='Author', through='Book_Author', through_fields=('book','author'))
    
    
    class Author(models.Model):
          ...
          books = models.ManyToManyField(to='Book', through='Book2Author', through_fields=('author', 'book'))
    
    class Book_Author(models.Model):
          book = models.ForeignKey(to='Book')
          author = models.ForeignKey(to='Author')
          ...
    

form校验组件的应用

我们在写一些大大小小的项目的时候,多多少少都会用到校验的功能,比如用户注册账号的时候,验证用户名是否存在,用户密码两次输入是否相同,都需要用到校验.不管是前端还是后端,都有必要加上数据校验的功能,一方面是为了数据库的安全,另外一方面也可以阻止一些非法数据的流入.不过相比之下,后端的校验比前端校验更有必要,因为前端页面是写给大众看的,很多东西都是公开的,可以直接修改,对数据的校验有很大的阻力,所以我们就更要重视在后端对数据的校验.

那么form组件就可以帮助我们来完成数据的校验工作,当然,他不止能完成数据的校验,还可以完成页面的渲染和错误信息的展示,可谓十分强大.

在对form进行说明之前,我们首先要在views.py里面建立一个类,以便于后面的调用和测试

# views.py
from django import forms
class MyRegForm(forms.Form):
      sername = forms.CharField(min_length=3,max_length=8)
      password = forms.CharField(min_length=3,max_length=8)
      email = forms.EmailField()
# 是不是觉得以上定义的类有些眼熟?确实,跟在models.py里面定义表结构十分相似

渲染页面

渲染页面的实际意义是我们在后端用form校验过数据之后把数据发送到前端,然后前端通过一些形式来展示出来,forms组件会自动帮我们渲染用户输入(或者是选择,下拉框等)的标签,但是提交按钮以及一些别的数据并不会自动帮我们渲染,还是需要我们手动去设置.forms常用的三种渲染页面的方式如下:

# formm.html
<body>
<p>
第一种渲染前端页面的方式:封装程度非常高,但是标签样式及参数不方便调整,可扩展性较差
	{{ form_obj.as_p }}
	{{ form_obj.as_ul }}
</p>

<p>第二种渲染页面的方式:扩展性较高,需要我们手写的代码量比较多</p>
<p>
    {{ form_obj.username.label }}{{ form_obj.username }}{#.lable可以看到其标签,.username则是显示出其真实的内容,这里是一个input输入框#}
</p>
<p>
    {{ form_obj.password.label }}{{ form_obj.password }}
</p>
<p>
    {{ form_obj.email.label }}{{ form_obj.email }}
</p>

<p>第三种渲染前端页面的方式:代码量和扩展性都很高(推荐使用)</p>
<form action="" method="post" novalidate> {#这里添加novalidate参数可以取消前端帮我们做校验,以便于我们只在后端做校验#}
    {% for foo in form_obj %}
    <p>
        {{ foo.label }}:{{ foo }}
        <span style="color: red">{{ foo.errors.0 }}</span>
    </p>
    {% endfor %}
    <input type="submit">
</form>
</body>

展示错误信息

在forms组件里面展示错误信息非常简单,只需要用对象点errors.0就可以了,如下:

#formm.html
<body>
<form action="" method="post" novalidate>
     {% for foo in form_obj %}
     <p>
         {{ foo.label }}:{{ foo }}
         <span style="color: red">{{ foo.errors.0 }}</span> {#这里就是真正展示出错误的地方,即对象foo.errors.0 #}
     </p>
     {% endfor %}
     <input type="submit">
</form>
</body>

当然以上的报错都是英文显示的,我们可以手动重写来实现报错用中文来显示,具体就是在之前我们在views.py里定义的类MyRegForm,在里面加参数error_message={}即可,内部我们可以以报错类型来重写报错内容,直接在冒号后面写即可,

from django import forms
class MyRegForm(forms.Form):
      sername = forms.CharField(min_length=3,max_length=8,label='用户名',
                               error_messages={
                                   'min_length':'用户名最短三位',	# 这里用户名小于三位的话就会报这个错,报错内容被我们重写之后就会这样报错,以下同理
                                   'max_length':'用户名最长八位',
                                   'required':'用户名不能为空'
                               },initial='我是初始值',required=False,# required赋值false的话该项不填也不会报错,即允许不填,实际运用就是可以用在非必填项
widget= widgets.TextInput(attrs={'class':'form-control others'}))# widget可以改变该框的type属性
      password = forms.CharField(min_length=3,max_length=8)
      email = forms.EmailField()

校验数据

手动校验(is_valid,cleaned_data,errors)

手动校验即我们手动调用几个函数来观察数据是否通过校验,比如is_valid,cleaned_data和errors,测试代码如下:

# 这里我们测试的时候在左下角的Python Console里面测试,可以实时看到结果,比较方便
from app01 import views

#1. 给自定义的类定义一个字典
obj = views.MyRegForm({'username':'jason','password':'123','email':'12'})

#2. is_vaild()数据全部符校验标准合才会返回True,但凡有不符合标准的都会返回False
obj.is_valid()  
Out[5]: False

#3. cleaned_data可以查看查看所有符合条件的数据
obj.cleaned_data
Out[9]: {'username': 'jason', 'password': '123'}

#4. errors可以查看不符合条件的数据以及报错的原因
obj.errors
Out[10]: {'email': ['Enter a valid email address.']}

#5. 校验数据的时候,默认情况下类里面所有的字段都必须传值
obj = views.MyRegForm({'username':'jason','password':'123'})
obj.is_valid()
Out[12]: False
obj.errors
Out[13]: {'email': ['This field is required.']}

#6. 默认情况下可以多传,多传后面的数据会舍弃,但是绝对不能少传,少传就会不符合校验标准,出现False
obj=views.MyRegForm({'username':'jason','password':'1233','email':'123@qq.com','xxx':'ooo'})
obj.is_valid()
Out[15]: True

钩子函数的校验

对于数据的字段来说,定义的时候限制其格式是一方面,另外一方面我们可以用钩子函数来对其做额外的校验,这种方式在对于大项目的后期修改,维护和二次开发上有较大的应用.

局部钩子

当我们需要对数据库的表中的某一个字段的数据进行额外的校验的时候,局部钩子就是非常好的选择,示例如下:

class MyRegForm(forms.Form):
	def clean_username(self):# 局部钩子要写在MyRegForm总类的下面
        username = self.cleaned_data.get('username')
        if '黄' in username:# 检测所有的用户姓名,带'黄'字的不符合标准,加入报错信息
            self.add_error('username','这本书的名字不符合标准')
        return username	# 最后要返回我们校验的字段的数据,不然会看不到最后结果
全局钩子

上面局部钩子是针对单个字段进行二次校验,所以全局钩子就是可以根据多个字段进行校验,比如,用户注册的时候我们用来校验其输入的两次密码是否一致,示例如下:

class MyRegForm(forms.Form):
     def clean(self):
        password = self.cleaned_data.get('password')
        confirm_password = self.cleaned_data.get('confirm_password')
        if not password == confirm_password:
            self.add_error('confirm_password','两次密码不一致')
        return self.cleaned_data

正则校验

我们还有第三种校验数据的方式,即正则校验.

其实正则校验也是在我们定义类的时候就定义好的,与前面手动校验的前提比较相似

# views.py 这里是对于159开头的手机号的校验,RegexValidator即为正则校验的关键字
from django import forms
from django.forms import Form
from django.core.validators import RegexValidator

class MyForm(Form):
    user = forms.CharField(
        validators=[RegexValidator(r'^[0-9]+$', '请输入数字'), RegexValidator(r'^159[0-9]+$', '数字必须以159开头')],
    )

常用字段

以下为几个常用字段,需要使用时直接复制即可.

# 单选框select
gender = forms.ChoiceField(
    choices=((1, "男"), (2, "女"), (3, "保密")),
    label="性别",
    initial=3,
    widget=widgets.RadioSelect()
)


# 多选框select
hobby1 = forms.MultipleChoiceField(
    choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
    label="爱好",
    initial=[1, 3],
    widget=widgets.SelectMultiple()
)

# 单选heckbox
keep = forms.ChoiceField(
    label="是否记住密码",
    initial="checked",
    widget=forms.widgets.CheckboxInput()
)

# 多选checkbox
hobby2 = forms.MultipleChoiceField(
    choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
    label="爱好",
    initial=[1, 3],
    widget=forms.widgets.CheckboxSelectMultiple()
)


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM