[Python自学] day-20 (Django-ORM、Ajax)
一、外键跨表操作(一对多)
在 [Python自学] day-19 (2) (Django-ORM) 中,我们利用外键实现了一对多的表操作。
可以利用以下方式来获取外键指向表的数据:
def orm_test(request): # 向UserGroup表中插入一个group (gid=1,groupname='Dev') models.UserGroup.objects.create(groupname="Dev") # 向UserInfo表中插入一个user (uid=1,username='Leo',password='123',group_id=1),注意这里要使用真实列名group_id models.UserInfo.objects.create(username="Leo", password='123', group_id=1) # 联合查询UserInfo以及UserGroup # 获取第一个用户,这里只有一个用户Leo obj = models.UserInfo.objects.filter(uid=1).first() # 打印用户的用户名、密码(这些内容都在UserInfo表中) print(obj.username) print(obj.password) # 这里注意,group是外键,指向UserGroup表,所以这里的group属性是一个对象(UserGroup表的一条记录),我们通过该对象来获取groupname print(obj.group.groupname) return HttpResponse('ok')
但是,当我们在获取局部列数据的情况下:
def orm_test(request): # 因为使用了values,所以QuerySet v中的元素都是字典 # 在values()中,使用'__'来跨表操作 v = models.UserInfo.objects.filter(uid=1).values('username','group_id','group__groupname') for row in v: print(row['username']) print(row['group_id']) # 使用key来获取字典中的值,如果是通过render返回给模板,则模板语言也要使用v.group__groupname来获取值 print(row['group__groupname']) return HttpResponse('ok')
以上规则同样适合于使用values_list()的情况,只不过将QuerySet内部元素变成元组而已。values_list()中跨表也使用"__"。
二、实现简单资产管理
1.首先使用models.py在数据库中创建两张表
from django.db import models # Create your models here. # 创建一个业务线表 class Business(models.Model): businame = models.CharField(max_length=32) # 创建一个主机表 class Host(models.Model): nid = models.AutoField(primary_key=True) hostname = models.CharField(max_length=32, db_index=True) ip = models.GenericIPAddressField(protocol='ipv4', db_index=True) port = models.IntegerField() busi = models.ForeignKey('Business', on_delete=models.CASCADE, to_field='id')
在工程的setting中进行配置:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'cmdb', 'mgmt', ]
执行命令:
python manage.py makemigrations
python manage.py migrate
我们先手工在Business表中添加一些数据(这里主要为了方便,真正管理系统中,业务线肯定也是页面上进行添加删除的):
2.创建mgmt/urls.py映射
from django.contrib import admin from django.urls import path from django.urls import re_path from mgmt import views urlpatterns = [ path('index/', views.index), path('host/', views.host), ]
3.添加基础版host.html文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Host Page</title> </head> <body> <h1>主机列表</h1> <table border="1"> <thead> <tr> <th>主机名</th> <th>IP</th> <th>端口</th> <th>业务线</th> </tr> </thead> <tbody> {% for row in host_list %} <tr hid="{{ row.nid }}" bid="{{ row.busi_id }}"> <td>{{ row.hostname }}</td> <td>{{ row.ip }}</td> <td>{{ row.port }}</td> <td>{{ row.busi.businame }}</td> </tr> {% endfor %} </tbody> </table> <h1>业务线列表</h1> <table border="1"> <thead> <tr> <th>业务线ID</th> <th>业务线名称</th> </tr> </thead> <tbody> {% for row in busi_list %} <tr> <td>{{ row.id }}</td> <td>{{ row.businame }}</td> </tr> {% endfor %} </tbody> </table> </body> </html>
4.编写对应的mgmt/views.py中的host方法
def host(request): # 如果是get请求,则将数据库中查询到的host列表和业务线列表返回,展示在页面上 if request.method == 'GET': host_list = models.Host.objects.all() busi_list = models.Business.objects.all() return render(request, 'host.html', {'host_list': host_list, 'busi_list': busi_list})
访问http://127.0.0.0:8000/mgmt/host,效果如下:
因为目前还未添加主机条目,所以为空。业务线数据我们在前面用手工的方式添加到了数据库中。
5.修改html,为主机列表添加模态对话框(用于添加主机)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Host Page</title> <style> .shade{ position: fixed; top:0px ; bottom: 0px; left: 0px; right: 0px; background-color: black; opacity: 0.5; z-index: 100; } .hide{ display: none; } .add_modal{ background-color: #eeeeee; position: fixed; height: 300px; width: 400px; top:100px; left:50%; margin-left: -200px; z-index: 101; border: 2px solid #dddddd; } </style> </head> <body> <h1>主机列表</h1> <input id='add_host' type="button" value="添加主机"/> <table border="1"> <thead> <tr> <th>主机名</th> <th>IP</th> <th>端口</th> <th>业务线</th> </tr> </thead> <tbody> {% for row in host_list %} <tr hid="{{ row.nid }}" bid="{{ row.busi_id }}"> <td>{{ row.hostname }}</td> <td>{{ row.ip }}</td> <td>{{ row.port }}</td> <td>{{ row.busi.businame }}</td> </tr> {% endfor %} </tbody> </table> <h1>业务线列表</h1> <table border="1"> <thead> <tr> <th>业务线ID</th> <th>业务线名称</th> </tr> </thead> <tbody> {% for row in busi_list %} <tr> <td>{{ row.id }}</td> <td>{{ row.businame }}</td> </tr> {% endfor %} </tbody> </table> <!-- 遮罩 --> <div class="shade hide"></div> <!-- 添加主机 弹窗 --> <div class="add_modal hide"> <form action="/mgmt/host/" method="post"> <div class="group"> <input type="text" placeholder="主机名" name="hostname"/> </div> <div class="group"> <input type="text" placeholder="IP地址" name="ip"/> </div> <div class="group"> <input type="text" placeholder="端口" name="port"/> </div> <div class="group"> <select name="busi_id"> {% for bi in busi_list %} <option value="{{ bi.id }}">{{ bi.businame }}</option> {% endfor %} </select> </div> <input type="submit" value="提交"/> <input id='cancel' type="button" value="取消"/> </form> </div> <script src="/static/jquery-1.12.4.js"></script> <script> $(function(){ // 为添加主机按钮绑定事件,显示遮罩层和模态框 $('#add_host').click(function(){ $('.shade,.add_modal').removeClass('hide'); }); // 为模态框的取消按钮绑定事件,隐藏遮罩层和模态框 $('#cancel').click(function(){ $('.shade,.add_modal').addClass('hide'); }) }); </script> </body> </html>
6.修改mgmt/views.py中的host()视图函数
def host(request): # 如果是get请求,则将数据库中查询到的host列表和业务线列表返回,展示在页面上 if request.method == 'GET': host_list = models.Host.objects.all() busi_list = models.Business.objects.all() return render(request, 'host.html', {'host_list': host_list, 'busi_list': busi_list}) elif request.method == 'POST': # 当用户使用模态框添加主机时,使用表单POST提交 # 获取表单提交的数据 host = request.POST.get('hostname') ip = request.POST.get('ip') port = request.POST.get('port') # 这里的busi获取到的是select对应的busi_id busi = request.POST.get('busi_id') # 插入数据库 models.Host.objects.create( hostname=host, ip=ip, port=port, busi_id=busi ) # 重定向到host页面,以GET重新请求,页面就可以显示新的值 return redirect('/mgmt/host')
接受来自模态框表单提交的数据,插入数据库,并重定向到/mgmt/host页面,展示新数据。
7.页面效果:
三、Ajax
Ajax:Asynchronous Javascript And XML,异步的JS和XML。
主要用于免刷新页面提交数据。
例如,在第二节中实现的模态框,我们在提交数据前并未对输入的数据格式进行验证,例如是否为空,格式是否满足要求等。因为在模态框中要验证内容,只能使用以前了解的绑定多个事件来验证的方法(前端方法验证)。
而有了Ajax,我们就可以不刷新页面提交数据到后台进行验证:
(这里使用的是jQuery的ajax组件)
1.首先在模态框中添加一个按钮,用于发送ajax异步请求
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Host Page</title> <style> .shade{ position: fixed; top:0px ; bottom: 0px; left: 0px; right: 0px; background-color: black; opacity: 0.5; z-index: 100; } .hide{ display: none; } .add_modal{ background-color: #eeeeee; position: fixed; height: 300px; width: 400px; top:100px; left:50%; margin-left: -200px; z-index: 101; border: 2px solid #dddddd; } </style> </head> <body> <h1>主机列表</h1> <input id='add_host' type="button" value="添加主机"/> <table border="1"> <thead> <tr> <th>主机名</th> <th>IP</th> <th>端口</th> <th>业务线</th> </tr> </thead> <tbody> {% for row in host_list %} <tr hid="{{ row.nid }}" bid="{{ row.busi_id }}"> <td>{{ row.hostname }}</td> <td>{{ row.ip }}</td> <td>{{ row.port }}</td> <td>{{ row.busi.businame }}</td> </tr> {% endfor %} </tbody> </table> <h1>业务线列表</h1> <table border="1"> <thead> <tr> <th>业务线ID</th> <th>业务线名称</th> </tr> </thead> <tbody> {% for row in busi_list %} <tr> <td>{{ row.id }}</td> <td>{{ row.businame }}</td> </tr> {% endfor %} </tbody> </table> <!-- 遮罩 --> <div class="shade hide"></div> <!-- 添加主机 弹窗 --> <div class="add_modal hide"> <form action="/mgmt/host/" method="post"> <div class="group"> <input type="text" placeholder="主机名" name="hostname"/> </div> <div class="group"> <input type="text" placeholder="IP地址" name="ip"/> </div> <div class="group"> <input type="text" placeholder="端口" name="port"/> </div> <div class="group"> <select name="busi_id"> {% for bi in busi_list %} <option value="{{ bi.id }}">{{ bi.businame }}</option> {% endfor %} </select> </div> <input type="submit" value="提交"/> <input id='ajax_submit' type="button" value="Alax提交"/> <input id='cancel' type="button" value="取消"/> </form> </div> <script src="/static/jquery-1.12.4.js"></script> <script> $(function(){ // 为添加主机按钮绑定事件,显示遮罩层和模态框 $('#add_host').click(function(){ $('.shade,.add_modal').removeClass('hide'); }); //为ajax_submit按钮绑定事件,发送Ajax请求 $('#ajax_submit').click(function(){ $.ajax({ url: '/mgmt/test_ajax', type: 'GET', data: {'user':'root','pwd':'123123'}, success:function(data) { alert(data) } }); }); // 为模态框的取消按钮绑定事件,隐藏遮罩层和模态框 $('#cancel').click(function(){ $('.shade,.add_modal').addClass('hide'); }) }); </script> </body> </html>
添加一个按钮,叫做"Ajax提交",然后为其绑定点击事件,点击按钮是,提交Ajax异步请求,请求url为"/mgmt/test_ajax",请求方式为"GET",传递数据为 " {'user':'root','pwd':'123123'} "。
最重要的一点是,设置了一个"success:function(data){}",这是一个回调函数,当收到后台返回的数据后,被自动调用。
2.在mgmt/urls.py中添加一个映射,用于处理/mgmt/test_ajax
from django.contrib import admin from django.urls import path from django.urls import re_path from mgmt import views urlpatterns = [ path('index', views.index), path('host/', views.host), re_path('test_ajax$', views.test_ajax), ]
3.在mgmt/views.py中实现一个test_ajax()视图函数
def test_ajax(request): # 获取Ajax请求方式和内容 print(request.method) print(request.GET.get('user')) print(request.GET.get('pwd')) # 等待3s后回复数据 time.sleep(3) return HttpResponse("Ajax回复信息")
4.页面效果
以上效果可以看出,点击"Ajax提交"按钮后,后台收到了传递的data,然后在3s后返回"Ajax回复信息",前台使用alert()显示出来。
5.修改Ajax请求的数据,将输入框的数据发送到后台验证
html代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Host Page</title> <style> .shade{ position: fixed; top:0px ; bottom: 0px; left: 0px; right: 0px; background-color: black; opacity: 0.5; z-index: 100; } .hide{ display: none; } .add_modal{ background-color: #eeeeee; position: fixed; height: 300px; width: 400px; top:100px; left:50%; margin-left: -200px; z-index: 101; border: 2px solid #dddddd; } .error{ color: red; } </style> </head> <body> <h1>主机列表</h1> <input id='add_host' type="button" value="添加主机"/> <table border="1"> <thead> <tr> <th>主机名</th> <th>IP</th> <th>端口</th> <th>业务线</th> </tr> </thead> <tbody> {% for row in host_list %} <tr hid="{{ row.nid }}" bid="{{ row.busi_id }}"> <td>{{ row.hostname }}</td> <td>{{ row.ip }}</td> <td>{{ row.port }}</td> <td>{{ row.busi.businame }}</td> </tr> {% endfor %} </tbody> </table> <h1>业务线列表</h1> <table border="1"> <thead> <tr> <th>业务线ID</th> <th>业务线名称</th> </tr> </thead> <tbody> {% for row in busi_list %} <tr> <td>{{ row.id }}</td> <td>{{ row.businame }}</td> </tr> {% endfor %} </tbody> </table> <!-- 遮罩 --> <div class="shade hide"></div> <!-- 添加主机 弹窗 --> <div class="add_modal hide"> <form action="/mgmt/host/" method="post"> <div class="group"> <input id="hostname" type="text" placeholder="主机名" name="hostname"/> </div> <div class="group"> <input id="ip" type="text" placeholder="IP地址" name="ip"/> </div> <div class="group"> <input id="port" type="text" placeholder="端口" name="port"/> </div> <div class="group"> <select id="busi_id" name="busi_id"> {% for bi in busi_list %} <option value="{{ bi.id }}">{{ bi.businame }}</option> {% endfor %} </select> <span id="error_label"></span> </div> <input type="submit" value="提交"/> <input id='ajax_submit' type="button" value="Alax提交"/> <input id='cancel' type="button" value="取消"/> </form> </div> <script src="/static/jquery-1.12.4.js"></script> <script> $(function(){ // 为添加主机按钮绑定事件,显示遮罩层和模态框 $('#add_host').click(function(){ $('.shade,.add_modal').removeClass('hide'); }); //为ajax_submit按钮绑定事件,发送Ajax请求 $('#ajax_submit').click(function(){ $.ajax({ url: '/mgmt/test_ajax', type: 'POST', data: { 'hostname':$("#hostname").val(), 'ip':$("#ip").val(), 'port':$("#port").val(), 'busi_id':$("#busi_id").val() }, success:function(data) { var err = JSON.parse(data); if(err.status){ location.reload(); }else{ $("#error_label").text(err.err_msg); $("#error_label").addClass('error'); } } }); }); // 为模态框的取消按钮绑定事件,隐藏遮罩层和模态框 $('#cancel').click(function(){ $('.shade,.add_modal').addClass('hide'); }) }); </script> </body> </html>
我们在<select>标签后面添加了一个错误提示<span>标签,当后台返回格式错误时,我们将错误信息显示在该位置。
test_ajax()视图函数:
def test_ajax(request): host = request.POST.get('hostname') ip = request.POST.get('ip') port = request.POST.get('port') # 这里的busi获取到的是select对应的busi_id busi = request.POST.get('busi_id') err = {'status': True, 'err_msg': None} if len(host) < 6 or len(host) > 30: err['status'] = False err['err_msg'] = '主机名长度必须在6-30之间' try: # 插入数据库 models.Host.objects.create( hostname=host, ip=ip, port=port, busi_id=busi ) except Exception as e: err['status'] = False err['err_msg'] = '请求错误,请检查' import json return HttpResponse(json.dumps(err))
返回错误信息时,由于HttpResponse只能返回字符串,所以需要将字典序列化成字符串。然后在前端JS代码中进行反序列化。
建议:在后台返回数据时,数据以字典的形式组织,并使用JSON序列化。
6.最终效果
7.注意事项 & 获取表单数据的简单方法
1)Ajax请求的时候,后台必须使用HttpResponse来返回数据,不能使用redirect(对Ajax无效)。render是可以使用的,但是一般render用来渲染一个html页面,所以也不适合Ajax。
2)Ajax提供一个方便的获取表单填入数据的方法:
$.ajax({ url: '/mgmt/test_ajax', type: 'POST', /*data: { 'hostname':$("#hostname").val(), 'ip':$("#ip").val(), 'port':$("#port").val(), 'busi_id':$("#busi_id").val() },*/ data: $("#add_form").serialize(), success:function(data) { var err = JSON.parse(data); if(err.status){ location.reload(); }else{ $("#error_label").text(err.err_msg); $("#error_label").addClass('error'); } } });
其中data部分,直接使用" $("#表单ID").serialize() "来获取表单中所有输入标签的值。然后通过Ajax发送到后台,后台获取值得方式不变,还是使用GET或POST来获取。
四、ORM多对多关系
多对多的意思是,两个表的记录之间存在多对多关系。例如主机表和应用表之间存在多对多关系,主机A对应应用1和2,应用1又可以对应主机A和B。
这种多对多关系,由两张单独的表是不能表现的。所以要借助第三张表:关系表。
我们使用Django-ORM的时候,有以下两种方式创建关系表:
1.手工创建(比较灵活,关系表的字段可以任意添加和修改)
# 创建主机表 class Host(models.Model): nid = models.AutoField(primary_key=True) hostname = models.CharField(max_length=32, db_index=True) ip = models.GenericIPAddressField(protocol='ipv4', db_index=True, null=True) port = models.IntegerField() busi = models.ForeignKey('Business', on_delete=models.CASCADE, to_field='id') # 创建应用表 class Application(models.Model): appname = models.CharField(max_length=64) # 创建主机--应用关系表(用于描述多对多关系) class HostToApp(models.Model): hobj = models.ForeignKey('Host', to_field='nid') aobj = models.ForeignKey('Application', to_field='id')
在这种创建方式下,关系表示我们手工通过ORM类来创建的,我们通过如下方式来操作:
# 创建一条hostA--App2的关系 models.HostToApp.objects.create(hobj_id=1, aobj_id=2) # 创建一条hostA--App3的关系 models.HostToApp.objects.create(hobj_id=1, aobj_id=3) # 创建一条hostB--App1的关系 models.HostToApp.objects.create(hobj_id=2, aobj_id=1) # 创建一条hostB--App3的关系 models.HostToApp.objects.create(hobj_id=2, aobj_id=3)
2.自动创建(自动生成关系表,但是无法直接通过类操作,无法任意增加列)
# 创建主机表 class Host(models.Model): nid = models.AutoField(primary_key=True) hostname = models.CharField(max_length=32, db_index=True) ip = models.GenericIPAddressField(protocol='ipv4', db_index=True, null=True) port = models.IntegerField() busi = models.ForeignKey('Business', on_delete=models.CASCADE, to_field='id') # 创建应用表 class Application(models.Model): appname = models.CharField(max_length=64) # 创建与Host表的多对多关系,有这句,Django就会帮我们自动创建一张关系表 rel = models.ManyToManyField('Host')
执行命令:
python manage.py makemigrations
python manage.py migrate
查看数据库中自动生成的关系表:
我们可以看到,Django帮我们自动生成了一张mgmt_application_rel的关系表。
由于这个关系表示自动生成的,我们无法直接使用对应的类来操作,所以只能通过Applicaiton类对象中的rel属性来操作:
# 如果我们操作的关系为App1(id=1的App)相关的,则先获得App1的对象。后面使用该对象的所有操作,都是与App1有关的。 obj = models.Application.objects.get(id=1) print(obj.appname) # 打印id=1的App名称 # obj.rel就是操作关系表的桥梁,或者说这个对象就是关系表 # 添加一个App1---HostA的关系,add()中的参数1表示nid=1的Host,即HostA obj.rel.add(1) # 添加 App1---HostB的关系 obj.rel.add(2) # 同时添加三条关系,App1---HostB、C、D obj.rel.add(2, 3, 4) # 同时添加四条关系,App1---HostA、C、D、E obj.rel.add(*[1, 3, 4, 5]) # 删除App1---HostA关系 obj.rel.remove(1) # 删除App1---HostB、C、D三条关系 obj.rel.remove(2, 3, 4) # 删除App1---HostB、C、D三条关系 obj.rel.remove(*[2, 3, 4]) # 清除所有App1相关的关系 obj.rel.clear() # 设置App1对应的所有关系,即关系表中关于App1相关的关系,只保留App1---HostA、B、D、E,其他全部删除 obj.rel.set([1, 2, 4, 5]) # 获取关系表中与App1相关的所有条目 app1_r_list = obj.rel.all() # 这里使用all()拿到的QuerySet列表中的元素为Host对象,因为这里App1是固定的,他关系的所有元素都是Host for row in app1_r_list: print(row.hostname) # 打印所有与App1关联的主机