接下来,我们把Django分为视图(View)、路由系统(URL)、ORM(Model)、模板(Templates )这4块进行学习。

视图

提交数据

上节课已经用过 request.POST.get() 获取提交的数据了,现在来看看有多选框的情况,多选的话应该要提交多个数据。先写一个有单选、多选、下拉列表的html:

<body>
<form action="/choice/" method="post">
    <p>
        性别:
        <input type="radio" name="gender" id="man" value="male" />
            <label for="man">男性</label>
        <input type="radio" name="gender" id="female" value="female" />
            <label for="female">女性</label>
    </p>
    <p>
        爱好:
        <input type="checkbox" id="football" name="favor" value="football" />
            <label for="football">足球</label>
        <input type="checkbox" id="basketball" name="favor" value="basketball" />
            <label for="basketball">篮球</label>
        <input type="checkbox" id="volleyball" name="favor" value="volleyball" />
            <label for="volleyball">排球</label>
        <input type="checkbox" id="baseball" name="favor" value="baseball" />
            <label for="baseball">棒球</label>
    </p>
    <p>
        <label for="city">城市:</label>
        <select name="city" id="city">
            <option value="BJ">北京</option>
            <option value="SH">上海</option>
            <option value="GJ">广州</option>
            <option value="SZ">深圳</option>
        </select>
    </p>
    <p>
        <label for="skill">技能:</label>
        <select name="skill" id="skill" multiple="multiple">
            <option value="python">Python</option>
            <option value="html">HTML</option>
            <option value="css">CSS</option>
            <option value="js">JavaScript</option>
        </select>
    </p>
    <p>
        上传:<input type="file" name="file" />
    </p>
    <input type="submit" value="提交" />
</form>
</body>

然后写一个处理函数,用get方法获取一下提交的值:

def choice(request):
    if request.method == 'GET':
        return render(request, 'choice.html')
    elif request.method == 'POST':
        gender = request.POST.get('gender')
        favor = request.POST.get('favor')
        city = request.POST.get('city')
        skill = request.POST.get('skill')
        file = request.POST.get('file')
        print(gender, favor, city, skill)
        print(file, type(file))
        return render(request, 'choice.html')
    # 处理POST和GET,还有其他的提交方法,比如:PUT、DELETE、HEAD、OPTION
    else:
        return redirect('/admin/')

所有的值都能获取到,但是对于多选的值也只能获取到一个,对于这种情况,我们需要用到另一个方法 request.POST.getlist() ,把上面的代码都替换成新的方法:

def choice(request):
    if request.method == 'GET':
        return render(request, 'choice.html')
    elif request.method == 'POST':
        gender = request.POST.getlist('gender')
        favor = request.POST.getlist('favor')
        city = request.POST.getlist('city')
        skill = request.POST.getlist('skill')
        file = request.POST.getlist('file')
        print(gender, favor, city, skill)
        print(file, type(file))
        return render(request, 'choice.html')
    # 除了POST和GET,客户端还可能有其他的提交方法,比如:PUT、DELETE、HEAD、OPTION
    else:
        return redirect('/admin/')

使用 getlist() 方法,返回的是一个列表,多个值的情况也能获取完整。当然,单选的话还是继续使用 get() 方法方便。
例子中还有个上传文件的input,这里只能获取到文件名,下面接着讲。

上传文件

普通的form接收不了文件,需要在form标签中要定义 enctype="multipart/form-data" 。然后把 type="file" 的input标签放在这个form里。所以得为上传文件单独写一个form。另外POST里并没有文件内容,文件内容再FILES里:

<form action="/upload/" method="post" enctype="multipart/form-data">
    <p>
        上传:<input type="file" name="file" />
    </p>
    <input type="submit" value="提交" />
</form>

然后再处理函数里先尝试获取一下接收到的内容:

def upload(request):
    if request.method == 'GET':
        return render(request, 'upload.html')
    elif request.method == 'POST':
        obj = request.FILES.get('file')
        print(obj, type(obj), obj.name)
        return render(request, 'upload.html')
    # 处理POST和GET,还有其他的提交方法,比如:PUT、DELETE、HEAD、OPTION
    else:
        return redirect('/admin/')

上面打印了3个变量,obj打印出来是个文件名,但是实际是个class。打印type(obj)可以看到它的类型。obj.name才是真正的文件名。现在要把文件保存到本地,先建一个专门的文件夹upload,准备存放接收到的文件。要接收文件的内容,我们需要读取 obj.chunks() ,所以要接收文件上传参考下面的方法:

def upload(request):
    if request.method == 'GET':
        return render(request, 'upload.html')
    elif request.method == 'POST':
        # 获取文件对象
        obj = request.FILES.get('file')
        # 本地创建一个文件用来接收上传的文件内容
        with open('%s/%s' % ('upload', obj.name), 'wb') as file:
            # 循环接收文件的内容,写入到本地的文件中去
            for data in obj.chunks():
                file.write(data)
        return render(request, 'upload.html')
    # 处理POST和GET,还有其他的提交方法,比如:PUT、DELETE、HEAD、OPTION
    else:
        return redirect('/admin/')

CBV 和 FBV

到目前为止,所有的处理都是写在一个函数里的。Django还提供另外一个方式,我们也可以通过类来处理。

  • FBV(function base views) 就是在视图里使用函数处理请求。
  • CBV(class base views) 就是在视图里使用类处理请求。

创建处理请求的类
现在使用CBV把上面提交数据里的choice方法重新写一下:

