Django 数据库操作
通过ORM(Object-Relation Mapping,对象关系映射),把一个类对应到一个表,类的每个实例对应表中的一条记录,类的每个属性对应表中的每个字段,开发人员只需像操作对象一样从数据库操作数据。
注意,自定义的mysql_client.py
和redis_client.py
中应使用单例模式,保证同一进程下只有一个数据库实例。
数据库配置
在settings.py
的DATABASES
中可以配置数据库,默认使用SQLite3。
如使用MySQL,则需要安装驱动模块:mysqlclient,这是MySQLdb的分支,用于兼容Python3,安装时也会附带MySQLdb包。Django进一步封装了该驱动模块以实现内置的ORM模型(见django.db.backends.mysql.base
的DatabaseWrapper
类),也可以创建其同名子类并进一步自定义封装:
# Linux
pip3 install mysqlclient -i https://pypi.tuna.tsinghua.edu.cn/simple
# Windows
pip3 install mysqlclient @ file:///D:/ChinaDuanCai/DRF/static/mysqlclient-2.0.3-cp37-cp37m-win_amd64.whl
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # 也可以自定义为封装后的模块路径
'NAME': 'drf', # 连接的数据库 #一定要存在的数据库名
'HOST': '127.0.0.1', # mysql的ip地址
'PORT': 3306, # mysql的端口
'USER': 'root', # mysql的用户名
'PASSWORD': 'vl1qaz@WSX' # mysql的密码
}
}
关于为什么不用Pymysql
,以及通过连接池解决并发问题,详见https://www.jianshu.com/p/e51dc4f6356a
设计模型
# models.py
from django.db import models
from DRF.settings import MEDIA_ROOT
class Type(models.Model):
id = models.AutoField(primary_key=True, unique=True)
name = models.CharField(verbose_name="类型名", max_length=10) # verbose_name用于自带的admin页面中显示名称
def cb():
return None
def upload_to(instance, filename):
return "/".join([MEDIA_ROOT, "upload", filename])
class Blog(models.Model):
sex_choices = (
(0, '男'),
(1, '女'),
)
# 自增int + 主健 + 唯一值,model中无主键列时会自动创建该id字段
id = models.AutoField(primary_key=True, unique=True)
# 建立索引
code = models.CharField(max_length=64, null=True, verbose_name="编号", db_index=True)
name = models.CharField(verbose_name="姓名", max_length=32)
age = models.IntegerField("年龄")
sex = models.SmallIntegerField('性别', choices=sex_choices, default=0)
has_hair = models.BooleanField()
phone = models.CharField("手机号", max_length=14, null=True) # 该字段可以为null
# 超过254个字符应使用TextField,对应MySQL中的longtext
description = models.TextField("简介", blank=True) # 可以为空字符串
register_date = models.DateField('注册日期', auto_now_add=True) # 首次添加时使用当前时间
update_time = models.DateTimeField('注册时间', auto_now=True) # 每次保存时使用当前时间
is_delete = models.BooleanField(default=False)
type_id = models.ForeignKey( # 外键
Type, # 主表
# related_name="blog_set", # 默认即为 子表的小写名称_set
on_delete=models.SET(cb), # 主表对应行删除时的子表行为
# on_delete=models.CASCADE,
null=True)
# models.FileField 用于接收上传的文件,其在数据库里只是一个路径字符串
# upload_to用于指定MEDIA_ROOT下的相对路径,也可以传入一个方法用于自定义保存逻辑。
# upload_file = models.FileField(upload_to="upload/") #指向 settings.MEDIA_ROOT 下的 upload 子目录
# upload_file = models.FileField(upload_to="upload/%Y/%m/%d")
upload_file = models.FileField(upload_to=upload_to, default="")
# 自定义实例print输出内容
def __str__(self):
return f"name={self.name}"
class Meta:
db_table = 'blog' # 通过db_table自定义数据表名
应用模型
- python manage.py makemigrations
查找所有(或指定)app中可用模型(model),创建迁移脚本(XXXX_initial.py)
python manage.py makemigrations
python manage.py makemigrations app_name
- python manage.py migrate
检查django_migrations
表,并运行其中不存在的_initial.py
脚本创建/修改表结构,并在django_migrations
表中插入一条记录。
python manage.py migrate
python manage.py migrate app_name
QuerySet
查询集,也称查询结果集,表示ORM从数据库中获取的对象(model实例)集合。
可迭代,可转为列表,也可以直接索引或切片,支持链式调用:
items = Blog.objects.all()
# 迭代
for item in items:
print(item)
# 转为列表
[item for item in items]
list(items)
# 索引、切片
items[0]
items[1:3]
model 实例对象
- 实例对象来源
对QuerySet调用get
、first
、last
、索引等方法,或直接索引取出
将QuerySet先转为列表,再从中取出
model类.objects.create
方法 或 直接实例化model类 创建 - 更新
修改属性并调用save
方法即可
注意,不能直接修改QuerySet
中的成员,应先将其重新赋值:
query_set = Blog.objects.all()
query_set[0].name="new" # 无效
query_set.first().name="new" # 无效
new_item = query_set[0]
new_item.name="new"
new_item.save()
- 删除
直接调用delete
方法即可
query_set = Blog.objects.all()
query_set[0].delete()
- 转化为dict
可以直接调用__dict__
属性,但会包含一些冗余字段。
更好的方法是使用django自带的model_to_dict(instance, fields=None, exclude=None)
方法,其中,fields指定需要哪些字段,exclude指定排除哪些字段,exclude比fields优先更级高。
from django.forms.models import model_to_dict
model_to_dict(Blog.objects.first(), exclude=['create_time', 'update_time'])
惰性查询
创建查询集本身不进行数据库访问,仅读取其值时才真正运行:
query_set = Book.objects.all() # 不会查询数据库
print(query_set) # 查询数据库
缓存机制
每个查询集都包含一个缓存来最小化对数据库的访问。
对原本的查询集执行filter
等操作会创建一个新的查询集,拥有独立的缓存空间。
首次对整个查询集求值时,发生数据库访问,其查询结果会存入缓存,后续对该查询集的取值直接从缓存中读取。
- 直接
print(query_set)
无需获知其完整细节,不会进入缓存,但可以读取缓存 - 对查询集索引或切片只是读取查询集中部分项,不会进入缓存,但可以读取缓存
- 布尔判断
直接对查询集进行如下布尔判断,会触发整个查询集求值:
bool(query_set)
if query_set:
...
可使用exist
方法节省性能,此时本质上只查了一条看是否存在,因此不会加入缓存:
if query_set.exists():# SELECT (1) AS `a` FROM `app01_book` LIMIT 1
...
- 迭代操作
如下,普通的迭代操作都会触发缓存:
[book for book in query_set]
bool(query_set)
any_obj in query_set
list(query_set)
当数据量过大时,为防止占用过多内存,可用iterator
迭代器。迭代器也会在第一次被调用时完成完整的查询,只是不会进入缓存:
objs = Book.objects.all().iterator()
for obj in objs:
print(obj.name)
# 强调:再次遍历没有打印,因为迭代器已经在上一次遍历(next)到最后一次了,没得遍历了
for obj in books:
print(obj.name)
- only 与 defer
only
方法指定加入缓存的字段,其他字段都不加入缓存
defer
方法指定不加入缓存的字段,其他字段都加入缓存
res = Book.objects.only('name')
for obj in res:
print(obj.name) # .name不走数据库 .其他字段会重新走数据库查询
objects 与 链式方法
models中的类均继承models.Model
,也继承了其 objects
属性用于管理模型。
其具有如下链式方法,如返回值是QuerySet则可以继续调用:
- .all()
查询所有结果,返回 QuerySet 实例 - .filter(**kwargs)
筛选符合条件的内容,返回 QuerySet 实例。
当无符合条件者时返回<QuerySet []>,该值转为布尔值时为False
(同空列表) - .exclude(**kwargs)
排除符合条件的内容,返回 QuerySet 实例。 - .distinct()
去重,返回 QuerySet 实例 - .order_by(*args)
排序,返回 QuerySet 实例。传入字段以-
开头则为倒序。 - .reverse()
倒序,返回 QuerySet 实例 - .count()
总数,返回 int - .first() / .last()
返回第一个 / 最后一个模型对象实例,无值则返回None - .get(**kwargs)
返回一个符合条件的模型对象实例,如无符合条件或有多个符合条件,都会报错 - .exists()
如果 QuerySet 包含数据,就返回 True,否则返回 False。 - .values(*args)
返回一个可迭代的字典序列,支持继续调用QuerySet方法。
默认包含所有字段,也可指定仅包含部分字段 - .values_list(*args)
类似values,返回不带key的元组序列
如有参数flat=True
则返回不带key的列表序列 - .aggregate()
实现SQL聚合函数,返回一个dict。
字典key若未指定则默认为字段__聚合函数名
def db_retrieve2(request):
vvs = Blog.objects.all().aggregate(Avg('age'), Sum('age'), count=Count('*'),
max_age=Max('age'), min_age=Min('age'))
return JsonResponse(vvs, json_dumps_params={'ensure_ascii': False})
- .annotate()
实现SQL的GROUP BY功能,返回 QuerySet 实例
如下,前置的values
用于指定group by的目标字段,默认为主键字段。
vvs = Blog.objects.values("age").annotate(Min('id'))
# SELECT `blog`.`age`, MIN(`blog`.`id`) AS `id__max` FROM `blog` GROUP BY `blog`.`age`
- .extra(select=None, where=None, params=None,
tables=None, order_by=None, select_params=None)
用于其他ORM语句满足不了时的复杂语句,注意防范SQL注入
## select提供简单数据
# SELECT age, (age > 18) as is_adult FROM myapp_person ORDER BY is_adult;
Person.objects.all().extra(select={'is_adult': "age > 18"}, order_by=['is_adult']) # 加在select后面
## where提供查询条件
# SELECT * FROM myapp_person WHERE first||last ILIKE 'jeffrey%';
Person.objects.all().extra(where=["first||last ILIKE 'jeffrey%'"]) # 加一个where条件
## table连接其它表
# SELECT * FROM myapp_book, myapp_person WHERE last = author_last
Book.objects.all().extra(table=['myapp_person'], where=['last = author_last']) # 加from后面
## params添参数
# !! 错误的方式 !!
first_name = 'Joe' # 如果first_name中有SQL特定字符就会出现漏洞
Person.objects.all().extra(where=["first = '%s'" % first_name])
# 正确方式
Person.objects.all().extra(where=["first = '%s'"], params=[first_name])
双下划线过滤参数
__exact 精确等于 (缺省值)
__iexact 精确等于 忽略大小写
__contains 包含(sqlite中,等同于icontains)
__icontains 包含 忽略大小写
__regex 正则
__iregex 正则 忽略大小写
__gt 大于
__gte 大于等于
__lt 小于
__lte 小于等于
__in 存在于一个 list 内
__startswith 以...开头
__istartswith 以...开头 忽略大小写
__endswith 以...结尾
__iendswith 以...结尾,忽略大小写
__range 在...范围内
__year 日期字段的年份
__month 日期字段的月份
__day 日期字段的日
__isnull 取 True 或 False,对应 IS NULL 和 IS NOT NULL 的 SQL 查询
Q 查询
使用符号&
表示与(或直接,
隔开),|
表示或,~
表示非,将多个Q对象进行组合,合并成一个新的Q对象。
Q对象和普通关键词参数共同使用时,应放在普通关键词参数前面
from django.db.models imports Q
News.objects.get(
~Q(question__startswith='Who'),
Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
is_delete=1)
)
F 查询
用于引用该model下的另一个字段
from django.db.models imports F
News.objects.filter(aaa__gt=F('bbb')*2)
跨表查询优化
- 外键关联查询
select_related(外键字段1, 外键字段2)
,底层为JOIN
prefetch_related(外键字段1, 外键字段2)
,底层为子查询
将当前表和外键关联表一次性都查出来,以免多次查询。通过使用双下划线“__”连接字段名还能实现指定的递归查询:
res = Book.objects.select_related('publish')
for obj in res:
print(obj.publish.name)
article= Article.objects.select_related("blog__user").get(nid=1)
print(article.blog.user.username)
增
可以直接调用create方法新增,也可以实例化model并在赋值后save。
两种方法返回值均为model的实例。
Blog.objects.create(name="VV", age=8, has_hair=False)
vv = {"name": "VV2", "age": 6, "has_hair": False}
Blog.objects.create(**vv)
instance = Blog(name = "VV", age=7)
instance.has_hair= False
instance.save()
删
queryset 和 实例 都具有 delete 方法
注意该方式是硬删除
Blog.objects.all().delete()
Blog.objects.first().delete()
r = Blog.objects.create(name="VV", age=8, has_hair=False)
r.delete()
改
queryset 具有 update 方法
实例 可以修改后调用 save 方法
Blog.objects.all().update(age=18)
Blog.objects.first().update(age=18)
r = Blog.objects.create(name="VV", age=8, has_hair=False)
r.age = 18
r.save()
查
通过上述QuerySet语法进行查询,注意缓存的使用即可。
Entry.objects.filter(is_delete=0,headline__startswith="What")
.exclude(pub_date__gte=datetime.date.today())
.order_by('-title', 'pub_date')
外键
创建
其中on_delete
可选的值包括:CASCADE、PROTECT、SET_NULL、SET_DEFAULT、SET()
# models.py
type_id = models.ForeignKey( # 外键
Type, # 主表
# related_name="blog_set", # 默认即为 子表的小写名称_set
on_delete=models.SET(cb), # 主表对应行删除时的子表行为
# on_delete=models.CASCADE,
null=True)
赋值
Django中,外键属性赋值时,需直接赋值所在行的实例对象:
def db_update_2(request):
vv = Blog.objects.first()
vv.type_id = Type.objects.get(id=1)
vv.save()
return JsonResponse(model_to_dict(vv), json_dumps_params={'ensure_ascii': False})
主表实例查看关联的子表QuerySet
默认可以通过主表实例.子表小写名称_set.all()
获取。
也可以在子表类中声明外键字段时,通过related_name
自定义名称。
type = Type.objects.first()
type.blog_set.all()
查看Django实际的数据库语句
from django.db import connection
print(connection.queries[-1]['sql'])