Django学习记录
文章目录
这篇博客为个人学习记录,不定期更新
另外,博客是博主使用Markdown语法写完后直接导入的,可能会存在外链图片失效、章节重复等问题,为节约时间,博主不打算处理,仅作为个人查询技术细节使用
Django应用及分布式路由
创建新的应用
-
创建方法:终端输入
python manage.py startapp news
,其中news是新的应用名 -
注册创建的APP,在项目的配置文件中(settings.py)找到INSTALLED_APPS列表,并在该列表中添加项目名,如下:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'music', 'sport', 'news', ]
最后的news即为新添加的应用
分布式路由
创建分布式路由的目的是为了让单个应用有单个应用的专属路由,不用一直在项目主路由中添加地址,正常的逻辑是创建了新的应用后,应该在项目主路由中为该应用配置专属的路由,而这个新建应用的路由则在自己的路由文件中去添加
-
在项目路由中添加新的应用路由地址,如下:
urlpatterns = [ path('admin/', admin.site.urls), path('img', views.test_static), path('music/', include('music.urls')), path('sport/', include('sport.urls')), path('news/', include('news.urls')), ]
列表的最后即为news应用的专属路由,而使用include方法,表明以后news的路由应该到news自己的urls.py文件中去添加
-
在新建应用下,创建一个urls.py来添加本应用的路由地址和对应的视图函数,如下:
from django.urls import path from news import views urlpatterns = [ path('index', views.index_view), ]
index是news应用的地址,这种情况下,访问域名
127.0.0.1:8000/news/index
,浏览器返回的就是视图函数views中index_view方法所写的内容
模型层及ORM介绍
模型:
是Python的一个类,要求必须继承自django.db.models.Model, 一个模型类代表数据库中的一张数据表,模型类的每个熟悉代表数据库的一个字段,模型是数据增删改查的接口,用来操作数据库
ORM框架:
对象关系映射,允许使用类和对象对数据库进行操作,避免通过SQL语句操作数据库
终端进入MySQL
可以从cmd终端进入,也可以从编译器终端进入,指令都一样
mysql -uroot -p
接下来在终端输入密码
退出在终端输入quit即可
数据库模型文件
通过models.py生成数据表,如下
from django.db import models
# Create your models here.
class Book(models.Model):
title = models.CharField("书名", max_length=50, default='')
price = models.DecimalField("定价", max_digits=7, decimal_places=2, default=0.0)
通过上面这个文件,执行后应该会在数据库中生成一张Book数据表,这张表有两个字段,分别是title和price,但仅写完这个文件是不够的,需要在终端执行如下命名:
- 生成迁移文件,终端执行
python manage.py makemigrations
,将应用下的models.py文件生成一个中间件,并保存在migration文件夹中 - 执行迁移脚本程序,终端执行
python manage.py migrate
,将每个应用下的migration目录中的中间文件同步回数据库
ORM基础字段及选项
字段类型
- BooleanField()布尔型
- CharField()字符型
- DateField()日期,参数:
- auto_now:每次更新对象时,自动设置该字段为当前时间(取值True/False)
- auto_now_add:对象第一次创建时自动设置当前时间(取值True/False)
- default:设置当前时间
- DateTimeField()日期时间,高频使用,比上面那个更频繁使用,基本创建表就会使用,一般会建两个字段,创建时间和修改时间,参数与DateField()相同
- FloatField()浮点型
- DecimalField()小数
- EmailField(),数据库类型:varchar,Django封装好的
- IntegerField()
- ImageField()图片的存储路径,字符串
- TextField() 类型longtext
- ……
字段选项
- 主键primary_key
- blank,设置为True时,可以为空,设置为false时,字段必须填写
- null,设置为True表示允许为空,默认为False
- default,设置所在列的默认值,如果null=False建议添加此项
- db_index,索引
- unique, 唯一索引
- db_column,指定字段名,一般不用给
- verbose_name,admin上的显示名称
模型类Meta
使用内部类给模型赋予属性,比如修改表名
class Meta:
db_table = 'book'
ORM基本操作-创建数据
models.Model模型类中的objects对象被继承,增删改查都是通过这个objects对象来实现
终端进入Django shell的命令:
python manage.py shell
第一种方法:
MyModel.models.Model.objects.create()
第二种方法:
创建模型对象实例,调用save()保存
obj = MyModel(attr1=value1, attr2=value2....)
obj.attr = value
obj.save()
ORM查询
-
objects.all() == select * from tabel,返回QuerySet容器对象,内部存放模型实例
一般情况下,为了更好的显示查询结果,都要重新____str____函数,这是为了设置模型的返回值
以下是查询示例
from bookstore.models import Book al = Book.models.all()
重写str函数
def __str__(self): return '%s_%s_%s_%s' % (self.title, self.pub, self.price, self.market_price)
查询结果
<QuerySet [<Book: python_清华大学出版社_20.00_30.00>, <Book: Django_清华大学出版社_70.00_75.00>, <Book: JQuery_机械工业出版社_90.00_85.00>, <Book: Linux_机械工业出版社_80.00_65 .00>, <Book: HTML5_清华大学出版社_90.00_105.00>]>
-
objects.values(‘列1’, ‘列2’),查询部分列数据,返回QuerySet字典列表,等同于
select lie1, lie2 from Book
-
objects.values_list(‘列1’, ‘列2’),查询部分列数据,返回QuerySet元组列表
-
order_by()查询结果排序
ORM综合练习
目的:将数据库中,所有的book信息显示在all_book页面上,最终结果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I3lMPOyo-1652317066151)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220429095725708.png)]
-
创建分布式路由:在主项目的urls.py文件中添加bookstore应用的路由,如下
from django.contrib import admin from django.urls import path, include from mysite import views urlpatterns = [ path('admin/', admin.site.urls), path('img', views.test_static), path('music/', include('music.urls')), path('sport/', include('sport.urls')), path('news/', include('news.urls')), path('bookstore/', include('bookstore.urls')), ]
最后一行即为bookstore应用的分布式路由
-
在bookstore应用中创建该应用的路由,在应用目录下新建一个urls.py,填入一下路由信息
from django.urls import path from bookstore import views urlpatterns = [ path('all_book', views.all_book), ]
也就是当我访问
http://127.0.0.1:8000/bookstore/all_book
时指向的文件和视图函数 -
视图函数,在views.py文件中添加查询和渲染网页的代码
from django.shortcuts import render from .models import Book # Create your views here. def all_book(request): all_book = Book.objects.all() return render(request, 'bookstore/all_book.html', locals())
使用all()方法查询到所有的数据,并把查到的all_book传递给模板文件夹(templates)下bookstore文件夹中的all_book.html文件,locals()函数将查询到的局部变量转换成字典数据,也就是把all_book转换成字典,讲真,我真没理解这个locals到底怎么用的
-
模板文件,在模板中填写网页展示的内容,并接受视图函数传递过来的数据,并进行展示
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>查看所有表格</title> <style> td, th { border: 1px solid; } .mt {border-collapse: collapse;} </style> </head> <body> <table class="mt"> <tr> <th>id</th> <th>title</th> <th>pub</th> <th>price</th> <th>market_price</th> <th>op</th> </tr> {% for book in all_book %} <tr> <td>{{ book.id }}</td> <td>{{ book.title }}</td> <td>{{ book.pub }}</td> <td>{{ book.price }}</td> <td>{{ book.market_price }}</td> <td> <a href="#" >更新</a> <a href="#">删除</a> </td> </tr> {% endfor %} <tr></tr> </table> </body> </html>
这里关键是要理解那一段for循环的写法
不写CSS样式显示出来的表格很丑,没有边框,我在样式中改了一下边框,在两个地方
小结1
其实写到这里,Django的作用和整个操作流程,已经大致显示出来了,有必要做个小结:
- 首先启动一个Django项目——mysite
- 在Django项目中新建一个应用——bookstore
- 为项目(mysite)创建分布式路由,在路由中添加bookstore的分布式路由
- 在应用(bookstore)中添加路由,指定地址指向的视图函数
- 在应用的视图函数中添加方法,如果需要向数据库传递数据,就要启动数据库模型
- 在应用的model.py文件中添加模型,注意,一个模型就是一张表,可以在这个表中进行增删改查操作
- 如果有必要,重写一下默认的str方法
- 视图函数中对模型数据进行处理,可以查询、排序等操作,并返回查询的值
- 在应用中创建模板文件(html文件),需要在应用下新建一个templates文件夹,最好是在templates文件夹下再创建一个与应用同名的文件夹(防止路由找不到模板文件)用来存放模板文件,在模板文件下编写页面文件,并接收视图函数传递过来的数据,使用Django特有的语法处理接收到的数据并写入到模板文件中
- 启动项目,访问地址,即可展示页面
条件查询
普通查询
-
filter()方法,返回QuerySet
MyModel.objects.filter(attr1=value1, attr2=valur2…)
多个属性在一起时为“与”关系
-
exclude(条件),不包含此条件的数据集
-
get(条件),返回单条数据,如果给定的条件超过一条,或者没有数据,都会报错
查询谓词
查询示例:
Book.objects.filter(id__exact=1)
等同于
select * from Book where id = 1
由字段名__谓词组成
- __exact等值匹配,经常用来匹配是否为空,is null
- __contains包含指定值查询,**经常使用,**类似于sql中的like ‘%xxx%’
- __startwith以XXX开始
- __endwith以XXX结尾
- __gt大于指定值,常用
- __gte大于等于指定值,常用
- __lt小于指定值,常用
- __lte小于等于指定值,常用
- __in查找的数据是否在指定范围内,常用,示例
Book.objects.filter(id__in = [1,3,5])
- __range指定区间范围内查询,和sql中的between一样,常用,示例
Book.objects.filter(id__range = (20,40))
- 还有很多…
ORM更新操作
流程:查(使用get)->改->存(使用save)
后端更改操作
单条数据更新的具体示例:
# 单条数据更新
b1 = Book.objects.get(id=1)
b1.price = 22
b1.save()
批量更新,直接将查询结果使用update方法更新
# 批量更新
b1 = Book.objects.filter(price__gt=50)
b1.update(market_price=100)
将所有定价大于50的书市场价均设置为100
通过前端超链接页面更新
前面的图书管理界面上,最后一列“op”绑定了两个空链接,现在先来实现更新功能
一步步来
添加视图函数
views.py中添加的内容
def update_book(request, book_id):
try:
book = Book.objects.get(id=book_id)
except Exception as e:
print('--update book error is %s' % (e))
return HttpResponse("书本不存在")
if request.method == 'GET':
return render(request, 'bookstore/update_book.html', locals())
elif request.method == 'POST':
price = request.POST['price']
market_price = request.POST['market_price']
book.price = price
book.market_price = market_price
book.save()
return HttpResponseRedirect('/bookstore/all_book')
这个视图函数还挺复杂的,记录一下逻辑
既然是要更改数据,就说明要像数据库传递数据,那么一般是post请求,但是超链接是get请求,所以实际上要对请求进行判断
-
首先,如果是get请求,就跳转到路由指定的模板文件上,并通过local()将get到的局部变量打包成字典,传递给模板文件update_book.html,模板文件见下面小节(添加模板文件)
-
如果是post请求,也就是说要通过页面给后台传递参数了,那么把要实现的功能在这里写好,也就是在这里实现数据的更新
-
查数据
-
改数据
-
存数据
-
返回HttpResponse实体,代码中是重定向,指定了路由的相对路径,绝对路径应该是http://127.0.0.1:8000/bookstore/all_book
强调一下,这里的return必须写,至于是render、HttpResponse还是HttpResponseRedirect根据业务需求,不然项目不知道下一步该往哪里去
-
-
逻辑上已经没什么问题,但有一点要注意,查询book是用的get方法,get方法具有唯一性,极易报错,所以有必要进行异常检测
-
视图函数中我传递了一个book_id参数,这点非常重要。首先这个参数是我们要查询的书名,其次,路由中也要用到,下面会讲到
修改模板文件all_book.html
实际上就是修改一下表格中“更新”的超链接地址,我上一下完整的内容,后面与此类似,就不上完整的内容了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>查看所有表格</title>
<style>
{#为表格增加边框#}
td, th {
border: 1px solid;
}
{#删掉表格单元格之间的空白#}
.mt {border-collapse: collapse;}
</style>
</head>
<body>
<table class="mt">
<tr>
<th>id</th>
<th>title</th>
<th>pub</th>
<th>price</th>
<th>market_price</th>
<th>op</th>
</tr>
{% for book in all_book %}
<tr>
<td>{{ book.id }}</td>
<td>{{ book.title }}</td>
<td>{{ book.pub }}</td>
<td>{{ book.price }}</td>
<td>{{ book.market_price }}</td>
<td>
<a href="/bookstore/update_book/{{ book.id }}" >更新</a>
<a href="#">删除</a>
</td>
</tr>
{% endfor %}
<tr></tr>
</table>
</body>
</html>
添加模板文件update_book.html
视图函数完成后,就要进行前端页面的设计了,前面的逻辑是点击超链接(get请求)的话,就去渲染网页文件,并向网页传递book参数
update_book.html里的内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>更改书籍</title>
</head>
<body>
<form action="/bookstore/update_book/{{ book.id }}" method="post">
<p>
title <input type="text" value="{{ book.title }}" disabled="disabled" />
</p>
<p>
publish <input type="text" value="{{ book.pub }}" disabled="disabled" />
</p>
<p>
price <input type="text" value="{{ book.price }}" name="price" />
</p>
<p>
market_price <input type="text" value="{{ book.market_price }}" name="market_price" />
</p>
<p>
<input type="submit" value="更新" />
</p>
</form>
</body>
</html>
看看界面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DqjbC8Yo-1652317066152)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220429142029973.png)]
界面还有待优化,这是CSS的事情,暂时不进行美化
很明显是表单数据,因此使用form标签,form的action参数是表单数据请求指向的地址,也就是路由,这个路由的写法也是用的Django语法
注意get请求向这个网页文件传递了book参数,也就是通过book_id获取到的book信息,因此book.id就是数据库中的id字段,title,pub等等也是,4个input标签中各填入了book的4个参数,通过value匹配,用的也是Django的语法,其中title,pub是不可修改的(“disabled” )
最后一个input标签是提交按钮,也就是点击后发送post请求到数据库,并重定向到视图函数中写好的地址
添加路由
目前数据库中是有5本书的,也就是说,如果按绝对路径来定义路由的话,要定义5个路由,分别是
path('update_book/1', views.update_book),
path('update_book/2', views.update_book),
path('update_book/3', views.update_book),
path('update_book/4', views.update_book),
path('update_book/5', views.update_book),
这么写明显不合理,如果有100个,是不是要写100行
这里有多种替代方式
- 通过正则表达式来传递
- 通过Django的path转换器来传递
这里使用的是path转换器,用path转换器记得在视图函数中传递参数,这点非常重要,前面就已经提到了,视图函数中传递了一个book_id这个参数,视图函数通过这个book_id进行了一系列的操作,使用path路由的时候也要用到,正确的写法如下
urls.py中的内容
from django.urls import path
from bookstore import views
urlpatterns = [
path('all_book', views.all_book),
path('update_book/<int:book_id>', views.update_book),
]
添加了一个path('update_book/<int:book_id>', views.update_book)
path转换器的精髓就在这里,接收视图函数传递的形参,使用<>来转换,前面是这个数据的类型,后面是参数,有点像java中的泛型
小结2
这里算是完成了一个相对初学者来说,比较复杂的功能,涉及到的技术栈有:
- 数据库的更新,包括查-改-存
- 视图函数的创建,包括两种不同的请求,重定向等
- 如何像模板文件传递参数
- 如何通过path转换器指定路由
ORM删除操作
单条删除:先get(),然后delete()
批量删除:filter()查找,然后delete()删除
伪删除:在表中添加字段is_active,布尔类型,做伪删除时,把值设为false,显示的时候只显示is_active=True的数据,这样就实现了伪删除操作
删除实例
ORM查询中,表格中还有个删除功能没实现,现在补充一下
过程如下
在模型中添加伪删除字段
class Book(models.Model):
def __str__(self):
return '%s_%s_%s_%s' % (self.title, self.pub, self.price, self.market_price)
title = models.CharField("书名", max_length=50, default='', unique=True)
pub = models.CharField("出版社", max_length=100, default='')
price = models.DecimalField("定价", max_digits=7, decimal_places=2, default=0.0)
market_price = models.DecimalField("零售价", max_digits=7, decimal_places=2, default=0.0)
# info = models.CharField("描述", max_length=100, default='')
is_active = models.BooleanField('是否活跃', default=True)
添加的是最后一行,后面删除实际上是将这个字段设置为false
视图函数
在views.py中添加以下函数:
def delete_book(request):
book_id = request.GET.get('book_id')
if not book_id:
return HttpResponse('---book_id不存在')
try:
book = Book.objects.get(id=book_id, is_active=True)
except Exception as e:
print('delete book get error %s' % e)
# print('--update book error is %s' % (e))
return HttpResponse("书本不存在")
book.is_active = False
book.save()
return HttpResponseRedirect('/bookstore/all_book')
注意,这里在函数中没有传形参book_id了,说明路由配置不是之前的那种了,这里跟着老师使用了第二种路由配置方式**,就是通过request.GET.get()方法获取请求中的参数,注意,get方法是寻找名为name(本例中为‘book_id’)的GET参数,而且如果参数没有提交,返回一个空的字符串,如果我点击第5条图书,那么请求的链接应该是http://127.0.0.1:8000/bookstore/delte_book?id=5
,也就是说,我的book_id=5**,这一点的理解很重要,不然会陷入一个理解的死循环,就像月读一样,为什么请求链接是这样的呢,那就要看我的模板文件(all_book.html)了。
模板文件修改
这里的模板文件只涉及all_book.html,因为删除后直接重定向到这个文件了,所以对比上面的更新要简单一点,在模板文件中修改成下面的内容
<td>
<a href="/bookstore/update_book/{{ book.id }}" >更新</a>
<a href="/bookstore/delete_book?book_id={{ book.id }}" onclick="return confirm('是否确认删除');">删除</a>
</td>
回到上面的问题,我增加了一个传递参数的符号?,表明后面的book_id是要传的参数,其实上面get()方法得到的参数就传到了这里,也就是超链接应该是bookstore/delete_book?book_id=5
然后再回到视图函数,后面是删除操作,说是删除,其实不如理解为修改,因为执行的是伪删除操作,注意修改后需要save一下
路由添加
注意这里的路由不再是用path转换器了,而是直接传递参数,写法如下
from django.urls import path
from bookstore import views
urlpatterns = [
path('all_book', views.all_book),
path('update_book/<int:book_id>', views.update_book),
path('delete_book', views.delete_book),
]
最后一行就是添加的路由,后面通过?传参,理解上相对第一种更麻烦一点,我推荐使用第一种
F对象和Q对象
F对象
先上一下数据库中book表
id | title | price | market_price | pub | is_active |
---|---|---|---|---|---|
1 | python | 25.00 | 50.00 | 清华大学出版社 | 1 |
2 | Django | 70.00 | 75.00 | 清华大学出版社 | 1 |
3 | JQuery | 90.00 | 85.00 | 机械工业出版社 | 1 |
4 | Linux | 80.00 | 65.00 | 机械工业出版社 | 1 |
5 | HTML5 | 90.00 | 105.00 | 清华大学出版社 | 1 |
F对象代表数据库中某条记录的字段的信息,是对数据库中的字段值在不获取的情况下进行操作,可以解决并发的问题,类似于给数据库加锁,把并发事件转为串联事件,第二个作用的用于字段之间的比较,比如比较售价大于定价的书籍,写法如下:
Book.objects.filter(market_price__gt=F('prive'))
Q对象
使用复杂的逻辑或、逻辑非等操作,可以灵活组合
语法
Book.objects.filter(Q(price__lt=20)|Q(pub="清华大学出版社"))
聚合查询和原生数据库操作
聚合查询
普通聚合
聚合函数: Sum Avg Count Max Min
语法:Book.objects.aggregate(结果变量名=Sum(列))
结果变量名就是sql中的查询结果别名
示例:
Book.objects.aggregate(res=Count('id'))
返回:
{res: 4}
分组聚合
语法:QuerySet.annotate(结果变量名=Sum(列))
sql中的group by
示例——统计每个出版社有多少本书:
pub_set = Book.objects.values('pub')
pub_set_count = put_set.annotate(mycount=Count('pub'))
原生数据库操作
raw
语法:Book.objects.raw(sql语句, 拼接参数)
个人对基础的sql语句是比较熟的,因此个人喜欢用原生的数据库操作,按理说也可以用pandas操作
但是原生的有很多缺陷和漏洞,SQL注入
解决方法:把参数放入拼接参数中
cursor 游标
语法:
from django.db import connection
with connection.cursor() as cur:
cur.execute('sql语句', '拼接参数')
还是推荐使用ORM
admin后台管理系统
创建超级用户指令
python manage.py createsuperuser
这里需要设置用户名、邮箱和密码
进入用户管理系统地址
http://127.0.0.1:8000/admin
根据上面设置的用户名密码登录,可以进入Django的后台管理系统
注册自定义的模型类
目前模型文件(models.py)文件中有Book和Author两个类,也就是数据库中的两张表,如果要自己定义的模型类也能在/admin后台管理系统中显示和管理,需要将自己的类注册到后台管理界面
方法步骤:
- 在应用的admin.py中导入要注册的模型类,
from .models import Book
- 调用admin.site.register方法进行注册,如
admin.site.register(Book)
模型管理器类
可以为后台管理界面添加便于操作的新功能,继承自django.contrib.admin中的ModelAdmin类
什么意思,就是我们自建的模型展现形式很丑,而自带的展示起来更加美观
使用方法:
-
在应用的admin.py里进行定义,
class XXXManager(admin.ModelAdmin)
-
绑定注册模型管理器和模型类
from django.contrib import admin
from .models import *
# Register your models here.
class BookManager(admin.ModelAdmin):
list_display = ['id', 'title', 'pub', 'price', 'market_price', 'is_active']
admin.site.register(Book, BookManager)
list_display参数是将数据以表格的形式展示是管理后台上,列表中有几个数据,就展示多少个列
除了list_display参数,还有以下几个常用的参数:
- list_display_link = [‘title’],修改页面的超链接,默认在ID列上
- list_filter = [‘pub’] 添加过滤器
- search_fields = [‘title’] 添加搜索框 模糊查询
- list_editable = [‘price’, ‘market_price’] 添加可在列表页编辑的字段
Meta类修改属性
模型(models.py)中添加Meta类,见前文**ORM基础字段及选项中**的模型类Meta
示例:
class Book(models.Model):
def __str__(self):
return '%s_%s_%s_%s_%s' % (self.title, self.pub, self.price, self.market_price, self.is_active)
title = models.CharField("书名", max_length=50, default='', unique=True)
pub = models.CharField("出版社", max_length=100, default='')
price = models.DecimalField("定价", max_digits=7, decimal_places=2, default=0.0)
market_price = models.DecimalField("零售价", max_digits=7, decimal_places=2, default=0.0)
# info = models.CharField("描述", max_length=100, default='')
is_active = models.BooleanField('是否活跃', default=True)
class Meta:
verbose_name = '图书' # 英文中的单数
verbose_name_plural = verbose_name # 英文中的复数,如果不该的话,会在中文字符后面多一个s
关系映射
通常不会把所有的数据都放在同一张表,不易于扩展,如一个学生可以报多个课程,一个课程可以有多个学生报名
一对一映射
语法:OneToOneField(类名,on_delete=xxx),on_delete是级联删除
新概念:
外键——如果A表中的X字段引用了B表中的主键,A表叫做子表,B表叫做主表,X字段称为A表的外键,外键描述的是表之间的关系。级联删除——如员工表中一项数据是部专门ID,部门ID是部门表的主键,如果是级联删除,当删除了部门A的时候,会把所有属于部门A的员工都属给删除。
级联删除on_delete=models.CASCADE实例,步骤,新建应用oto,在设置文件中添加这个应用,然后在oto应用的模型文件中创建author及wife表
from django.db import models
# Create your models here.
class Author(models.Model):
name = models.CharField('姓名', max_length=11)
class Meta:
verbose_name = '作者'
verbose_name_plural = verbose_name
class Wife(models.Model):
name = models.CharField('姓名', max_length=11)
author = models.OneToOneField(Author, on_delete=models.CASCADE) # 级联删除,与Author表共生死
class Meta:
verbose_name = '作者之妻'
verbose_name_plural = verbose_name
创建数据:
from oto.models import *
a1 = Author.objects.create(name='hg') # Author中的表数据创建
w1 = Wife.objects.create(name='zl', author=a1) # wife表中的数据创建
# 还可以用下面的方式创建外键连接
w1 = Wife.objects.create(name='zl', author_id=a1.id)
正向查询方法:
wife = Wife.objects.get(name='zl')
# 关联到wife的丈夫
wife.author.name
反向查询方法:
author = Author.objects.get(name='hg')
# 关联到author的妻子
author.wife.name
反向查询时,没有外键的一方,可以调用反向属性查询到关联的另一方,author虽然没有wife的外键,但Django在创建Wife模型时,默认创建反向属性
一对多映射
一个学生又多个班级,一个班级有多个学生,一本图书只能属于一个出版社,一个出版社可以出版多本图书
一对多需要明确具体角色,在多表上设置外键
创建一对多的语法:
# 一
class A(model.Model):
pass
# 多
class B(model.Model):
attr = models.ForeignKey(A, on_delete=XXX) # 同样需要定义级联删除
## 实例
from django.db import models
# Create your models here.
class Publisher(models.Model):
name = models.CharField('出版社名称', max_length=255)
class Book(models.Model):
title = models.CharField('书名', max_length=255)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
终端进入shell向数据库中添加数据:
from otm.models import *
pub1 = Publisher.objects.create(name='清华大学出版社')
Book.objects.create(title='C++', publisher=pub1)
Book.objects.create(title='JAVA', publisher_id=pub1.id)
正向查询与一对一映射的正向查询一致
反向查询:
## 示例
pub1 = Publisher.objects.get(name='清华大学出版社')
books = pub1.book_set.all() # 查找所有出版社为pub1的图书
多对对映射
MySQL创建多对多需要依赖第三张表
Django无需手动创建第三张表
使用models.ManyToManyField()来关联,并创建第三张表,创建表的方式与上面两种映射相似,只是无需级联删除
示例:
from django.db import models
# Create your models here.
class Author(models.Model):
name = models.CharField('姓名', max_length=200)
class Book(models.Model):
title = models.CharField('书名', max_length=200)
author = models.ManyToManyField(Author)
场景,两个老师都写了名为Python的书
终端进入shell向数据库中添加数据:
from mtm.models import *
## 方案一,先创建作者
# 创建作者
author1 = Author.objects.create(name='黄某')
author2 = Author.objects.create(name='朱某')
# 创建图书,并关联作者
# 创建书名,用create方法把图书和第一个作者关联
book1 = author1.book_set.create(title='python')
# 用add方法把图书和第二个作者关联
author2.book_set.add(book1)
## 方案二,先创建图书
# 创建图书
book = Book.objects.create(title='python1')
#hzx和author1都参与了Python1的创作
author3 = book.author.create(name='hzx')
book.author.add(author1)
目前数据库中有三张表,分别是author、book和book_author,以下简称a表、b表和ab表
a表中列名是id和name,b表中列名是id和title,ab表中列名是a_id和b_id
正向查询:
谁有属性查谁
反向查询继续用xxx_set方法
cookies和session
会话
服务器和浏览器端进行的交互称为会话
交互行为的记忆
会话保持技术:cookies和session存储技术
cookies
保存在客户端浏览器上的存储空间
特点:
- 以键值对形式存储,不能是中文
- 存储的数据有生命周期
- 数据按域隔离存储
- 每次访问同一网址时都会携带到服务器端,如果数据过大会影响响应速度
因此,是在网页响应的时候传递的
语法:
HttpResponse.set_cookie(key, value, max_age, expires)
参数名:
- key——cookie名
- value——cookie值
- max_age——存活时间,秒
- expires——过期时间
不指定max_age和expires时,关闭浏览器失效
获取cookies:
value = request.COOKIES.get('usename')
举个例子:
def get_cookies(request):
value = request.COOKIES.get('usename')
return HttpResponse('usename is %s' % value)
页面响应 usename is hg
删除cookies:
response.delete_cookie(key)
session
cookies把用户数据存储在浏览器上,而session则把数据存储在服务器上,相对更安全
实现方式:
- 使用session需要在浏览器客户端启动cookie,并在cookie中存储sessionid
- 每个客户端都可以在服务器端有一个独立的session
- 不同的请求者之间不共享这个数据
初始配置
settings.py中配置(默认配置)
ISTALLED_APPS 添加’django.contrib.sessions’,
中间件中添加’django.contrib.sessions.middleware.SessionMiddleware’
调用
session封装给了request对象,是一个字典
存、取示例:
def set_session(request):
request.session['usename'] = 'zl'
return HttpResponse('set session is ok')
def get_session(request):
value = request.session['usename']
return HttpResponse('session usename is %s' % value)
在项目的settings.py文件中可以配置session的性能
- 修改保存时间:SESSION_COOKIE_AGE
- 关闭时失效:SESSION_EXPIRE_AT_BROWSER_CLOSE = TRUE
session存在的问题
- 存储在数据库中,单表设计,数据量会持续增加
- 可执行python manage.py clearsessions删除过期的session数据
云笔记项目
用户可以在系统中记录自己的日常学习、旅游笔记,用户数据存储在云笔记平台,用户只有在登录后才能使用笔记功能,且只能查阅自己的笔记内容
用户模块(cookie session):
- 注册
- 登录
- 退出登录
笔记模块(ORM):
- 查看笔记
- 创建笔记
- 修改笔记
- 删除笔记
项目准备
- 禁掉csrf,避免出现post请求出现403问题
- 语言、时区更改
- 数据库配置,创建新的数据库名cloud_note
- 创建并注册应用user
- 数据库中创建数据库cloud_note
项目创建并实现注册功能
-
为应用添加模型(数据表)
from django.db import models # Create your models here. class User(models.Model): username = models.CharField('用户名', max_length=200, unique=True) password = models.CharField('密码', max_length=32) created_at = models.DateTimeField('创建时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) def __str__(self): return "用户" + self.username
-
数据迁移
-
添加视图函数
from django.http import HttpResponse from django.shortcuts import render from .models import * # Create your views here. def reg_view(request): if request.method == 'GET': return render(request, 'user/register.html') elif request.method == 'POST': username = request.POST['username'] password1 = request.POST['password1'] password2 = request.POST['password2'] if password1 != password2: return HttpResponse('两次密码输入不一致') old_users = User.objects.filter(username=username) if old_users: return HttpResponse('用户名已注册') User.objects.create(username=username, password=password1) return HttpResponse('注册成功')
get请求则渲染网页,post请求则检验数据并向数据库中添加注册信息
-
创建模板文件,在项目应用下创建模板文件夹(cloud_note/user/templates/user)并新建模板文件,并适当美化,这一步可以放在编写视图函数之前
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>注册</title> <style> .input { width: 100px; } </style> </head> <body> <form action="/user/reg" method="post"> <p class="input"> 用户名:<input type="text" name="username"> </p> <p class="input"> 密码:<input type="text" name="password1"> </p> <p class="input"> 再次输入密码:<input type="text" name="password2"> </p> <p> <input type="submit" value="注册"> </p> </form> </body> </html>
-
创建分布式路由
from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('user/', include('user.urls')), ]
-
分布式路由中添加新的路由和视图函数
from django.urls import path from user import views urlpatterns = [ # path('admin/', admin.site.urls), path('reg', views.reg_view), ]
优化注册功能
- 上面使用的是将密码明文写入数据库,安全性差
使用哈希算法——给定明文,计算出一段定长的,不可逆的值
修改视图函数
from django.http import HttpResponse
from django.shortcuts import render
from .models import *
import hashlib
# Create your views here.
def reg_view(request):
if request.method == 'GET':
return render(request, 'user/register.html')
elif request.method == 'POST':
username = request.POST['username']
password1 = request.POST['password1']
password2 = request.POST['password2']
if password1 != password2:
return HttpResponse('两次密码输入不一致')
m = hashlib.md5()
m.update(password1.encode()) # 必须转码
password_m = m.hexdigest()
old_users = User.objects.filter(username=username)
if old_users:
return HttpResponse('用户名已注册')
User.objects.create(username=username, password=password_m)
return HttpResponse('注册成功')
将密码存储为哈希MD5值
-
并发问题,多个用户同时注册统一用户名时,即使有if判断,也会因为并发报错,可以使用try来规避
继续优化视图函数,在创建新的数据时,使用异常检测
try: user = User.objects.create(username=username, password=password_m) except Exception as e: print(e) return HttpResponse('用户名已注册')
-
免登陆一天功能
视图函数中增加session的功能
request.session['username'] = username request.session['id'] = user.id
settings.py中修改session存储时间
SESSION_COOKIE_AGE = 60 * 60 * 24
实现登录界面
-
先写模板文件login.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登录</title> <style> .input { width: 100px; } </style> </head> <body> <form action="/user/login" method="post"> <p class="input"> 用户名<input type="text" name="username"> </p> <p class="input"> 密码<input type="password" name="password"> </p> <p><input type="checkbox" name="remember_me">记住用户名</p> <p><input type="submit" value="登录"></p> </form> </body> </html>
我在脱离教程独自写这个简单界面的时候犯了一个非常低级的错误,就是没有写form表单标签,上来就直接input,导致怎么点submit按钮都没反应,这再一次验证了form表单的作用
另外,这里还添加了一个记住用户的CheckBox,后面会用到
-
添加路由
urlpatterns = [ # path('admin/', admin.site.urls), path('reg', views.reg_view), path('login', views.login_view), ]
-
创建视图函数
def login_view(request): if request.method == 'GET': return render(request, 'user/login.html') if request.method == 'POST': username = request.POST['username'] password = request.POST['password'] # remember_me = request.POST['remember_me'] m = hashlib.md5() m.update(password.encode()) password_m = m.hexdigest() try: # 取用户名,建议用get,也可以用filter,如果用filter,则应写成user = User.objects.filter(username=username)[0], # 因为filter查出来的是一个集合Queryset user = User.objects.get(username=username) except Exception as e: print(e) return HttpResponse('用户名或密码错误') # if user: # if password_m != user.password: # return HttpResponse('密码错误') # else: # return HttpResponse('用户名不存在') if password_m != user.password: return HttpResponse('用户名或密码错误') # 记住会话状态 request.session['username'] = username request.session['id'] = user.id response = HttpResponse('登录成功') # 判断用户是否选择了记住用户名 if 'remember_me' in request.POST: response.set_cookie('username', username, 3600 * 24 * 3) response.set_cookie('id', user.id, 3600 * 24 * 3) return response
这里的逻辑应该是很清楚的:
-
判断是get请求还是post请求
-
post请求下,取出用户名,密码,密码进行哈希转换
-
判断用户名和密码是否正确,我自认为的逻辑是判断用户名是否存在,然后判断密码是否正确,看我注释起来的一段代码,但是听讲课的那个老师讲,实际中不要这么明确,防止别人窃取信息
-
记住会话状态,通过设置session和cookie,并修改cookie的存储时间
记住会话状态是一个非常复杂的过程,对于初学者来说,总体来说,有这么几个步骤:
- 首先把用户名和id写入到session中去,也可以只保存id,无所谓
- 看用户是否勾选记住了用户名,如果勾选了,把用户名和id存到cookie里面去,为什么要进行这一步,按理说只有session写了就可以了,看视频教程时,老师说的是因为session是写进数据库了的,如果太多人都勾选的话,这个数据库压力比较大,因此,可以把session的存储时间写短一点,然后cookie中再存储一次,这样数据库的压力会小一点,但在获取是否登录就会相对麻烦一点
- 在get请求(也就是输入地址)时判断session和cookie是否记录了登录状态,同时校验session和cookie,对应上面的
这里有一个很坑的地方:
如果我的用户名是中文的,勾选记住状态时,会报下面的错:
UnicodeEncodeError: 'latin-1' codec can't encode characters in position 153-154: ordinal not in range(256)
从报错信息来看,说的是编码的问题,我百度了很多方法,都没能解决,但是都指向同一个问题,就是用户名不能是中文名,因此,在注册时,应该禁止用户输入中文字符,所以我在reg_view()视图函数中添加了一段判断用户名是否包含中文字符的代码,如下:
def reg_view(request): if request.method == 'GET': return render(request, 'user/register.html') elif request.method == 'POST': username = request.POST['username'] password1 = request.POST['password1'] password2 = request.POST['password2'] # 经验证,注册的用户名中不能使用中文,否则登录的时候选择记住用户的话,会报错,所以用户注册时,用户名中不能有中文 for _char in username: if '\u4e00' <= _char <= '\u9fff': return HttpResponse('用户名中不能包含中文') if password1 != password2: return HttpResponse('两次密码输入不一致') # 将密码存储为MD5值 m = hashlib.md5() m.update(password1.encode()) password_m = m.hexdigest() old_users = User.objects.filter(username=username) if old_users: return HttpResponse('用户名已注册') # 考虑注册时的并发问题,使用异常检测 try: user = User.objects.create(username=username, password=password_m) except Exception as e: print(e) return HttpResponse('用户名已注册') # 免登陆功能 request.session['username'] = username request.session['id'] = user.id # request.session return HttpResponse('注册成功')
这样再次注册的时候,就能保证用户名的正确输入了
回到前文,怎样记住会话状态呢,在login_view()函数的get请求里面添加如下代码:
def login_view(request): if request.method == 'GET': # 检查登录状态,如果登录了,显示“已登录” if request.session.get('username') and request.session.get(id): return HttpResponse('已登录') # 检查cookies c_username = request.COOKIES.get('username') c_id = request.COOKIES.get('id') if c_id and c_username: # 回写session request.session['username'] = c_username request.session['id'] = c_id return HttpResponse('已登录') return render(request, 'user/login.html')
从代码来看,先是判断session中是否存了usename和id,有的话显示已登录,没有的话检查cookies,也是检查是否存储了这两个值,如果都没有存储,就跳转到登录页面
至此,逻辑已经相对比较清楚
-
创建主页
前面在登录的时候,get请求发现用户有登录的时候,直接HttpResponse(‘已登录’)显然是不合理的,正常是系统应该是登录后跳转到一个主页界面,现在就是要干这个事儿
创建应用index,注册应用,添加路由,这些操作不再记录
先写模板文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
{#{{ request.session }}#}
{#{{ request.COOKIES }}#}
{% if request.session.username %}
<p>
欢迎 {{ request.session.username }}
</p>
<p>
<a href="">退出登录</a>
</p>
<p>
<a href="">进入我的笔记</a>
</p>
{% else %}
{% if request.COOKIES.username %}
<p>
欢迎 {{ request.COOKIES.username }}
</p>
<p>
<a href="">退出登录</a>
</p>
<p>
<a href="">进入我的笔记</a>
</p>
{% else %}
<p>
<a href="/user/login">登录</a>
</p>
<p>
<a href="/user/reg">注册</a>
</p>
{% endif %}
{% endif %}
</body>
</html>
退出和进入笔记的超链接还没写
登录和注册都已经链接上,注意,登录或注册成功后,应该跳转到首页,同时,检查到cookies和session有过登录,也应该直接跳转的登录后的主页,因此,所有user/views.py中的HttpResponse('已登录')
和HttpResponse('登录成功')
都应该改成网页跳转,也就是重定向
return HttpResponseRedirect('/index')
给出的是相对地址
注意,还没写视图函数,这里的视图函数就非常简单了,如下
from django.shortcuts import render
# Create your views here.
def index_view(request):
return render(request, 'index/index.html')
直接渲染文件就行了
笔记模型
创建note应用,注册,添加路由
视图文件views.py中添加视图函数,首先检查是否是登录状态,后续使用装饰器来检查,节省代码
编写装饰器函数
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render
from .models import *
from django.contrib import messages
# Create your views here.
def check_login(fn):
def wrap(request, *args, **kwargs):
if 'username' not in request.session or 'id' not in request.session:
c_username = request.COOKIES.get('username')
c_id = request.COOKIES.get('id')
if not c_username or not c_id:
return HttpResponseRedirect('/user/login')
else:
request.session['username'] = c_username
request.session['id'] = c_id
return fn(request, *args, **kwargs)
return wrap
添加增加笔记函数
仍然在视图文件中添加
@check_login
def add_note(request):
if request.method == 'GET':
return render(request, 'note/add_note.html')
if request.method == 'POST':
id = request.session['id']
title = request.POST['title']
content = request.POST['content']
Note.objects.create(title=title, content=content, user_id=id)
# return HttpResponse('添加笔记成功')
return HttpResponseRedirect('/note/note') # 重定向,跳转到图书列表
对应的模板文件add_note.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加笔记</title>
<style>
p {
width: 150px;
}
</style>
</head>
<body>
<form action="/note/add" method="post">
<p>
标题:<input type="text" name="title">
</p>
<p>
<textarea cols="30" rows="10" name="content"></textarea>
</p>
<p>
<input type="submit" value="保存" onclick="alert('保存成功')">
</p>
</form>
</body>
</html>
显示界面:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yi84cT29-1652317066153)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220507175730859.png)]
增加笔记视图函数中最后重定向到图书列表,因此要显示一个图书列表模板文件
list_note.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>笔记列表</title>
<style>
td, th {
border: 1px solid;
}
.note {border-collapse: collapse;}
</style>
</head>
<body>
<h3>
{{ request.session.username }} 的笔记 <a href="/note/add">添加新笔记</a> <a href="/index">返回首页</a>
</h3>
<table class="note">
<tr>
<th>ID</th>
<th>标题</th>
<th>创建时间</th>
<th>修改时间</th>
<th>修改</th>
<th>删除</th>
</tr>
{% for note in all_notes %}
<tr>
<td>{{ note.id }}</td>
<td>{{ note.title }}</td>
<td>{{ note.created_at }}</td>
<td>{{ note.updated_at }}</td>
<td><a href="/note/mod/{{ note.id }}">修改</a></td>
<td><a href="">删除</a></td>
</tr>
{% endfor %}
</table>
</body>
</html>
显示界面:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CipFue2A-1652317066154)(C:\Users\HP\Desktop\在办工作2021\99-其他临时工作\个人技术博客\image-20220507175800788.png)]
对应的视图函数
def list_view(request):
all_notes = Note.objects.all()
return render(request, 'note/list_note.html', locals())
增加修改笔记的功能
先上模板文件mod.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>修改笔记</title>
</head>
<body>
<form action="/note/mod/{{ note.id }}" method="post">
<h3>
<p>主题:</p>
<p>
<input type="text" name="title" value="{{ note.title }}">
</p>
</h3>
<h3>
<p>笔记详情</p>
<p>
<textarea cols="30" rows="10" name="content" value="{{ note.content }}">{{ note.content }}</textarea>
</p>
</h3>
<p>
<input type="submit" value="保存" onclick="alert('保存成功')">
<a href="/note/note">返回列表页</a>
</p>
</form>
</body>
</html>
显示界面:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o7oHCNQV-1652317066154)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220507180334442.png)]
视图函数:
def mod_view(request, id):
try:
note = Note.objects.get(id=id)
except Exception as e:
print("修改出现错误:%s" % e)
return HttpResponse('笔记不存在')
if request.method == 'GET':
return render(request, 'note/mod_note.html', locals())
if request.method == 'POST':
title = request.POST['title']
content = request.POST['content']
note.title = title
note.content = content
note.save()
return HttpResponseRedirect('/note/note')
注意,这里是有参传入,传入了一个参数id,其实就是书本的id,因此,路由文件中可以使用path转换器来批量添加路由,如下:
note\urls.py
from django.urls import path
from note import views
urlpatterns = [
path('add', views.add_note),
path('note', views.list_view),
path('mod/<int:id>', views.mod_view),
]
这里个人调试了好久,发现有几个易错点:
- 路由地址的相对路径和绝对路径问题,如
return render(request, 'note/mod_note.html', locals())
中的地址最前面是没有斜杆/的,而模板文件中传递的相对路由<form action="/note/mod/{{ note.id }}" method="post">
最前面则是有斜杆的 - path转换器中传递的参数要和视图函数中传递的参数一致
- 分布式路由中的路由地址到底怎么写,其实主路由已经给到了note,所以后面的路由就不需要再写note了,如果加上了,则会多一个note,比如第二条路由,实际上的访问地址应该是
127.0.0.1:8000/note/note
,这和课件是有出入的,而模板文件中,如第一个易错点的form标签中的路由地址,这应该是添加在127.0.0.1:8000之后的地址,这点比较重要,理解清楚了,路由就不会再犯错了。
增加笔记删除功能
删除功能的增加相对简单,删除不是真删除,要伪删除,也就是在模型中添加伪删除字段,is_active,默认值是true,笔记列表中仅显示is_active=True的笔记,然后增加删除的视图函数del_view(),将点击删除的笔记的is_active值设置为false,保存后重定向值note列表,具体流程如下:
-
models.py中添加下面一行代码
is_active = models.BooleanField('是否显示', default=True)
-
数据迁移
-
增加视图函数del_view(),如下
def del_view(request, id): try: note = Note.objects.get(id=id) except Exception as e: print("删除出现错误:%s" % e) return HttpResponse('笔记不存在') note.is_active = False note.save() return HttpResponseRedirect('/note/note')
-
分布式路由中添加删除操作的路由,并绑定视图函数
-
模板文件(list_note.html)中添加删除的超链接,如下
<td><a href="/note/del/{{ note.id }}" onclick="return confirm('是否确认删除');">删除</a></td>
-
注意,之前显示笔记列表的视图函数查找数据用的是
all_notes = Note.objects.all()
,这查的是所有的数据,显然不合理,删除后应该不显示伪删除的数据,所以应该改成all_notes = Note.objects.filter(is_active=True)
,而且这里的变量名all_notes不能瞎改,因为传入到模板文件list_note.html中的数据就是all_notes
至此,完成云笔记项目的所有功能,后面我会把整个项目放到git上去
小结3
通过这个非常简单的项目,大体上是捋清了使用Django框架进行前后端操作的步骤,总体上应把握以下几点
- 不同的功能模块应该单独创建应用,实现功能分离
- 重要的文件是:
- 视图函数views.py,用来向模板文件传递数据
- 模型文件models.py,用来创建数据表
- 模型文件(文件名不固定),用来显示前端页面,以及接收视图函数在执行过程中传递过来的数据在页面上显示,以及实现视图函数要执行的功能
- 路由文件urls.py,一般使用分布式路由,针对不同的地址,选择不同的路由方式,在使用路由转换器的时候,注意视图函数应该传递同名参数
- 地址的写法很重要,使用render渲染网页时,应该写到应用的网页文件,使用HttpResponseRedirect进行重定向时,应该给的是路由,模板文件中超链接给的也是路由
缓存
更快的读取数据的介质,存储临时数据
一般情况下,当数据库中有大量的数据时,使用orm查询数据会非常慢,因此如果缓存中有数据的话,直接从缓存中取出数据,会简单很多
Django官方给的思路:
- 页面是否在缓存中,如果在,直接返回缓存页面
- 如果不在,该怎么取怎么取,取完后把数据存入缓存
缓存的使用场景:数据变动频率较低
目前,个人科研工作中的项目开发,应该暂时不会去设置缓存,因此不做具体的项目,只做了解
Django中设置缓存
数据库缓存
把一次负责查询的结果直接存储到表里,比如多个条件的过滤查询结果,可以避免重复进行复杂的查询,以提升效率
配置方法,在settings.py中设置CACHES字段
可以存储在数据库中,也可以存在服务器本地内存中,文件系统中
数据库缓存示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GQL23Kp7-1652317066155)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220508141055835.png)]
服务器缓存:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3pukUlxj-1652317066155)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220508141123707.png)]
文件系统缓存
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9C3as1Ae-1652317066155)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220508141259518.png)]
整体缓存策略
-
视图函数中的相应内容放到缓存中,使用装饰器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AZ9rMcET-1652317066159)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220508142149690.png)]
-
在路由中套上装饰器,与1是一回事
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bs7VV787-1652317066160)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220508142221486.png)]
局部缓存策略
整体缓存无法干预,没法删除,简单粗暴,无法认为控制
如果只想缓存视图函数中的某行代码,就可以用到局部缓存
缓存API
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZyS3SlNx-1652317066160)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220508143303391.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5oZmz96c-1652317066160)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220508143816396.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pP8Us2ez-1652317066161)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220508143828878.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a0sEzvkr-1652317066161)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220508144052223.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RlXogKB2-1652317066161)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220508144124003.png)]
浏览器缓存策略
浏览器是否缓存,需要服务器给暗号
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Gi8M5h8-1652317066162)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220508144952534.png)]
上面这连个参数,都可以通过整体缓存策略中的装饰器传给浏览器,不需要单独配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5GNDsjy7-1652317066162)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220508145353529.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FjeQrjpR-1652317066162)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220508145527258.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VwqquOzd-1652317066163)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220508145737037.png)]
中间件
用于请求、响应,用于全局改变Django的输入输出
中间件以类的形式体现
每个中间件组件负责一些特定的功能
中间件必须继承自django.utils.desprecation.MiddlewareMixin类
必须实现以下五个方法中的一个或多个(主要前三个):
-
process_request(self, request)——起拦截作用
执行路由之前被调用,在每个请求上调用,返回none或HttpResponse对象
-
process_view(self, request, callback_args, callback_kwargs)——拦截
调用视图之前被调用,在每个请求上调用,返回none或HttpResponse对象
-
process_response(self, request, response)
响应返回给浏览器之前被调用,返回HttpResponse对象
-
process_exception(self, request, exception)
处理异常时调用,返回HttpResponse对象,可能项目会报错,比如给用户发个邮件,告诉用户有错误
-
process_template_response(self, request, response)——很少用到
在视图函数执行完毕且视图返回的对象中包含render方法时被调用,需要返回实现了render方法的响应对象
中间件的大多数方法在返回None时表示忽略当前操作进入下一项事件,当返回HttpResponse对象时表示此请求有问题,要结束,直接返回给客户端
注册方法:
settings.py中注册自定义的中间件
在项目目录下创建一个文件夹middleware(严谨来说,是创建一个Python包),接着去创建一个__init__.py
初始化文件和一个中间件Python文件(如mymiddleware.py),在mymiddleware.py中添加类,继承自,,,
写好之后去注册
编写中间件步骤
- 编写中间件类:
# file : middleware/mymiddleware.py
from django.http import HttpResponse, Http404
from django.utils.deprecation import MiddlewareMixin
class MyMiddleWare(MiddlewareMixin):
def process_request(self, request):
print("中间件方法 process_request 被调用")
def process_view(self, request, callback, callback_args, callback_kwargs):
print("中间件方法 process_view 被调用")
def process_response(self, request, response):
print("中间件方法 process_response 被调用")
return response
def process_exception(self, request, exception):
print("中间件方法 process_exception 被调用")
def process_template_response(self, request, response):
print("中间件方法 process_template_response 被调用")
return response
- 注册中间件:
# file : settings.py
MIDDLEWARE = [
...
'middleware.mymiddleware.MyMiddleWare',
]
#单个中间件输出
MyMW process_request do---
MyMW process_views do ---
----this is test cache views ----
MyMW process_response do ---
#多个中间件时 输出
MyMW process_request do---
MyMW2 process_request do---
MyMW process_views do ---
MyMW2 process_views do ---
----this is test cache views ----
MyMW2 process_response do ---
MyMW process_response do ---
- 中间件的执行过程
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IcHEHYb5-1652317066163)(C:\Users\HP\Desktop\第3阶段 Python WEB全栈式工程师(HTML,CSS,JavaScript,MYSQL核心,Django框架,Ajax)\第三阶段\Django\DJANGO02\day07\day07\day07-note\images\middleware.jpeg)]
中间件练习
-
用中间件实现强制某个IP地址只能向/test 发送 5 次GET请求
-
提示:
- request.META[‘REMOTE_ADDR’] 可以得到远程客户端的IP地址
- request.path_info 可以得到客户端访问的GET请求路由信息
-
答案:
from django.http import HttpResponse, Http404 from django.utils.deprecation import MiddlewareMixin import re class VisitLimit(MiddlewareMixin): '''此中间件限制一个IP地址对应的访问/user/login 的次数不能改过10次,超过后禁止使用''' visit_times = {} # 此字典用于记录客户端IP地址有访问次数 def process_request(self, request): ip_address = request.META['REMOTE_ADDR'] # 得到IP地址 if not re.match('^/test', request.path_info): return times = self.visit_times.get(ip_address, 0) print("IP:", ip_address, '已经访问过', times, '次!:', request.path_info) self.visit_times[ip_address] = times + 1 if times < 5: return return HttpResponse('你已经访问过' + str(times) + '次,您被禁止了')
跨站请求伪造保护 CSRF
-
跨站请求伪造攻击
- 某些恶意网站上包含链接、表单按钮或者JavaScript,它们会利用登录过的用户在浏览器中的认证信息试图在你的网站上完成某些操作,这就是跨站请求伪造(CSRF,即Cross-Site Request Forgey)。
-
说明:
-
CSRF中间件和模板标签提供对跨站请求伪造简单易用的防护。
-
作用:
- 不让其它表单提交到此 Django 服务器
-
解决方案:
-
取消 csrf 验证(不推荐)
- 删除 settings.py 中 MIDDLEWARE 中的
django.middleware.csrf.CsrfViewMiddleware
的中间件
- 删除 settings.py 中 MIDDLEWARE 中的
-
通过验证 csrf_token 验证
需要在表单中增加一个标签 {% csrf_token %}
-
分页
分页是指在web页面有大量数据需要显示,为了阅读方便在每个页页中只显示部分数据
- 好处:
- 方便阅读
- 减少数据提取量,减轻服务器压力。
- Django提供了Paginator类可以方便的实现分页功能
- Paginator类位于
django.core.paginator
模块中。
Paginator对象
掌控全部页面
-
对象的构造方法
- Paginator(object_list, per_page)
- 参数
- object_list 需要分页数据的对象列表
- per_page 每页数据个数
- 返回值:
- 分页对象
-
Paginator属性
- count:需要分类数据的对象总数
- num_pages:分页后的页面总数
- page_range:从1开始的range对象, 用于记录当前面码数
- per_page 每页数据的个数
-
Paginator方法
- Paginator.page(number)
- 参数 number为页码信息(从1开始)
- 返回当前number页对应的页信息
- 如果提供的页码不存在,抛出InvalidPage异常
- Paginator.page(number)
-
Paginator异常exception
- InvalidPage:当向page()传入一个无效的页码时抛出
- PageNotAnInteger:当向page()传入一个不是整数的值时抛出
- EmptyPage:当向page()提供一个有效值,但是那个页面上没有任何对象时抛出
Page对象
掌控单个页面
-
创建对象
Paginator对象的page()方法返回Page对象,不需要手动构造 -
Page对象属性
- object_list:当前页上所有数据对象的列表
- number:当前页的序号,从1开始
- paginator:当前page对象相关的Paginator对象
-
Page对象方法
- has_next():如果有下一页返回True
- has_previous():如果有上一页返回True
- has_other_pages():如果有上一页或下一页返回True
- next_page_number():返回下一页的页码,如果下一页不存在,抛出InvalidPage异常
- previous_page_number():返回上一页的页码,如果上一页不存在,抛出InvalidPage异常
- len():返回当前页面对象的个数
-
说明:
- Page 对象是可迭代对象,可以用 for 语句来 访问当前页面中的每个对象
-
参考文档https://docs.djangoproject.com/en/1.11/topics/pagination/
示例:
视图函数
from django.core.paginator import Paginator
from django.shortcuts import render
def test_page(request):
# /test_page?page=1 使用查询字符串的方式
page_num = request.GET.get('page', 1)
all_data = ['a', 'b', 'c', 'd', 'e']
paginator = Paginator(all_data, 2)
c_page = paginator.page(int(page_num))
return render(request, 'test_page.html', locals())
模板文件test_page.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>分页</title>
</head>
<body>
{% for p in c_page %}
<p>
{{ p }}
</p>
{% endfor %}
{% if c_page.has_previous %}
<a href="/test_page?page={{ c_page.previous_page_number }}">上一页</a>
{% else %}
上一页
{% endif %}
{% for p_num in paginator.page_range %}
{% if p_num == c_page.number %}
{{ p_num }}
{% else %}
<a href="/test_page?page={{ p_num }}">{{ p_num }}</a>
{% endif %}
{% endfor %}
{% if c_page.has_next %}
<a href="/test_page?page={{ c_page.next_page_number }}">下一页</a>
{% else %}
下一页
{% endif %}
</body>
</html>
路由
from django.contrib import admin
from django.urls import path, include
from cloud_note import views
urlpatterns = [
path('admin/', admin.site.urls),
path('user/', include('user.urls')),
path('index/', include('index.urls')),
path('note/', include('note.urls')),
path('test_page', views.test_page),
]
文件上传与下载
csv文件下载
视图函数
def make_page_csv(request):
page_num = request.GET.get('page', 1)
all_data = ['a', 'b', 'c', 'd', 'e']
paginator = Paginator(all_data, 2)
c_page = paginator.page(int(page_num))
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment;filename="page-%s.csv"' % (page_num)
print(response)
writer = csv.writer(response)
for b in c_page:
writer.writerow([b])
return response
模板文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>分页</title>
</head>
<body>
<a href="/make_page_csv?page={{ c_page.number }}">生成当前页面csv</a>
{% for p in c_page %}
<p>
{{ p }}
</p>
{% endfor %}
{% if c_page.has_previous %}
<a href="/test_page?page={{ c_page.previous_page_number }}">上一页</a>
{% else %}
上一页
{% endif %}
{% for p_num in paginator.page_range %}
{% if p_num == c_page.number %}
{{ p_num }}
{% else %}
<a href="/test_page?page={{ p_num }}">{{ p_num }}</a>
{% endif %}
{% endfor %}
{% if c_page.has_next %}
<a href="/test_page?page={{ c_page.next_page_number }}">下一页</a>
{% else %}
下一页
{% endif %}
</body>
</html>
修改路由
关键逻辑在于下面这两行代码
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment;filename="page-%s.csv"' % (page_num)
- 修改HttpResponse响应头中的content_type为文本内容
- 在响应中添加key——Content-Disposition为attachment(附件),并定义要下载的文件名
文件上传
-
文件上传必须为POST提交方式
-
表单
<form>
中文件上传时必须有带有enctype="multipart/form-data"
时才会包含文件内容数据。 -
表单中用
<input type="file" name="xxx">
标签上传文件- 名字
xxx
对应request.FILES['xxx']
对应的内存缓冲文件流对象。可通能过request.FILES['xxx']
返回的对象获取上传文件数据 file=request.FILES['xxx']
file 绑定文件流对象,可以通过文件流对象的如下信息获取文件数据
file.name 文件名
file.file 文件的字节流数据
- 名字
-
上传文件的表单书写方式
<!-- file: index/templates/index/upload.html --> <html> <head> <meta charset="utf-8"> <title>文件上传</title> </head> <body> <h3>上传文件</h3> <form method="post" action="/upload" enctype="multipart/form-data"> <input type="file" name="myfile"/><br> <input type="submit" value="上传"> </form> </body> </html>
-
在setting.py 中设置一个变量MEDIA_ROOT 用来记录上传文件的位置
# file : settings.py ... MEDIA_ROOT = os.path.join(BASE_DIR, 'static/files')
-
在当前项目文件夹下创建
static/files
文件夹$ mkdir -p static/files
-
添加路由及对应的处理函数
# file urls.py urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^upload', views.upload_view) ]
-
配置media后,需要手动添加路由,添加方式比较特殊
# file urls.py from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import path from mysite08 import views urlpatterns = [ path('admin/', admin.site.urls), path('test_upload', views.test_upload), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
这个过程等价于做了MEDIA_URL开头的路由,Django接到该请求后去MEDIA_ROOT路径查找资源
-
上传文件的视图处理函数–第一种方法,传统的open写入,但会存在重名文件覆盖的问题
# file views.py from django.http import HttpResponse, Http404 from django.conf import settings import os def upload_view(request): if request.method == 'GET': return render(request, 'index/upload.html') elif request.method == "POST": a_file = request.FILES['myfile'] print("上传文件名是:", a_file.name) filename =os.path.join(settings.MEDIA_ROOT, a_file.name) with open(filename, 'wb') as f: data = a_file.file.read() f.write(data) return HttpResponse("接收文件:" + a_file.name + "成功") raise Http404
-
上传文件的视图处理函数–第二种方法,Django封装好的方法
数据库中添加一张表,用于存放上传数据的名称和文件名,步骤
创建应用---->添加模型类---->添加模型---->模型迁移---->视图函数
模型类(upload_app/models.py)
from django.db import models # Create your models here. class Content(models.Model): title = models.CharField('文章名字', max_length=200) picture = models.FileField(upload_to='picture')
视图函数(views.py)
#!/usr/bin/env python #-*- coding:utf-8 -*- # author:HP # datetime:2022/5/11 10:45 from django.http import HttpResponse from django.shortcuts import render # from upload_app import models from upload_app.models import Content def test_upload(request): if request.method == 'GET': return render(request, 'test_upload.html') elif request.method == 'POST': title = request.POST['title'] myfile = request.FILES['myfile'] Content.objects.create(title=title, picture=myfile) return HttpResponse('---文件上传成功')
后端缓存:
1, 将视图函数最终结果 转存到其他介质里
【mysql表里,文件里, 内存里】
2,解决了 views重复计算问题 【有效降低视图层时间复杂度】
3,http 1.1 cache头 触发了 浏览器强缓存
浏览器缓存:
1,带有强缓存的响应头 的响应数据,存储自己的硬盘中或内存里
2,当强缓存有数据时,可以完全不给服务器发送请求,直接读取缓存内容 【减少 浏览器与服务器之间的请求次数】
内建用户系统
这里的用户内建系统和云笔记项目中的user应用作用一样
-
Django带有一个用户认证系统。 它处理用户账号、组、权限以及基于cookie的用户会话。
-
作用:
- 添加普通用户和超级用户
- 修改密码
-
文档参见
-
User模型类
-
位置:
from django.contrib.auth.models import User
-
默认user的基本属性有:
属性名 类型 是否必选 username 用户名 是 password 密码 是 email 邮箱 可选 first_name 名 last_name 姓 is_superuser 是否是管理员帐号(/admin) is_staff 是否可以访问admin管理界面 is_active 是否是活跃用户,默认True。一般不删除用户,而是将用户的is_active设为False。 last_login 上一次的登录时间 date_joined 用户创建的时间
auth基本模型操作
-
创建用户
-
创建普通用户create_user
from django.contrib.auth import models user = models.User.objects.create_user(username='用户名', password='密码', email='邮箱',...) ... user.save()
-
创建超级用户create_superuser
from django.contrib.auth import models user = models.User.objects.create_superuser(username='用户名', password='密码', email='邮箱',...) ... user.save()
-
-
删除用户
from django.contrib.auth import models try: user = models.User.objects.get(username='用户名') user.is_active = False # 记当前用户无效 user.save() print("删除普通用户成功!") except: print("删除普通用户失败") return HttpResponseRedirect('/user/info')
-
修改密码set_password
from django.contrib.auth import models try: user = models.User.objects.get(username='xiaonao') user.set_password('654321') user.save() return HttpResponse("修改密码成功!") except: return HttpResponse("修改密码失败!")
-
检查密码是否正确check_password
from django.contrib.auth import models try: user = models.User.objects.get(username='xiaonao') if user.check_password('654321'): # 成功返回True,失败返回False return HttpResponse("密码正确") else: return HttpResponse("密码错误") except: return HttpResponse("没有此用户!")
-
登录状态保持
from django.contrib.auth import login def login_view(request): user = authenticate(username=username, password=password) login(request, user)
-
登录状态校验
需要使用装饰器login_required
# 伪代码 from django.contrib.auth import login_required @login_required def index_view(request): # 该视图必须为用户登录状态下才可访问 # 当前登录用户可以通过request.user获取 login_user = request.user ...
-
登录状态取消
更简单,直接使用内置的logout方法
from django.contrib.auth import logout def logout_view(request): logout(request)
Django发送邮件
业务场景:
业务警告
邮件验证
密码找回
邮件相关协议
SMTP 简单邮件传输协议 发邮件 推送协议
IMAP 交互式邮件访问协议 收邮件 拉取协议
POP3 支持使用客户端远程管理在服务器上的电子邮件 拉取协议
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tyr8LZZH-1652317066163)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220511135259744.png)]
Django充当邮件客户端的身份,自动发出邮件
原理:
- 给Django授权一个邮箱
- Django用该邮箱给对应的收件人发送邮件
- djang.core.mail封装了电子邮件的自动发送SMTP协议
邮件发送实例
-
settings.py中配置邮件发送
# 发送邮件设置 EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # 固定写法 EMAIL_HOST = 'smtp.qq.com' # 腾讯QQ邮箱 SMTP 服务器地址 EMAIL_PORT = 25 # SMTP服务的端口号 EMAIL_HOST_USER = 'xxxx@qq.com' # 发送邮件的QQ邮箱 EMAIL_HOST_PASSWORD = '******' # 邮箱的授权码(即QQ密码) EMAIL_USE_TLS = True # 与SMTP服务器通信时,是否启动TLS链接(安全链接)默认false
这一段根据自己的实际情况添加
-
settings.py中自定义要发送邮件的对象
EX_EMAIL = ['马赛克2@qq.com', '马赛克3@qq.com']
-
添加中间件,报错时发送邮件
from django.http import HttpResponse, Http404 from django.utils.deprecation import MiddlewareMixin import traceback from django.core import mail from django.conf import settings class ExceptionMW(MiddlewareMixin): def process_exception(self, request, exception): print(exception) print(traceback.format_exc()) mail.send_mail( subject='来自Django的项目告警信息', message=traceback.format_exc(), from_email='马赛克1@qq.com', # 发送者[当前配置邮箱] recipient_list=settings.EX_EMAIL ) return HttpResponse('---对不起 当前网页忙')
有几点要注意,中间件写好后,要到settings.py中去注册,另外,邮件的接受列表直接调用自第2步中添加的邮件列表
发送邮件结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-44s6QHJE-1652317066164)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20220511152306208.png)]
项目部署上线
部署过程中遇到的问题先记录
-
局域网其他设备无法访问的问题
解决方法:
-
settings.py中修改ALLOWED_HOSTS = [‘*’]
-
pycharm终端启动Django服务命令
python manage.py runsever 0.0.0.0:8000
, 0.0.0.0代表网络内所有的主机都可以访问 -
其他设备访问
192.168.43.246:8000/index
即可,192.168.43.246为主机(服务器)的ip
-
局域网内手机访问云笔记项目
django项目上线