from django.views import View
# CBV 的类需要继承上面的View
class Choice(View):

    def get(self, request):
        """GET请求提交到这里"""
        print('get')
        return render(request, 'choice.html')

    def post(self, request):
        """POST请求提交到这里"""
        gender = request.POST.getlist('gender')
        favor = request.POST.getlist('favor')
        city = request.POST.getlist('city')
        skill = request.POST.getlist('skill')
        file = request.POST.getlist('file')
        print(gender, favor, city, skill)
        print(file, type(file))
        return render(request, 'choice.html')

创建对应关系
urls.py 里的对应关系也要修改一下,中间写上类名,后面固定跟一个 .as_view() ,就是执行这个类的as_view()方法,具体如下:

from cmdb import views
urlpatterns = [
    # path('choice/', views.choice),
    path('choice/', views.Choice.as_view()),
]

各种类型的提交方法
先去看看继承的View类里有什么,在源码的base.py这个文件里。首先里面定义了一个公有属性:

http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

所以处理post和get,还可以处理这么多的请求方法,用一起来也很简单,在类里照着别的一样定义一个同名方法就可以了。
处理执行前后自定义操作
继续看源码的View。这里可以跳过只看结论,调用了as_view()方法里面会再调用一个dispatch()方法。这个dispatch()方法里是通过映射获取我们的 request.method 即提交的方法来调用我们的处理方法的。dispatch()的源码如下:

    def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed
        return handler(request, *args, **kwargs)

结论就是,根据不同的请求类型提交到不同的处理方法,是用过dispatch()方法里通过映射来实现的。先执行dispatch()方法然后再调用对应的提交类型的处理方法。所以通过继承和重构dispatch()方法,可以在处理方法执行前和执行后自定义一些操作。如果需要的话就在我们的类里继承并重构,参考这里:

from django.views import View
# CBV 的类需要继承上面的View
class Choice(View):

    def dispatch(self, request, *args, **kwargs):
        print('before')  # 处理前执行的操作
        # 完全执行父类的这个方法
        obj = super(Choice, self).dispatch(request, *args, **kwargs)
        print('after')  # 处理后执行的操作
        # 这里一定要把处理结果返回
        return obj

所有提交类型都不匹配的情况
还是上面的dispatch()方法,在最后return之前,也就是提交的类型没有匹配的处理方法的时候,默认调用执行 http_method_not_allowed() 方法,返回一个405页面。如果需要按照之前FBV中的else那样匹配其余所有类型的提交方法的话,那就在我们的类里重构这个方法,把之前FBV中if里面最后的else的逻辑补上:

    def http_method_not_allowed(self, request, *args, **kwargs):
        return redirect('/admin/')

给views.py分类

默认所有的处理函数都是写在views.py这个文件里的。如果处理函数很多,全部写在一个文件里也会很乱。这是可以考虑创建一个views包来替代原来的views.py文件。然后在views包里创建多个py文件来写我们的处理函数。比如:

  • views/account.py 是用户相关的操作,登录认证之类的
  • views/test.py 是用来测试的处理函数
  • views/order.py 订单相关的操作

路由系统,URL

模板言语循环字典

模板语言不属于路由系统,由于后面的例子会用到,先讲一点。
先看一下模板语言如何处理字典的,在 views.py 里添加一个字典,然后在页面里返回:

DICT = {
    'k1': 'value1',
    'k2': 'value2',
    'k3': 'value3',
}

def dict(request):
    return render(request, 'dict.html', {'dict': DICT})

下面的html里演示了用法:

<body>
<p>返回整个字典</p>
{{ dict }}
<p>返回的是key</p>
<ul>
    {% for i in dict %}
        <li>{{ i }}</li>
    {% endfor %}
</ul>
<p>返回的是key</p>
<ul>
    {% for key in dict.keys %}
        <li>{{ key }}</li>
    {% endfor %}
</ul>
<p>返回的是value</p>
<ul>
    {% for value in dict.values %}
        <li>{{ value }}</li>
    {% endfor %}
</ul>
<p>返回key和value</p>
<ul>
    {% for k,v in dict.items %}
        <li>{{ k }}: {{ v }}</li>
    {% endfor %}
</ul>
</body>

循环字典,和python里是差不多的,就是后面没有括号():

  • 直接dict :循环的是key,不明确所以不推荐
  • dict.keys :循环key
  • dict.values :循环values
  • dict.items :循环key和values

一条对应关系对应多个页面

现在我们已经可以用模板语言处理字典了,先来一个有点数据的字典:

USER_DICT = {
    '1': {'name': 'Adam', 'age': 22, 'dept': 'IT'},
    '2': {'name': 'Bob', 'age': 32, 'dept': 'IT'},
    '3': {'name': 'Carmen', 'age': 30, 'dept': 'Sales'},
    '4': {'name': 'David', 'age': 40, 'dept': 'HR'},
    '5': {'name': 'Edda', 'age': 26, 'dept': 'HR'},
}

def users(request):
    return render(request, 'users.html', {'user_dict': USER_DICT})

上面的处理函数只是把内存的数据变的复杂了一点。另外这里的key用的是数字,我们可以把它当做是数据库获取到的数据的自增id。

基于get方法的实现

接下来重新写一个简单的html,页面里只显示字典的name的值,其他的值都不显示出来。换做提供一个a标签,可以通过点击a标签打开一个显示详细内容的页面:

<ul>
    {% for k,v in user_dict.items %}
        <li><a target='_blank' href='/detail/?nid={{ k }}'>{{ v.name }}</a></li>
    {% endfor %}
