在建立一个最简单的对单表进行增删改查并且输出的应用之后,Django基础的架构已经知道了.现在进行一些更复杂的通过外键的多表查询以及多对多等技巧.通过建立一个图书管理系统来学习.
设计表和建立数据表
数据库的设计是非常重要的,这里先设计图书与出版社对应的关系,由于在版权周期内,一本书只对应一个出版社,一个出版社可以出版很多书,所以设计表格如下:
Publisher 出版社表 |
id |
name |
主键id |
出版社名称 |
Book 书表 |
id |
title |
publiser_id |
主键id |
书名 |
书的出版社名称-外键连到出版社表主键 |
设计出表格之后,新建一个APP叫做book,在其models.py中编写ORM代码如下:
from django.db import models
class Publisher(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=64, null=False, unique=True)
class Book(models.Model):
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=64, null=False, unique=True)
# 创建外键
publisher = models.ForeignKey(to="Publisher")
这里有两个地方要注意:
- 一是编写外键的to属性的值,在这个例子里,Book类建立在Publisher类之后,所以可以直接写成to=Publisher,但是不推荐这么做,最好写成类的名字的字符串.
- 二是publisher在表内会被改写成publisher_id字段名,涉及到外键,会自动在后边加_id,所以表的实际字段名称是publisher_id
建立模板和视图
之前的项目是建立了单表的增删改查,对于单独修改出版社和单独修改书籍来说都不成问题.连表做成一行其实也没有问题,只要注意取到的是按照书来取得id就可以顺利删除.这次的页面和选择要做的更加复杂.
对于publisher这张表,就和之前做的增删改查是一样的,不再赘述.现在要做的,是Book表的增删改查.这里由于每一本书和出版社有对应关系,我们假定出版社那张表在添加书之前是必须先被添加出版社才行,不能够同时添加出版社和书,只能够在给定的出版社内选择.(其实如果给定一个表单同时输入书名和出版社的话,后台需要做判断,首先判断出版社是否存在,再判断书是否存在,然后先添加出版社再添加书.可以在option的最后一个弄一个固定值表示新的出版社用于添加.现在还没有用上JS,以后用JS可以比较方便的使用.)
展示书籍的页面和函数:
def books(request):
book_list = models.Book.objects.all()
return render(request, 'books.html', {"book_list": book_list})
<div class="container">
<h1>书籍列表</h1>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>序号</th>
<th>书籍id</th>
<th>书籍名称</th>
<th>出版社</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for book in book_list %}
<tr>
<th>{{ forloop.counter }}</th>
<th>{{ book.id }}</th>
<th>{{ book.title }}</th>
<th>{{ book.publisher.name }}</th>
<th><a href="/edit_book/?id={{ book.id }}" class="btn btn-primary">修改</a>
<a href="/del_book/?id={{ book.id }}" class="btn btn-danger">删除</a>
</th>
</tr>
{% endfor %}
</tbody>
</table>
<a href="/add_book/" class="btn btn-primary">增加书籍</a>
</div>
这里的关键点是外键查询的写法:要显示书籍对应的出版社,直接调用Book对象的publisher属性,可以得到通过外键关联的出版社表的行对象,再用name取出出版社名字即可.
增加书籍的函数和页面
和原来相比主要的变化就是需要增加一个下拉菜单供选择出版社.
def add_book(request):
if request.method == "POST":
book_name = request.POST.get("title", None)
pub_id = request.POST.get("publisher", None)
models.Book.objects.create(title=book_name, publisher_id=pub_id).save()
return redirect("/books/")
pub_list = models.Publisher.objects.all()
return render(request, 'add_book.html', {"publisher_list": pub_list})
页面里增加一组SELECT-OPTION用于读入出版社的数据.每个option的value用出版社的id来赋值,返回的时候即可取得书籍的名称和对应出版社的id
<div class="container">
<h3>请输入书籍名称</h3>
<form action="/add_book/" method="post">
<div class="form-group">
<label for="bookname">书籍名称</label>
<input type="text" id="bookname" class="for" name="title">
</div>
<div class="form-group">
<label for="publisher_list">选择出版社</label>
<select name="publisher" id="publisher_list">
{% for publisher in publisher_list %}
<option value="{{ publisher.id }}">{{ publisher.name }}</option>
{% endfor %}
</select>
</div>
<button class="btn btn-primary" type="submit">提交</button>
</form>
</div>
修改书籍的函数和页面
这个页面非常类似于增加的页面,不同点是需要将书名默认填写在框中,另外判断一下书名不能为空,还需要采用一个隐藏的表单元素来存放书籍ID.从books页面里的a标签用GET请求附加书籍id数据的方式,然后返回一个表单页面供提交.这里还一个要注意的就是,应该让书对应的出版社默认是选中状态,以展示其原来的内容.这里要增加一些处理.
def edit_book(request):
if request.method == "GET":
book_id = request.GET.get("id", None)
if book_id:
edit_obj = models.Book.objects.get(id=book_id)
pub_id = edit_obj.publisher_id
pub_list = models.Publisher.objects.all()
return render(request, 'edit_book.html',
{"publisher_list": pub_list, "default": edit_obj.title, "book_id": book_id,"pub_id":pub_id})
else:
return redirect("/books/")
else:
book_name = request.POST.get("title", None)
book_id = request.POST.get("book_id", None)
pub_id = request.POST.get("publisher", None)
edit_obj = models.Book.objects.get(id=book_id)
edit_obj.title = book_name
edit_obj.publisher_id = pub_id
edit_obj.save()
return redirect("/books/")
页面中需要添加一个隐藏的input元素用来接受当前的书籍id,以供返回表单时候获取需要修改的书籍id.
<div class="container">
<h3>请修改书籍名称</h3>
<form action="/edit_book/" method="post">
<div class="form-group">
<label for="bookname">书籍名称</label>
<input type="text" id="bookname" class="for" name="title" value="{{ default }}">
</div>
<div class="form-group">
<label for="publisher_list">选择出版社</label>
<select name="publisher" id="publisher_list">
{% for publisher in publisher_list %}
{% if publisher.id == pub_id %}
<option value="{{ publisher.id }}" selected>{{ publisher.name }}</option>
{% else %}
<option value="{{ publisher.id }}">{{ publisher.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<input type="hidden" value = "{{ book_id }}" name="book_id">
<button class="btn btn-primary" type="submit">提交</button>
</form>
</div>
这里用到了模板语言的判断语句.由于要把原本的书名和出版社名展示给用户,所以除了传送通过书id得到的书名以外,还必须传送书表内的publisher_id给页面,然后用模板语言的if -else -endif语句来做判断.在列出所有出版社的时候,如果出版社的id与选中的书的外键id相同,则该选项加上selected,否则就不加,这样就得到了将原来的书名和出版社作为默认修改内容的方法.
删除书籍的函数和页面
删除书籍如果直接用GET请求返回id是可以的,但是这样可以通过URL直接操作,存在一定风险,做法是返回一个固定内容的表单,等待用户再次确认.只需要返回书籍ID即可.
def del_book(request):
if request.method == "GET":
book_id = request.GET.get('id', None)
book_obj = models.Book.objects.get(id=book_id)
return render(request, 'del_book.html',
{"default": book_obj.title, "pub_name": book_obj.publisher.name, "book_id": book_id})
else:
book_id = request.POST.get('book_id', None)
del_obj = models.Book.objects.get(id=book_id)
del_obj.delete()
return redirect("/books/")
页面上改成不可修改的输入框,用于提示用户,增加一个返回按钮.
<div class="container">
<h3>请确认需要删除的书籍名称</h3>
<form action="/del_book/" method="post">
<div class="form-group">
<label for="bookname">书籍名称</label>
<input type="text" id="bookname" class="for" name="title" value="{{ default }}" disabled>
</div>
<div class="form-group">
<label for="publisher_list">出版社名称</label>
<input type="text" name="pub_name" id="publisher_list" value="{{ pub_name }}" disabled>
</div>
<input type="hidden" value = "{{ book_id }}" name="book_id">
<button class="btn btn-danger" type="submit">确认</button>
<a href="/books/" class="btn btn-primary">取消</a>
</form>
</div>
增删改查系统主要是采用了外键一对多的方式操作数据库,以及Django的模板语言中的if条件判断.
增加作者以及多对多关系
现在我们给这个系统再增加一张作者表.由于一本书可以有多个作者,一个作者也可以写多个书,因此作者和书之间是一个多对多的关系.
在原来直接操作MySQL的时候,需要增加一张作者表,然后用一个两个外键的表存放书和作者的多对多关系.但是在Django的ORM里,需要先创建作者表,然后用一个特殊的方式构建多对多关系:
class Author(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=32,null=False,unique=True)
# 创建简单的多对多的需求
book = models.ManyToManyField(to="Book")
这里的多对多指的是书和作者是多对多关系,由于我是作者表,所以我想要拿书,就把book变量的值设置为特殊的与book表的多对多关系,执行makemigrations和migrate之后,会发现数据库里多了两张表,一张是这个author表,另外一张是author_book表,就是通过多对多关系创建的新表,每一行分别外链到两张表.这恰好就是所需要的.然后给两张表随便添加一些数据,之后准备编写CRUD
展示作者以及所有的书
展示作者的基本逻辑比较简单,如果想要展示作者所有的书,放在同一个格子里,用一些特殊符号隔开,来看一下如何实现:
def author_list(request):
all_author = models.Author.objects.all()
return render(request, "author_list.html", {"author_list": all_author})
函数比较简单,返回了所有作者的清单,后边的部分主要体现在模板语言里
<div class="container">
<h1>作者列表</h1>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>#</th>
<th>id</th>
<th>姓名</th>
<th>著作</th>
</tr>
</thead>
<tbody>
{% for author in author_list %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ author.id }}</td>
<td>{{ author.name }}</td>
<td>{% for book in author.book.all %}
{% if forloop.last %}
<span>{{ book.title }}</span>
{% else %}
<span>{{ book.title }} | </span>
{% endif %}
{% endfor %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
这里的关键在于第四列的数据填充设置.
首先是多对多的取值问题:这里通过author.book.all 取到了作者对应的所有书.这是模板语言,而在python里,实际上是author.book.all()来得到对应的所有数据.
之后,想用管道符分割各个书名而显示在同一行内,这里用了判断forloop.last来判断是否为队列末尾,如果是则插入不带管道符的内联元素,如果不是,则插入带管道符的内联元素.这样就实现了需求.
这里的关键就是models里如何定义多对多关系,以及在python和模板语言内如何使用多对多关系(具体到每一个元素来说其实还是一对多的关系)
添加作者
这里如果单独向作者表内添加作者,其实比较简单,目前做的是,添加作者的时候同时与书进行关联,则在添加作者的页面上,还必须列出所有的书供选择,好产生对应关系.这里的关键是获取select返回的值并且添加多对多关系.
def add_author(request):
if request.method == "POST":
author_name = request.POST.get("author_name", None)
# 拿checkbox和多选selelct的时候要用getlist
books = request.POST.getlist("books")
new_author_obj = models.Author.objects.create(name=author_name)
# 直接用book.set,传入id列表来设置对应关系.
new_author_obj.book.set(books)
return redirect("/author_list/")
book_list = models.Book.objects.all()
return render(request, "add_author.html", {"book_list": book_list})
处理函数的关键有两步,一是对于取得多个值的表单元素如checkbox和select多选,要用getlist拿到所有值构成的一个列表.二是通过book属性的set方法,一次性将列表设置进多对多的表格里,无需再编写函数去一行一行追加.HTML页面的代码主要是select的option里要带上书籍的id供选择:
<div class="container">
<h1>添加作者</h1>
<form action="/add_author/" method="post">
<div class="form-group">
<label for="add">输入作者姓名:</label>
<input type="text" id="add" name="author_name">
</div>
<div class="form-group">
<select name="books" id="books" multiple="multiple" size="5">
{% for book in book_list %}
<option value="{{ book.id }}">{{ book.title }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
</div>
删除作者
删除作者主要是说明.delete()方法会做两个事情,一是先删除多对多表格里的书与作者关系,二是从作者表内删除作者,也就是无需编写额外代码再去操作多对多的表格.比较简单,就直接用GET请求写了:
def delete_author(request):
if request.method == "GET":
delete_id = request.GET.get("id")
models.Author.objects.get(id=delete_id).delete()
return redirect("/author_list/")
HTML页面像原来一样,在最后一列增加一个删除按钮.这里就一起把编辑按钮也加上去了
<div class="container">
<h1>作者列表</h1>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>#</th>
<th>id</th>
<th>姓名</th>
<th>著作</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for author in author_list %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ author.id }}</td>
<td>{{ author.name }}</td>
<td>{% for book in author.book.all %}
{% if forloop.last %}
<span>{{ book.title }}</span>
{% else %}
<span>{{ book.title }} | </span>
{% endif %}
{% endfor %}</td>
<td>
<a href="/delete_author/?id={{ author.id }}" class="btn btn-danger">删除</a>
<a href="/edit_author/?id={{ author.id }}" class="btn btn-primary">编辑</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="/add_author/" class="btn btn-primary">增加作者</a>
</div>
编辑作者
编辑作者的思路与增加是类似的,都需要拿到原来的设置,然后显示为默认设置,再让用户修改,之后再提交表单.又需要埋一个隐藏的input标签用于存放当前作者的id.还需要通过一个列表拿到该作者所有的book对应的id,列出所有的书籍,然后判断,当书的id在这个作者所写的书的范围内,则默认选中该选项.
其他设置新名称等都和增加很类似.
def edit_author(request):
if request.method == "GET":
author_id = request.GET.get("id")
author_obj = models.Author.objects.get(id=author_id)
# 该作者所写的书的id列表,用列表推导式获得
book_author = [x.id for x in author_obj.book.all()]
book_list = models.Book.objects.all()
return render(request, 'edit_author.html',
{"author_name": author_obj.name, "book_list": book_list, "book_author": book_author,
"author_id": author_id})
else:
author_id = request.POST.get('author_id')
author_new_name = request.POST.get("author_name")
books = request.POST.getlist("books")
author_obj = models.Author.objects.get(id=author_id)
author_obj.name = author_new_name
author_obj.book.set(books)
author_obj.save()
return redirect("/author_list/")
HTML页面内,需要用到新的模板语言 in:
<div class="container">
<h1>添加作者</h1>
<form action="/edit_author/" method="post">
<div class="form-group">
<label for="add">输入作者姓名:</label>
<input type="text" id="add" name="author_name" value="{{ author_name }}">
</div>
<div class="form-group">
<select name="books" id="books" multiple="multiple" size="5">
{% for book in book_list %}
{% if book.id in book_author %}
<option value="{{ book.id }}" selected="selected">{{ book.title }}</option>
{% else %}
<option value="{{ book.id }}">{{ book.title }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<input type="hidden" name="author_id" value = "{{ author_id }}">
<button type="submit" class="btn btn-primary">提交</button>
</form>
</div>
HTML里用了 {% if book.id in book_author %} 来表示,当书属于该作者所写,则默认选中该项,否则不选中.
这个图书管理系统的逻辑是从最小约束开始,先添加出版社,再添加出版社所属的书,最后添加作者与书的关系.在设计数据系统的时候,也要从最基础的约束条件也就是各个基础单表数据出发,然后再添加一对多的数据,最后添加多对多的数据.
至此,已经编写了两个比较小的项目,用到了比较基础的方法,包括Django基础配置,单表,一对多,多对多增删改查,单个表单元素和多选元素的提交和页面内使用,以及循环,判断的模板语言.对Django有了比较初步的印象,之后是对Django的每个模块进行更加深入的学习.