</ul>

去urls.py里添加完对应关系后,就可以打开这个页面。上面a标签里的连接指向的是一个detail的页面,并且提交的同时也提交一个nid值用于detail页面查找并显示出详细的内容。
显示详细的处理函数:

def detail(request):
    # 用get方法获取到nid
    nid = request.GET.get('nid')
    # 通过nid获取到详细数据,最后给return返回
    detail_info = USER_DICT[nid]
    return render(request, 'detail.html', {'detail_info': detail_info})

还要写一个detail.html 的页面。上面处理函数已经通过get请求的nid去获取到具体得详细数据并返回了,这里直接把数据显示出来:

<body>
<h1>详细信息</h1>
<h3>用户名:{{ detail_info.name }}</h3>
<h3>年龄:{{ detail_info.age }}</h3>
<h3>部门:{{ detail_info.dept }}</h3>
</body>

上面的方法是在users页面以get形式提交到detail页面,然后detail页面里分析get的请求内容,获取到对应的详细信息,在页面里显示出来。

基于正则表达式的url来实现

还有另外一种实现方式。下面说的效果一样,但是这种方式更好。不传入参数,而是不同的urel '/detail-1/' 这样,这个就需要用到正则表达式。先把urls.py里的对应关系改成正则的形式:

from django.urls import path, re_path
from cmdb import views
urlpatterns = [
    # path('detail/', views.detail),
    re_path('^detail-(\d+).html', views.detail),
]

因为这里要匹配正则了,之前的path不再适用,这里要导入re_path来匹配正则。url的正则表达式都以^开头,从头开始匹配
users.html显示不用改,但是要修改一个a标签里的内容,现在url后面不需要用get方式提交任何数据,但是请求的url本事是会变化的:

<ul>
    {% for k,v in user_dict.items %}
        <!--
        <li><a target='_blank' href='/detail/?nid={{ k }}'>{{ v.name }}</a></li>
        -->
        <li><a target='_blank' href='/detail-{{ k }}.html'>{{ v.name }}</a></li>
    {% endfor %}
</ul>

最后是显示详细信息的页面,detail.html不需要任何变动,只要views.py里处理函数的return不变就好,但是获取数据的方式变了:

def detail(request, nid):
    # print(nid)
    detail_info = USER_DICT[nid]
    return render(request, 'detail.html', {'detail_info': detail_info})

这里的处理函数多传入了一下参数nid。名字不重要,但是这个值是正确分组匹配的结果。正则是这个 'detail-(\d+).html' ,里面括号中的 \d+ 的内容就传给了后面的第一个参数。也可以传多个参数(用多个括号),但是数量要一致(处理函数开头的形式参数),否则打开的页面会报错。
为什么这种更好:路由关系是一个动态的关系,一对多,一类url对应一个函数或类。

捕获参数

捕获组就是把正则表达式中子表达式匹配的内容,保存到内存中以数字编号或显式命名的组里,方便后面引用。当然,这种引用既可以是在正则表达式内部,也可以是在正则表达式外部。捕获组有两种形式:

  • 普通捕获组:(Expression)
  • 命名捕获组:(?P&lt;name&gt;Expression)这个是python中的语法,其他语言了有的有,但是可能有点小差别,比如没有这个P,比如不用尖括号换成引号
    前面的就是普通捕获组的例子。如果你的正则有多个子表达式,比如:‘detail-(\d+)-(\d+).html’ 。那么定义函数的时候必须注意参数的位置(名字为所谓)。这里可以使用命名捕获组来写正则表达式,正则本身没有任何变化,只是在子表达式前面加上加上一个命名。views.py里的对应关系可以这么写
    from django.urls import path, re_path
    from cmdb import views
    urlpatterns = [
    # re_path('^detail2-(\d+)-(\d+).html', views.detail2),
    re_path('^detail2-(?P<nid>\d?)-(?P<uid>\d?).html', views.detail2),
    ]

    上面被注释的是普通捕获组的写法,下面的是命名捕获组的写法。使用了命名捕获组后,我们的处理函数的参数名字就是正则中的命名,但是位置无所谓了。下面的处理函数直接在页面输出2个参数的值,就不写页面了:

    def detail2(request, uid, nid):
    return HttpResponse('%s-%s' % (nid, uid))

    可以用这样的url测试 http://127.0.0.1:8000/detail2-1-3.html

    用 path() 方法实现捕获参数

    课上讲的是旧版本,现在Django已经2.0了,url()方法被path()方法替代,用法也有区别。
    re_path() 可以看做是2.0里向下兼容的一个方法,也就是旧的1.0的 url() 方法。在2.0里用 path() 方法也可以实现捕获组的对应关系。使用 path() 方法需要注意:

    1. 要捕获一段url中的值,需要使用尖括号,而不是之前的圆括号;
    2. 可以转换捕获到的值为指定类型,比如int。默认情况下,捕获到的结果保存为字符串类型,不包含 '/' 这个特殊字符;
    3. 匹配模式的最开头不需要添加 '/' ,因为默认情况下,每个url都带一个最前面的 '/' ,既然大家都有的部分,就不用浪费时间特别写一个了。

那么现在 urls.py 里的对应关系可以这么写:

    # re_path('detail2-(\d+)-(\d+).html', views.detail2),
    # re_path('^detail2-(?P<nid>\d+)-(?P<uid>\d+).html', views.detail2),
    path('detail2-<int:nid>-<int:uid>.html', views.detail2),

上面的例子,就是捕获一个0或正整数,并且返回一个int类型,再用冒号把命名也完成了。除了int,还有下面这些。
默认情况下,Django内置下面的路径转换器:

  • str:匹配任何非空字符串,但不含斜杠/,如果你没有专门指定转换器,那么这个是默认使用的;
  • int:匹配0和正整数,返回一个int类型;
  • slug:可理解为注释、后缀、附属等概念,是url拖在最后的一部分解释性字符。该转换器匹配任何ASCII字符以及连接符和下划线,比如’ building-your-1st-django-site‘ ;
  • uuid:匹配一个uuid格式的对象。为了防止冲突,规定必须使用破折号,所有字母必须小写,例如’075194d3-6885-417e-a8a8-6c931e272f00‘ 。返回一个UUID对象;
  • path:匹配任何非空字符串,重点是可以包含路径分隔符’/‘。这个转换器可以帮助你匹配整个url而不是一段一段的url字符串。

小结

上面各种实现的方法由浅入深,并且一个比一个好,推荐用最后面的实现方式:

  • 基于正则的url比使用get方式获取参数的好
  • 命名捕获组比普通捕获组好
  • 推荐还是用最后的 path() 方法来实现,如果是1.x的版本,那么就是推荐基于正则的命名捕获组的方法。

另外,在定义函数的时候也可以写成这种万能的模式: def detail2(request, *args, **kwargs): ,这样的话,要使用 args[0] (普通捕获组)或 kwargs['nid'] (命名捕获组)来取值。

路由对应的名称

还可以对url的关系进行命名,完成命名后,以后可以通过这个名字获取到对应的url。好处是html里使用url的名称而不是写死,那么urls.py里修改了url,不用到html里修改了。
命名是在写对应关系的时候,加上一个参数 name=[url的名称]

    path('myurl/', views.myurl, name='myurl'),

然后再网页中使用的时候用 {% url [url的名称] %} 替代原来写死的url。

<form action="{% url 'myurl' %}" method="get">
    <input type="text" name="name" placeholder="NAME" />
    <input type="submit" value="提交" />
</form>

打开页面后可以按F12进入开发这模式查看页面中的url已经是转化后的具体的url了,就是我们在 urls.py 的对应关系里写的内容。
带捕获参数的情况:
url如果带有捕获参数,比如要捕获2个参数:

    path('jump-<int:nid>-<int:uid>/', views.jump, name='jump'),

那么首先处理函数你必须写上这2个参数(不写会报错),也可以用通用的 *args,**kwargs 的形式:

def jump(request, nid, uid):
    # return HttpResponse('%s-%s' % (nid, uid))
    return render(request, 'jump.html')

然后页面上做使用模板语言的时候,引用jump作为动态的url,但是后面也要跟上需要捕获的值:

<a href="{% url 'jump' 3 4 %}">跳转</a>
<a href="{% url 'jump' uid=3 nid=4 %}">跳转</a>

上面两种写法都可以,上面的是按照位置传参。下面故意先写了uid,会按照名字来传参。
访问测试页面随便输一个url,比如 “http://127.0.0.1:8000/jump-1-2/” 。然后页面里的两个a连接生成的是各自新的url。新url整体不变,但是捕获参数的值是在url名字后面的参数决定的。
引用当前页面的url:引申一下,{{ request.path_info }} 就是当前页面的url,如要要用的话,request默认就是处理函数里要返回的变量,所以页面里直接用就可以了。
在处理函数中根据名称获取url:
from django.urls import reverse 使用这个reverse也能获取到url。直接看获取带捕获的url的方法:

def jump(request, nid, uid):
    from django.urls import reverse
    r1 = reverse('jump', args=(3, 4))
    r2 = reverse('jump', kwargs={'uid':3, 'nid':4})
    print(r1, r2, request.path_info)
    # return HttpResponse('%s-%s' % (nid, uid))
    return render(request, 'jump.html')

也是2种形式都可以。

路由分发

之前所有的对应关系都是写在app目录外的urls文件里的。当我们的项目有多个app的时候,所有的页面都写在一起也不好。应该是每个app各自管理自己的那部分url。这就需要路由分发。
首先我们app目录外的公共url文件中导入一个include方法,声明好app的url文件的位置:

from django.urls import include
urlpatterns = [
    path('admin/', admin.site.urls),
    path('cmdb/', include("cmdb.urls"))
]

上面的例子中保留了原有的admin的对应关系,如果有别的公共页面还是可以在这里写的。然后是声明了cmdb这个app的url文件的位置。现在去 cmdb 目录下创建一个 urls.py 的文件。在这里写这个app的对应关系:

from django.urls import path
from cmdb import views
urlpatterns = [
    path('login/', views.login),
]

上面例子中的url是在cmdb这个app中的,所以访问的时候要带先加上app的名字,应该是 http://127.0.0.1:8000/cmdb/login/ 。这样app里可以有一个login页面,外面也可以有一个login页面。如果还有别的app,那个app也可以有login页面。名字前面会加上自己的app的名字,命名空间不冲突。
梳理一下逻辑:当一个url请求过来之后,先到达项目目录下的urls文件进行匹配。这里如果匹配到了项目名,比如cmdb。那么会再把它分发给之后的(就是app目录里的)urls文件继续处理。所以配置过分发后,首先还是到项目目录下的urls文件里进行匹配,然后再用这里的规则分发出去。

ORM

连接sqlite数据库

默认使用的是sqlite3作为数据库,使用数据库需要一下步骤
一、创建你的数据库表结构
app目录下的models.py文件就是用来写你的表结构的:

from django.db import models

# Create your models here.

# 文件默认会有上面的2行,下面是我们添加的内容
class UserInfo(models.Model):
    # 默认会自动创建自增id并作为主键
    username = models.CharField(max_length=32)  # 字符串,长度32
    password = models.CharField(max_length=64)

上面的类等到去数据库创建表的时候,表名是 “cmdb_userinfo” ,也就是 [app名]_[类名] 。
二、设置settings.py文件
在 INSTALLED_APPS 注册你的app,把你的app追加到这个列表里:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'cmdb',
]

配置你的数据库连接,默认已经配置好了一个sqlite3,所以不需要修改:

# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

三、去终端执行2条命令

python manage.py makemigrations
python manage.py migrate

第一条命令会在 app 目录下的 migrations 目录下创建一个文件(0001_initial.py),记录我们对 models.py 所做的变动。
第二条命令是真正去操作数据库了,除了创建我们自己写的表以外,还创建了很多 django 自己的表。
上面两条命令都是作用于全局的,如果要限定作用于只在某个app,可以在最后加上app的名称:

python manage.py makemigrations cmbd
python manage.py migrate cmdb

关于SQLite:

SQLite是一种嵌入式数据库,它的数据库就是一个文件。由于SQLite本身是C写的,而且体积很小,所以,经常被集成到各种应用程序中,甚至在iOS和Android的App中都可以集成。
Python就内置了SQLite3,所以,在Python中使用SQLite,不需要安装任何东西,直接使用。

使用SQLite

连接mysql数据库

步骤同上,理论上只要修改一下 settings.py 里的 DATABASES 的值就好了。但是还有一些别的坑。这里主要演示一下怎么连上mysql数据库,连上之后,后面的操作还是在SQLite下来做。
DATABASES 设置的上面就是官方的帮助文档的连接,或者直接参考下面的进行设置就好了:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'cmdb',
        'USER': 'admin',
        'PASSWORD': 'admin123',
        'HOST': '192.168.246.12',
        'port': '3306',
    }
}

然后是坑,首先用户我们得自己去数据库上创建好,注意如果不是本地的数据库,需要能够远程访问。库也要自己创建好,创建库:

CREATE DATABASE cmdb CHARSET "utf8";

然后可以试着执行终端的2条命令,但是可能会报错:

import MySQLdb as Database
ModuleNotFoundError: No module named 'MySQLdb'

意思是找不到这个库,在python3里mysql我们用 pymysql 这个库。不过装好了pymysql还是会提示找不到库,因为django就是耿直的要找MySQLdb。解决办法是编辑项目名同名目录下的 __init__.py 文件,在这里导入我们的pymysql并且会把它的名字就当做是 MySQLdb :

import pymysql
pymysql.install_as_MySQLdb()

ORM操作

添加

添加数据有2种方法,推荐用第一种。下面是写在app目录的views.py里的处理函数:

from cmdb import models
def add_user(request):
    models.UserInfo.objects.create(
        username='root',
        password='123456'
    )
    # 另外一个方法,先创建一个实例,然后调用它的save()方法
    obj = models.UserInfo(
        username='admin',
        password='admin123'
    )
    obj.save()
    # 方法一的变种,把字典直接作为参数传入
    dic = {'username': 'user', 'password': 'user123'}
    models.UserInfo.objects.create(**dic)
    return HttpResponse("add user")

首先我们要操作某个表,就要先把这个创建这个表的那个类导入进来,例子的第一行。上面一个创建了3条数据了。

查询

用all方法查询到的数据,首先是放在一个列表里,列表的元素是一个一个的对象,每一个对象就是一条记录。
筛选的方法有filter,这个返回的也是个列表,因为可能返回多条。

def show_user(request):
    res = models.UserInfo.objects.all()
    for row in res:
        print(row.id, row.username, row.password)
    users = models.UserInfo.objects.filter(username='root')
    for row in users:
        print(row.id, row.username, row.password)
    return HttpResponse("show user")

filter()里面还可以传入多个参数,就是多个条件,他们之间的关系是逻辑与(and)。
还有一个first()方法,取出第一个值,这样返回就不是列表而直接就是对象了。可以直接用,也可以用在filter()方法后面。all()方法后面也是可以用的,不过没意义

models.UserInfo.objects.first()
models.UserInfo.objects.filter(username='root', password='123456').first()
# 上面就是一个验证用户登录的逻辑了,返回结果是None或者是找到的第一个对象

另外还有一个get方法也可以获取到一条数据,但是如果数据不存在不是返回空而是会报错。如果要用那就得写个try:

models.UserInfo.objects.get(id=10)

QuerySet 对象,分别打印出查询结果和这个对象的query属性:

res = models.UserInfo.objects.all()
print(res)  # 结果在下面
# <QuerySet [<UserInfo: UserInfo object (1)>, <UserInfo: UserInfo object (2)>, <UserInfo: UserInfo object (3)>]>
print(res.query)  # 结果写下面
# SELECT "cmdb_userinfo"."id", "cmdb_userinfo"."username", "cmdb_userinfo"."password" FROM "cmdb_userinfo"

可以看到这是一个 QuerySet 对象,不是一个普通的列表。这里要引出它的一个属性 query 。
这个对象有一个query属性,该属性的内容是获取这个对象时对应的SQL语句。

删除

删除前首先要先做查找,调用查找结果的delete()方法,就完成了删除:

models.UserInfo.objects.all().delete
models.UserInfo.objects.filter(id=2).delete

修改

修改也是在查找的基础上,调用update()方法来完成的:

models.UserInfo.objects.filter(id=1).update(password='root123')

示例

先来写一个登录页面 index.html 用来提交用户名和密码进行验证:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        label{
            width: 80px;
            text-align: right;
            display: inline-block;
        }
    </style>
</head>
<body>
<form action="/login/" method="post">
    <p>
        <label for="usernmae">用户名:</label>
        <input id="usernmae" name="user" type="text" />
    </p>
    <p>
        <label for="password">密码:</label>
        <input id="password" name="pwd" type="password" />
        <input type="submit" value="提交">
        <span style="color: red">{{ error_msg }}</span>
    </p>
</form>
</body>
</html>

然后是index的处理函数,用户验证失败报错误信息,验证成功跳转的下一个页面:

def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')
    elif request.method == 'POST':
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        obj = models.UserInfo.objects.filter(username=user, password=pwd).first()
        if obj:
            # 先跳转到admin,可以测一下,之后再写index页面
            return redirect('/admin/')
            # return redirect('/userlist/')
        else:
            return render(request,
                          'login.html',
                          {'error_msg': '用户名或密码错误'})
    else:
        return HttpResponse("不支持您的请求方式")

测试跳转没问题之后,就可以把上面的跳转从admin页面换到userlist页面,然后就来写这个userlist页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <h2>添加用户</h2>
    <form method="post" action="/userlist/">
        <input type="text" name="user" placeholder="username">
        <input type="text" name="pwd" placeholder="password">
        <input type="submit" value="添加">
    </form>
</div>
<div>
    <h2>用户列表</h2>
    <table border="1">
        <thead>
        <tr>
            <th>id</th>
            <th>username</th>
            <th>password</th>
            <th>按钮</th>
        </tr>
        </thead>
        <tbody>
        {% for row in users %}
        <tr>
            <td>{{ row.id }}</td>
            <td>{{ row.username }}</td>
            <td>{{ row.password }}</td>
            <td>
                <a href="/userdel-{{ row.id }}/">删除</a>
                <span>|</span>
                <a href="/useredit-{{ row.id }}/">编辑</a>
            </td>
        </tr>
        {% endfor %}
        </tbody>
    </table>
</div>
</body>
</html>

首先页面的中要实现数据库查询的功能,就是显示用户列表,通过GET方法来实现。
另外还有一个增加数据的功能,页面上面的添加用户,请求是通过POST方法提交过来,完成的数据添加。POST方法可以有2中return的方式,直接的方式就是和GET方法一样。或者也可以用例子里使用的方法,就是再提交一次GET请求:

def user_list(request):
    if request.method == 'GET':
        users = models.UserInfo.objects.all()
        return render(request, 'userlist.html', {'users': users})
    elif request.method == 'POST':
        username = request.POST.get('user')
        password = request.POST.get('pwd')
        models.UserInfo.objects.create(username=username, password=password)
        # 这里可以和get返回的一样
        # return render(request, 'userlist.html', {'users': users})
        # 这里选择用redirect()方法返回,就是再调用一次get方法返回页面
        return redirect('/userlist/')

删除功能不需要写页面,只需要一个处理函数:

def user_del(request, nid):
    models.UserInfo.objects.filter(id=nid).delete()
    return redirect('/userlist/')

最后还有一个编辑功能,现在只能用写一个新的页面然后再那个页面里提交。这样实现起来比较简单,主要就是通过这个示例把数据库的最删改查都用一遍:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <h2>编辑用户</h2>
    <form method="post" action="/useredit-{{ obj.id }}/">
        <input type="text" name="user" value="{{ obj.username }}">
        <input type="text" name="pwd" value="{{ obj.password }}">
        <input type="submit" value="提交">
    </form>
</div>
</body>
</html>

这个页面对应的处理函数是如下:

def user_edit(request, nid):
    if request.method == 'GET':
        obj = models.UserInfo.objects.filter(id=nid).first()
        return render(request, 'useredit.html', {'obj': obj})
    elif request.method == 'POST':
        username = request.POST.get('user')
        password = request.POST.get('pwd')
        models.UserInfo.objects.filter(id=nid).update(
            username=username, password=password)
        return redirect('/userlist/')

在userlist页面点击编辑按钮后,GET请求跳转到useredit页面。在编辑页面提交后向useredit发送一个POST请求修改数据,然后返回userlist页面,完成一次编辑。
上面一个有4个处理函数,其中3个有html页面,urls.py的对应关系如下:

    path('login/', views.login),
    path('userlist/', views.user_list),
    path('userdel-<int:nid>/', views.user_del),
    path('useredit-<int:nid>/', views.user_edit),

ORM表结构

修改表结构

修改过表结构之后,需要再执行一下下面的2行命令,把新的表结构应用到数据库。

python manage.py makemigrations
python manage.py migrate

修改数据长度、删除一列,这类情况没什么特别的问题。
增加一列,默认情况下字段值不允许为空,此时会有提示。要么全部都设为空,要么你给个默认值,全部都设为默认值。另外还可以直接定义到表结构中:

class UserInfo(models.Model):
    # 默认会自动创建自增id并作为主键
    username = models.CharField(max_length=32)  # 字符串,长度32
    password = models.CharField(max_length=64)
    email = models.CharField(max_length=64, null=True)  # 设置为允许空值

字段类型

基本的字段类型有:字符串、数字、时间、二进制。
Django的ORM提供了非常多的字段类型,比如:EmailField、URLField、GenericIPAddressField。这些其实都是字符串类型而已,并且确实对我们没任何用(并不能帮我们做数据验证)。这些字段类型的只有在用Django的后台管理页面 admin 的时候才能发挥数据验证的效果。只有通过admin提交数据的时候才会验证你的数据格式是否正确。接下来就先讲怎么登进去
自增id,之前定义表结构的时候,省略了主键,让Django帮我创建了自增id。也可以自己定义主键和自增id:

class UserGroup(models.Model):
    uid = models.AutoField(primary_key=True)  # 数据类型是自增,并且设为主键
    group = models.CharField(max_length=32)

登录Admin

admin具体要到后面讲,这里先让我们登录进去

  1. 创建超级管理员,输入命令后会提示你输入用户名、邮箱(可以直接回车)、密码(似乎有长度和复杂度的要求):
    python manage.py createsuperuser
  2. 配置后台管理url,就是admin页面的对应关系,默认urls.py里面已经配好了:
    path('admin/', admin.site.urls),
  3. 注册和配置 admin 后台管理页面,把你的表注册号之后,就可以通过admin进行管理了:
    from django.contrib import admin
    # Register your models here.
    # 上面是文件默认就有的内容
    from cmdb import models
    admin.site.register(models.UserInfo)

参数

null :数据库中字段是否可以为空
default :数据库中字段的默认值
db_column :数据库中字段的列名。默认列明就是我们的变量名,可以通过这个参数设置成不一样的

class UserInfo(models.Model):
    username = models.CharField(max_length=32)  # 字段名就是变量名 username
    password = models.CharField(max_length=64, db_column='pwd')  # 数据库中的字段名会是 pwd

db_index :是否建立索引
unique :是否建立唯一索引
unique_for_date :只对字段中【日期】部分建立唯一索引
unique_for_month :只对字段中【月】部分建立唯一索引
unique_for_year :只对字段中【年】部分建立唯一索引
auto_now :自动生成一个当前时间,数据更新时(包括创建)
auto_now_add :自动生成一个当前时间,数据创建时

class UserInfo(models.Model):
    # 比如用户注册时会生成用户名和密码
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=64)
    # 创建记录时会生成当前时间存放在ctime里,这个就是用户的注册时间
    ctime = models.DataTimeField(auto_now_add=True)
    # 用户修改密码会更新uptime的时间,这个就是上次修改密码的时间
    uptime = models.DataTimeField(auto_now=True)

# models.UserInfo.objects.filter(id=nid).update(password='123456') 这种方法更新是不会刷新 auto_now 的时间的
# 用save()方法更新可以刷新 auto_now 的时间
# obj =  models.UserInfo.objects.filter(id=nid).first()
# obj.password = '654321'
# obj.save()

Admin中有效果的参数
choices :Admin中显示选择框的内容。(用不变动的数据放在内存中从而避免跨表操作,跨表操作会涉及到性能问题)

class UserInfo(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=64)
    # 用户有各种类型
    user_type_choices = (
        (1, '管理员')
        (2, '普通用户')
        (3, '访客')
    )
    # 定义一个用户类型的字段
    user_type_id = models.IntegerField(choices=user_type_choices, default=2)
# 这样数据库里是一个整数类型,值是1、2、3。使用字段名取值 obj.user_type_id 获取到的是数值
# 如果要获取后面的内容,使用 get_FOO_display() 方法, 即 obj.get_user_type_id_display()
# 但是我们在admin里看选择框的时候看到的是“管理员”、“普通用户”、“访客”,这就是因为把选项所对应的内容放到了内存中了
# 有了Django这个功能就不用再搞一张表,存放各个数值对应的内容了,还要做外键关联,用的时候还要连表查询
# 即使不用admin,我们也可以在自己的代码里读取这个属性获取到内容,避免连表查询

blank :Admin中是否允许用户输入为空
verbose_name :Admin中显示的字段名称,默认显示为变量名
editable :Admin中是否可以编辑。默认是True,设为False后就是在admin中不可编辑了,也不会显示出来了。
error_messages :自定义错误信息(字典类型)。字典key:null、blank、invalid、invalid_choice、unique、unique_for_date

class UserInfo(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=64, error_messages={'null': "不能为空", 'invalid': '格式错误'})

help_text :Admin中该字段的提示信息。默认没有提示信息,设置后会显示在input框的下方
validators :自定义错误验证(列表类型),具体要等到后面讲

外键操作-一对多

上面讲的choices参数,提供了一种将数据存在内存中来提高效率的方法。好处是避免了跨表操作提高了效率。坏处也有,就是数据不方便修改。如果要修改,那就要修改好之后重启一下服务使你的修改生效。而重启操作是有风险的应该避免,那么对于这种经常要修改的内容就不适合放在内存中了,而是要放到另外一张表里。

创建外键关联-修改表结构

在models.py里修改我们的表结构,新增一张用户部门表,原来的用户信息表中新增一列部门id:

from django.db import models

# Create your models here.

# 新增一张表
class UserGroup(models.Model):
    group_id = models.AutoField(primary_key=True)  # 这次自己写自增id
    dept = models.CharField(max_length=32, unique=True)

class UserInfo(models.Model):
    # 默认会自动创建自增id并作为主键
    username = models.CharField(max_length=32)  # 字符串,长度32
    password = models.CharField(max_length=64)
    # 新增一列存放部门
    # to_field参数可以缺省,默认就是主键
    # on_delete=models.CASCADE,这个在1里应该是默认值,现在不能缺省了
    user_group = models.ForeignKey('UserGroup', on_delete=models.CASCADE, to_field='group_id')

然后去终端执行那2条命令使新的表结构生效:

python manage.py makemigrations
python manage.py migrate

添加部门数据

这里可以的话最好直接是直接去操作数据库,否则简单搞个网页来添加数据。参看之前的userlist,简单搞个只做显示和添加的页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <h2>添加部门</h2>
    <form method="post" action="/grouplist/">
        <input type="text" name="dept" placeholder="部门名称">
        <input type="submit" value="添加">
    </form>
</div>
<div>
    <h2>部门列表</h2>
    <table border="1">
        <thead>
        <tr>
            <th>id</th>
            <th>dept</th>
        </tr>
        </thead>
        <tbody>
        {% for row in dept %}
        <tr>
            <td>{{ row.group_id }}</td>
            <td>{{ row.dept }}</td>
        </tr>
        {% endfor %}
        </tbody>
    </table>
</div>
</body>
</html>

然后是views.py里的处理函数:

def group_list(request):
    if request.method == 'GET':
        dept = models.UserGroup.objects.all()
        return render(request, 'grouplist.html', {'dept': dept})
    elif request.method == 'POST':
        dept = request.POST.get('dept')
        models.UserGroup.objects.create(dept=dept)
        return redirect('/grouplist/')

urls.py里的对应g关系:

    path('grouplist/', views.group_list),

查看被关联的属性

对于UserInfo中新增的一列,在类中我们的属性名称是 "user_group" ,而实在在数据库中创建的自动名是 "user_group_id"。
我们再操作的时候就有2个属性可以操作:

  • .user_group_id :就是这个字段里的值,也就是数据库里实际存放的内容
  • .user_group :这是一个对象,通过这个对象取到UserGroup里的内容,比如:
    • .user_group.group_id :就是UserGroup表里的自增id,结果和 .user_group_id 应该是一样的
    • .user_group.dept :就是这个username锁关联的部门名称了

修改之前的userlist页面,现在把部门名称也显示出来。这里只需要改html,处理函数时不用修改的。实际也只需要在表格中加上一列直接可以去到关联的表里的属性值。下面是userlist表格的部分内容:

<div>
    <h2>用户列表</h2>
    <table border="1">
        <thead>
        <tr>
            <th>id</th>
            <!-- 这里加一列 -->
            <th>username</th>
            <th>dept</th>
            <th>password</th>
            <th>按钮</th>
        </tr>
        </thead>
        <tbody>
        {% for row in users %}
        <tr>
            <td>{{ row.id }}</td>
            <td>{{ row.username }}</td>
            <!-- 这里加一列,直接就能取到部门名称 -->
            <td>{{ row.user_group.dept }}</td>
            <td>{{ row.password }}</td>
            <td>
                <a href="/userdel-{{ row.id }}/">删除</a>
                <span>|</span>
                <a href="/useredit-{{ row.id }}/">编辑</a>
            </td>
        </tr>
        {% endfor %}
        </tbody>
    </table>
</div>

添加用户时选择部门

显示没问题了,页面的上部还有添加用户,现在再要添加用户就需要把用户部门也加上了。部门搞成一个下拉框,不过下拉框的内容还需要修改处理函数传值过来。处理函数还要处理页面提交的内容:

def user_list(request):
    if request.method == 'GET':
        users = models.UserInfo.objects.all()
        # 这里多获取一个部门的列表,传给页面,页面的下来列表会用到。直接找UserGroup获取数据
        # 把对象传给页面的下拉列表,列表的value就是对象的id,列表的内容就是对象的dept
        depts = models.UserGroup.objects.all()
        return render(request, 'userlist.html', {'users': users, 'depts': depts})
    elif request.method == 'POST':
        username = request.POST.get('user')
        password = request.POST.get('pwd')
        # 这里通过select获取到的直接就是id的值,所以提交的时候也简单的提交值就可以了
        group_id = request.POST.get('group_id')
        models.UserInfo.objects.create(username=username, password=password, user_group_id=group_id)
        return redirect('/userlist/')

页面里加上下拉列表,下面是添加用户的部分:

<div>
    <h2>添加用户</h2>
    <form method="post" action="/userlist/">
        <input type="text" name="user" placeholder="username">
        <select name="group_id">
            {% for item in depts %}
            <option value="{{ item.group_id }}">{{ item.dept }}</option>
            {% endfor %}
        </select>
        <input type="text" name="pwd" placeholder="password">
        <input type="submit" value="添加">
    </form>
</div>

数据添加的另外一种方法
上面通过下拉列表方便的获取到了部门id的值,所以直接通过传值给user_group_id完成了数据的添加。也可以通过传对象给user_group完成数据的添加,大概是这样的:

        group = models.UserGroup.objects.filter(id=1).first()
        models.UserInfo.objects.create(username=username, password=password, user_group=group)

两种方法根据实际情况选择,不过传值的方法更好,少一次数据库的操作。
还有更多内容要下节讲了

模板

这天没讲到

课后练习

用户管理:

  • 一张用户表、一张用户组表。做好外键关联。分别实现对两张表的增删改查
  • 添加,做成模态对话框的形式
  • 修改,目前可以用页面跳转的形式,但是要显示默认值
  • 做一个比较好看的页面,推荐套用模板