Django-数据库操作

你是这个发疯的世界上,唯一一个不疯的人,大家才会觉得你疯了。

使用不同的数据库

Django原生支持sqlite3,所以一开始小项目使用sqlite数据库差不多够用了,并且很方便。

当然你也可以使用不同的数据库,比如mysql,oracle,如果你想使用其他的数据库,请先安装相应的数据库操作模块,并将settings文件中DATABASES位置的’default’的键值进行相应的修改,用于连接你的数据库。

在 settings.py文件中,设置DATABASES的值来选择使用不同的数据库,比如使用mysql

在settings.py文件中:

import pymysql         # 一定要添加这两行!通过pip install pymysql!
pymysql.install_as_MySQLdb()
# 修改DATABASES的值
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'mysite',
        'HOST': '192.168.1.1',
        'USER': 'root',
        'PASSWORD': 'pwd',
        'PORT': '3306',
    }
}

这里需要注意的是,Django和 Flask中一样,并不会帮你创建数据库,只会创建数据库的字段,需要你手动创建数据库

mysql中创建数据库

CREATE DATABASE database_name;

顺手在settings.py文件中把时区修改一下:

TIME_ZONE = Asia/Shanghai

创建数据库

和SQLalchemy一样,直接在你的应用的models.py文件中可以编写创建了

from django.db import models
class UserInfo(models.Model):
    uid = models.AutoField(primary_key=True)
    # 自增长ID,必须带参数为primary_key=True,并且是唯一主键
    user = models.CharField(max_length=10)
    # 字符串类型,最大长度为10
    age = models.IntegerField(default=1)
    # 数字类型,默认为1
    sex = models.BooleanField()
    # BOOL类型
    email = models.EmailField()
    # 自带检查 Email 合法性的 CharField
    Intor = models.TextField()
    # 长文本类型
    Blog = models.URLField()
    # 自带检查 URL 合法性的 CharField

    # -------------------文件上传-----------------
    upload = models.FileField(upload_to='uploads/')
    # 文件上传到 MEDIA_ROOT/uploads
    # 这个字段不能设置primary_key和unique选项.在数据库中存储类型是varchar,默认最大长度为100.
    upload_s = models.FileField(upload_to='uploads/%Y/%m/%d/')
    # 文件上传到 MEDIA_ROOT/uploads/2019/07/04

    upload_e = models.FilePathField(path="/home/images", match="foo.*", recursive=True)
    # 这个字段的值被限制在系统上某个目录中的所有文件名集合中。有三个参数
    #     path = '':  该参数必需。上行所说的‘某个目录’的绝对路径。Example: "/home/images".
    #     match = 'pattern':  可选参数。格式是正则表达式。用来拣选符合匹配正则表达式的文件
    #     recursive = True / False: 可选参数,默认为False。设定是否递归该目录下所有子目录的所有文件

    add_date = models.DateTimeField(auto_now_add=True)
    # 设置初次创建的时间
    update_date = models.DateTimeField(auto_now=True)
    # 设置最后修改的时间

设置索引

唯一索引,设置括号内参数unqiue=Ture
普通索引,设置括号内参数db_index=Ture
主键索引,设置括号内参数primary_key=Ture

写好数据库的结构后,就可以开始创建了,注意你需要在更目录的settings.py的 INSTALLED_APPS 变量中把你的应用名字加载保存进去。

接下来就可以创建生成数据库迁移文件了,使用命令:

python3 .\manage.py makemigrations 

这个时候会发现你的应用下的文件夹 migrations 下面生成了一个文件 0001_initial.py

迁移文件:Django会数据库模型文件修改的检测,如果发生修改,那么把修改的地方完成一次迁移,相当于方便阅读的备份。

然后把迁移文件同步到数据库结构呢?使用命令

python3 manage.py migrate

这样就会把写完的数据库模型同步到数据库。

那么如何看到数据库同步的过程中发生了什么呢?

使用命令:

python3 manage.py sqlmigrate show 0001
# 这里的show是我创建的功能应用的名字,0001代表生成的第一个迁移文件

通过这样就能直接看到数据库执行了什么语句

总结一下,创建数据库一共需要三步:

  1. 修改models.py中模型
  2. 运行python3 manage.py makemigrations为改动创建迁移记录
  3. 运行python3 manage.py migrate,将操作同步到数据库。

数据操作

数据的操作增删改查可以说是SQL的灵魂核心,掌握熟练的SQL语句也是非常重要的,Django为了让我们少背一写SQL语句,很贴心的提供ORM直接操作数据的增删改查,有时候怕你嫌麻烦,更是贴心的直接给你提供了一个每个应用对应的管理界面

使用后台操作

首先需要创建一个管理员用户,使用命令

python3 manage.py createsuperuser

然后输入用户名

好贴心….

然后输入账号密码直接登陆了….

登陆地址为:

http://127.0.0.1/admin

可以看到有用户组可以直接添加管理员用户,他们由django.contrib.auth模块提供的身份认证框架。。

还可以修改后台登陆地址,在根目录下的urls.py

from django.contrib import admin
from django.urls import path
urlpatterns = [
    path('LetMeLOGIN/', admin.site.urls)
]

这样登陆地址就变成了:

http://127.0.0.1/LetMeLOGIN

这TM谁能猜到你的后地址啊,66

但是吧,现在后台界面啥都没有啊,我要管理我的数据啊,这个时候需要注册一下

在你的应用的目录下的admin.py文件中,修改内容:

from django.contrib import admin
from .models import UserInfo
# 导入你创建的模型的名字
admin.site.register(UserInfo)

然后刷新管理界面,就会发现你的数据库出现了

点进去看看

哇哇哇哇要哭了,简直简直太贴心了吧!!

在页面的底部,则是一些可选项按钮:

delete:弹出一个删除确认页面
save and add another:保存当前修改,并加载一个新的空白的当前类型对象的表单。
save and continue editing:保存当前修改,并重新加载该对象的编辑页面。
save:保存修改,返回当前对象类型的列表页面。    

数据增删改查

数据的添加方法:

data.objects.create(User='陈二狗')

或者稍微复杂一点

datas = data()
datas.User = '陈二狗'
datas.save()

常规的有5种方法获取数据

第一种是 data.objects.all()

这种方法获取的是这张表里面的所有数据

第二种是 data.objects.get(id=1)

这种方法必须确保获取的结果只有一个,不然会报错,获取的条件是id=1

第三种方法是 data.objects.filter(id=2)

这种方法是筛选出id=2的数据

第四种方法是 data.objects.exclude(id=2)

这种方法是获取到id不等于2的数据

第五种方法是 data.objects.order_by(name)

这种方法获取到的结果是按照name进行排序后的结果

以上方法获取的数据的结果是QuerySet()类型的数据,是一个结果类,想要获取最后的实际结果,需要使用索引获取

datas = data.objects.filter(id=2).values()[0]

这样的结果是一个字典,索引0即是获取values()列表的第一个结果

当然你也可以使用赋予变量的形式进行获取

datas = data.objects.get(id=1)
u_id = datas.id
u_User = datas.User

这种方式进行获取数据,这个前提是在只有一个数据的情况下进行

如果是all()或者其他方法获取较多的数据集时候,也可以这样获取每个结果

data_0 = data.objects.all()
# 这里的结果是一个数据集合,可以循环迭代和使用索引获取值
data_1 = data_0[0]
u_id = data_1.id
u_User = data_1.User
# 通过这样的方式,就可以实现进行单独数据的获取

像这种的集合,如果想要把里面的数据全都打印出来,可以这么处理

ret = ''
data_0 = data.objects.all()
for data_1 in data_0:
    ret += data_1.id + '|' + data_1.User + '<br>'

然后使用render在前端直接显示出来

如果要查询数据库的字段中包含的字符串,即SQL语句中的like,使用方法如下:

datas = data.objects.filter(User__cotains='陈')

相当于SQL语句 select * from data where User like ‘%陈%’

这种条件判断查询的方式很有趣,可以记录一下

User__contains = '陈'
# 模糊查询字段中带有 陈 的数据
# icontains 则不区分大小写
User__exact = '陈二狗'
# 精准查询字段中名字为 陈二狗 的的数据,相当于 User='陈二狗'
# iexact 则不区分大小写
User_startswith = '陈'
# 精准查询字段中名字以 陈 开头的数据
# 类似的还有eduswith,当然两者前头加上i则不区分大小写
User_isnull = False
# 找到User字段不为空的数据
User_in = [‘苗翠花’,'妙脆角','秒啊','陈二狗']
# 找到User字段中在这个列表内的数据

# 判断符 gt:大于  gte:大于等于  lt:小于 lte:小于等于

C_Date = data.objects.filter(UDate_date__year>2001)
# 找到时间的年份大于2001年之后的
C_Date = date.objects.filter(UDate_date__gt=date(1999,2,2))
# 找到时间在1992年2月2日之后的数据
# 注意时间日期 后面要加上 _date__year

数据的修改也很简单

users = data.objects.filter(User='陈二狗')
users.User = '浪子'
users.asve()

当然还可以这么做

data.objects.filter(User='陈二狗').update(User='浪子')

这条SQL语句简短而且执行速度要优于使用save的速度。

从SQL的执行情况来看,使用upate是要优于save方式的。

从使用情境上看,update更加适用于批量数据更新,而save则更适合当然也只适合做单条记录的数据更新操作了。

数据的删除也很简单,只需要在后面加上delete()即可

比如

data.objects.filter(User='陈二狗').delete()

外键关联的时候,如果删除则不做任何事
user = models.ForeignKey(User,on_delete=models.DO_NOTHING)

外部引用

在自己的第三方文件使用models

新建项目并且做好配置文件数据库生成后,可以按照如下配置使用

创建的项目名为 Lnew

创建的models的内容为:

from django.db import models
class News_Data(models.Model):
    id = models.AutoField(primary_key=True)
    user = models.CharField(max_length=8)
    title = models.CharField(max_length=30)
    content = models.TextField()
    datetime = models.DateTimeField(auto_now=True)

在项目Lnews的应用show目录下新建文件,内容为:

# coding:utf-8
import requests
requests.packages.urllib3.disable_warnings()
import re

import django
import os
import sys
pathname = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0,pathname)
sys.path.insert(0,os.path.abspath(os.path.join(pathname,'..')))
os.environ.setdefault("DJANGO_SETTINGS_MODULE","Lnews.settings")
django.setup()
from show.models import News_Data
def Get_Content_Of_Url(url):
    try:
    headers = {
        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
    r = requests.get(url, headers=headers, verify=False, timeout=5)
    encoding = 'utf-8'
    try:
        encoding = requests.utils.get_encodings_from_content(r.text)[0]
    except:
        pass
    content = r.content.decode(encoding, 'replace')
    return (content, r.status_code)
    except Exception as e:
    return ('langzi', 404)

def Get_Title_Of_Content(content):
    if content[1] != 404:
    Url_Title_pattern = re.compile('<a href="(http.*?)" target="_blank">(.*?)</a>',re.I|re.S)
    res = re.findall(Url_Title_pattern,content[0].strip().replace('\n',''))
    return res[0:-1]


All_Urls = ['https://www.xj.hk/?index-{}.htm'.format(page) for page in range(1,18)]
for url in All_Urls:
    CONTENT = Get_Content_Of_Url(url)
    result = Get_Title_Of_Content(CONTENT)
    for r in result:
    print('URL:{}'.format(r[0]))
    print('TITLE:{}'.format(r[1].strip().lstrip()))
    News_Data.objects.create(user='langzi',title=r[1].strip().lstrip(),content=r[0])

这样就能完成models的外部引用

转载地址

管理器

Django的models类中有Manager这个方法,名称管理器,是Django管理数据库操作的接口,可以理解为ORM操作数据库都是通过Manager提供的API实现,Manager提供比如常用的.all(),.filter()方法你可以重写这些方法,不过这里不多描述,具体可查官方文档

其他具体操作资料

以下操作资料来源于刘江博客,侵删

一些常用的内容需要自己记住,其他的不常用的需要时候查一查即可

查询操作是Django的ORM框架中最重要的内容之一。我们建立模型、保存数据为的就是在需要的时候可以查询得到数据。Django自动为所有的模型提供了一套完善、方便、高效的API,一些重要的,我们要背下来,一些不常用的,要有印象,使用的时候可以快速查找参考手册。

本节的内容基于如下的一个博客应用模型:

from django.db import models

class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

    def __str__(self):              # __unicode__ on Python 2
        return self.name

class Author(models.Model):
    name = models.CharField(max_length=200)
    email = models.EmailField()

    def __str__(self):              # __unicode__ on Python 2
        return self.name

class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateField()
    mod_date = models.DateField()
    authors = models.ManyToManyField(Author)
    n_comments = models.IntegerField()
    n_pingbacks = models.IntegerField()
    rating = models.IntegerField()

    def __str__(self):              # __unicode__ on Python 2
        return self.headline

一、创建对象

假设模型位于mysite/blog/models.py文件中,那么创建对象的方式如下:

from blog.models import Blog
b = Blog(name='Beatles Blog', tagline='All the latest Beatles news.')
b.save()

在后台,这会运行一条SQL的INSERT语句。如果你不显式地调用save()方法,Django不会立刻将该操作反映到数据库中。save()方法没有返回值,它可以接受一些额外的参数。

如果想要一行代码完成上面的操作,请使用creat()方法,它可以省略save的步骤:

b = Blog.objects.create(name='Beatles Blog', tagline='All the latest Beatles news.')

二、保存对象

使用save()方法,保存对数据库内已有对象的修改。例如如果已经存在b5对象在数据库内:

b5.name = 'New name'
b5.save()

在后台,这会运行一条SQL的UPDATE语句。如果你不显式地调用save()方法,Django不会立刻将该操作反映到数据库中。

  1. 保存外键和多对多字段
    保存一个外键字段和保存普通字段没什么区别,只是要注意值的类型要正确。下面的例子,有一个Entry的实例entry和一个Blog的实例cheese_blog,然后把cheese_blog作为值赋给了entry的blog属性,最后调用save方法进行保存。

    from blog.models import Entry
    entry = Entry.objects.get(pk=1)
    cheese_blog = Blog.objects.get(name=”Cheddar Talk”)
    entry.blog = cheese_blog
    entry.save()

多对多字段的保存稍微有点区别,需要调用一个add()方法,而不是直接给属性赋值,但它不需要调用save方法。如下例所示:

from blog.models import Author
joe = Author.objects.create(name="Joe")
entry.authors.add(joe)

在一行语句内,可以同时添加多个对象到多对多的字段,如下所示:

john = Author.objects.create(name="John")
paul = Author.objects.create(name="Paul")
george = Author.objects.create(name="George")
ringo = Author.objects.create(name="Ringo")
entry.authors.add(john, paul, george, ringo)

如果你指定或添加了错误类型的对象,Django会抛出异常。

三、检索对象

想要从数据库内检索对象,你需要基于模型类,通过管理器(Manager)构造一个查询结果集(QuerySet)。

每个QuerySet代表一些数据库对象的集合。它可以包含零个、一个或多个过滤器(filters)。Filters缩小查询结果的范围。在SQL语法中,一个QuerySet相当于一个SELECT语句,而filter则相当于WHERE或者LIMIT一类的子句。

通过模型的Manager获得QuerySet,每个模型至少具有一个Manager,默认情况下,它被称作objects,可以通过模型类直接调用它,但不能通过模型类的实例调用它,以此实现“表级别”操作和“记录级别”操作的强制分离。如下所示:

Blog.objects
<django.db.models.manager.Manager object at ...>
b = Blog(name='Foo', tagline='Bar')
b.objects
Traceback:
...
AttributeError: "Manager isn't accessible via Blog instances."
  1. 检索所有对象
    使用all()方法,可以获取某张表的所有记录。

    all_entries = Entry.objects.all()

  1. 过滤对象
    有两个方法可以用来过滤QuerySet的结果,分别是:

    filter(kwargs):返回一个根据指定参数查询出来的QuerySet
    exclude(
    kwargs):返回除了根据指定参数查询出来结果的QuerySet

其中,**kwargs参数的格式必须是Django设置的一些字段格式。

例如:

Entry.objects.filter(pub_date__year=2006)

它等同于:

Entry.objects.all().filter(pub_date__year=2006)

链式过滤

filter和exclude的结果依然是个QuerySet,因此它可以继续被filter和exclude,这就形成了链式过滤:

Entry.objects.filter(
...     headline__startswith='What'
... ).exclude(
...     pub_date__gte=datetime.date.today()
... ).filter(
...     pub_date__gte=datetime(2005, 1, 30)
... )

(这里需要注意的是,当在进行跨关系的链式过滤时,结果可能和你想象的不一样,参考下面的跨多值关系查询)

被过滤的QuerySets都是唯一的

每一次过滤,你都会获得一个全新的QuerySet,它和之前的QuerySet没有任何关系,可以完全独立的被保存,使用和重用。例如:

q1 = Entry.objects.filter(headline__startswith="What")
q2 = q1.exclude(pub_date__gte=datetime.date.today())
q3 = q1.filter(pub_date__gte=datetime.date.today())

例子中的q2和q3虽然由q1得来,是q1的子集,但是都是独立自主存在的。同样q1也不会受到q2和q3的影响。

QuerySets都是懒惰的

一个创建QuerySets的动作不会立刻导致任何的数据库行为。你可以不断地进行filter动作一整天,Django不会运行任何实际的数据库查询动作,直到QuerySets被提交(evaluated)。

简而言之就是,只有碰到某些特定的操作,Django才会将所有的操作体现到数据库内,否则它们只是保存在内存和Django的层面中。这是一种提高数据库查询效率,减少操作次数的优化设计。看下面的例子:

q = Entry.objects.filter(headline__startswith="What")
q = q.filter(pub_date__lte=datetime.date.today())
q = q.exclude(body_text__icontains="food")
print(q)

上面的例子,看起来执行了3次数据库访问,实际上只是在print语句时才执行1次访问。通常情况,QuerySets的检索不会立刻执行实际的数据库查询操作,直到出现类似print的请求,也就是所谓的evaluated。

  1. 检索单一对象
    filter方法始终返回的是QuerySets,那怕只有一个对象符合过滤条件,返回的也是包含一个对象的QuerySets,这是一个集合类型对象,你可以简单的理解为Python列表,可迭代可循环可索引。

如果你确定你的检索只会获得一个对象,那么你可以使用Manager的get()方法来直接返回这个对象。

one_entry = Entry.objects.get(pk=1)

在get方法中你可以使用任何filter方法中的查询参数,用法也是一模一样。

注意:使用get()方法和使用filter()方法然后通过[0]的方式分片,有着不同的地方。看似两者都是获取单一对象。但是,如果在查询时没有匹配到对象,那么get()方法将抛出DoesNotExist异常。这个异常是模型类的一个属性,在上面的例子中,如果不存在主键为1的Entry对象,那么Django将抛出Entry.DoesNotExist异常。

类似地,在使用get()方法查询时,如果结果超过1个,则会抛出MultipleObjectsReturned异常,这个异常也是模型类的一个属性。

所以:get()方法要慎用!

  1. 其它QuerySet方法
    大多数情况下,需要从数据库中查找对象时,使用all()、 get()、filter() 和exclude()就行。针对QuerySet的方法还有很多,都是一些相对高级的用法。

  2. QuerySet使用限制
    使用类似Python对列表进行切片的方法可以对QuerySet进行范围取值。它相当于SQL语句中的LIMIT和OFFSET子句。参考下面的例子:

    Entry.objects.all()[:5] # 返回前5个对象
    Entry.objects.all()[5:10] # 返回第6个到第10个对象
    注意:不支持负索引!例如 Entry.objects.all()[-1]是不允许的

通常情况,切片操作会返回一个新的QuerySet,并且不会被立刻执行。但是有一个例外,那就是指定步长的时候,查询操作会立刻在数据库内执行,如下:

Entry.objects.all()[:10:2]
>

若要获取单一的对象而不是一个列表(例如,SELECT foo FROM bar LIMIT 1),可以简单地使用索引而不是切片。例如,下面的语句返回数据库中根据标题排序后的第一条Entry:

Entry.objects.order_by('headline')[0]

它相当于:

Entry.objects.order_by('headline')[0:1].get()

注意:如果没有匹配到对象,那么第一种方法会抛出IndexError异常,而第二种方式会抛出DoesNotExist异常。

也就是说在使用get和切片的时候,要注意查询结果的元素个数。

  1. 字段查询
    字段查询其实就是filter()、exclude()和get()等方法的关键字参数。 其基本格式是:field__lookuptype=value,注意其中是双下划线。 例如:

    Entry.objects.filter(pub_date__lte=’2006-01-01’)

相当于:

SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';

其中的字段必须是模型中定义的字段之一。但是有一个例外,那就是ForeignKey字段,你可以为其添加一个“_id”后缀(单下划线)。这种情况下键值是外键模型的主键原生值。例如:

Entry.objects.filter(blog_id=4)

如果你传递了一个非法的键值,查询函数会抛出TypeError异常。

Django的数据库API支持20多种查询类型,下面介绍一些常用的:

exact:

默认类型。如果你不提供查询类型,或者关键字参数不包含一个双下划线,那么查询类型就是这个默认的exact。

Entry.objects.get(headline__exact="Cat bites dog")
# 相当于
# SELECT ... WHERE headline = 'Cat bites dog';
# 下面两个相当
Blog.objects.get(id__exact=14)  # Explicit form
Blog.objects.get(id=14)         # __exact is implied
iexact:

不区分大小写。

Blog.objects.get(name__iexact="beatles blog")
# 匹配"Beatles Blog", "beatles blog",甚至"BeAtlES blOG".
contains:

表示包含的意思!大小写敏感!

Entry.objects.get(headline__contains='Lennon')
# 相当于
# SELECT ... WHERE headline LIKE '%Lennon%';
# 匹配'Today Lennon honored',但不匹配'today lennon honored'
icontains:

contains的大小写不敏感模式。

startswith和endswith

以什么开头和以什么结尾。大小写敏感!

istartswith和iendswith

是不区分大小写的模式。

  1. 跨越关系查询

Django提供了强大并且直观的方式解决跨越关联的查询,它在后台自动执行包含JOIN的SQL语句。要跨越某个关联,只需使用关联的模型字段名称,并使用双下划线分隔,直至你想要的字段(可以链式跨越,无限跨度)。例如:

返回所有Blog的name为’Beatles Blog’的Entry对象

一定要注意,返回的是Entry对象,而不是Blog对象。

objects前面用的是哪个class,返回的就是哪个class的对象。

Entry.objects.filter(blog__name='Beatles Blog')
反之亦然,如果要引用一个反向关联,只需要使用模型的小写名!

获取所有的Blog对象,前提是它所关联的Entry的headline包含’Lennon’

Blog.objects.filter(entry__headline__contains='Lennon')

如果你在多级关联中进行过滤而且其中某个中间模型没有满足过滤条件的值,Django将把它当做一个空的(所有的值都为NULL)但是合法的对象,不会抛出任何异常或错误。例如,在下面的过滤器中:

Blog.objects.filter(entry__authors__name='Lennon')

如果Entry中没有关联任何的author,那么它将当作其没有name,而不会因为没有author 引发一个错误。通常,这是比较符合逻辑的处理方式。唯一可能让你困惑的是当你使用isnull的时候:

Blog.objects.filter(entry__authors__name__isnull=True)

这将返回Blog对象,它关联的entry对象的author字段的name字段为空,以及Entry对象的author字段为空。如果你不需要后者,你可以这样写:

Blog.objects.filter(entry__authors__isnull=False,entry__authors__name__isnull=True)

跨越多值的关系查询

最基本的filter和exclude的关键字参数只有一个,这种情况很好理解。但是当关键字参数有多个,且是跨越外键或者多对多的情况下,那么就比较复杂,让人迷惑了。我们看下面的例子:

Blog.objects.filter(entry__headline\_\_contains='Lennon', entry__pub_date__year=2008)

这是一个跨外键、两个过滤参数的查询。此时我们理解两个参数之间属于-与“and”的关系,也就是说,过滤出来的BLog对象对应的entry对象必须同时满足上面两个条件。这点很好理解。也就是说上面要求至少有一个entry同时满足两个条件。

但是,看下面的用法:

Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)

把两个参数拆开,放在两个filter调用里面,按照我们前面说过的链式过滤,这个结果应该和上面的例子一样。可实际上,它不一样,Django在这种情况下,将两个filter之间的关系设计为-或“or”,这真是让人头疼。

多对多关系下的多值查询和外键foreignkey的情况一样。

但是,更头疼的来了,exclude的策略设计的又和filter不一样!

Blog.objects.exclude(entry__headline__contains='Lennon',entry__pub_date__year=2008,)
这会排除headline中包含“Lennon”的Entry和在2008年发布的Entry,中间是一个-和“or”的关系!

那么要排除同时满足上面两个条件的对象,该怎么办呢?看下面:

Blog.objects.exclude(
entry=Entry.objects.filter(
    headline__contains='Lennon',
    pub_date__year=2008,
),
)

(有没有很坑爹的感觉?所以,建议在碰到跨关系的多值查询时,尽量使用Q查询)

  1. 使用F表达式引用模型的字段
    到目前为止的例子中,我们都是将模型字段与常量进行比较。但是,如果你想将模型的一个字段与同一个模型的另外一个字段进行比较该怎么办?

使用Django提供的F表达式!

例如,为了查找comments数目多于pingbacks数目的Entry,可以构造一个F()对象来引用pingback数目,并在查询中使用该F()对象:

from django.db.models import F
Entry.objects.filter(n_comments__gt=F('n_pingbacks'))

Django支持对F()对象进行加、减、乘、除、取模以及幂运算等算术操作。两个操作数可以是常数和其它F()对象。例如查找comments数目比pingbacks两倍还要多的Entry,我们可以这么写:

Entry.objects.filter(n_comments__gt=F('n_pingbacks') * 2)

为了查询rating比pingback和comment数目总和要小的Entry,我们可以这么写:

Entry.objects.filter(rating__lt=F('n_comments') + F('n_pingbacks'))

你还可以在F()中使用双下划线来进行跨表查询。例如,查询author的名字与blog名字相同的Entry:

Entry.objects.filter(authors__name=F('blog__name'))

对于date和date/time字段,还可以加或减去一个timedelta对象。下面的例子将返回发布时间超过3天后被修改的所有Entry:

from datetime import timedelta
Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3))
F()对象还支持.bitand()、.bitor()、.bitrightshift()和.bitleftshift()4种位操作,例如:

F('somefield').bitand(16)
  1. 主键的快捷查询方式:pk
    pk就是primary key的缩写。通常情况下,一个模型的主键为“id”,所以下面三个语句的效果一样:

    Blog.objects.get(id__exact=14)
    Blog.objects.get(id=14)
    Blog.objects.get(pk=14)

可以联合其他类型的参数:

Get blogs entries with id 1, 4 and 7

Blog.objects.filter(pk__in=[1,4,7])

Get all blog entries with id > 14

Blog.objects.filter(pk__gt=14)

可以跨表操作:

Entry.objects.filter(blog__id__exact=3) 
Entry.objects.filter(blog__id=3) 
Entry.objects.filter(blog__pk=3)

(当主键不是id的时候,请注意了!)

  1. 在LIKE语句中转义百分符号和下划线
    在原生SQL语句中%符号有特殊的作用。Django帮你自动转义了百分符号和下划线,你可以和普通字符一样使用它们,如下所示:

    Entry.objects.filter(headline__contains=’%’)

它和下面的一样

SELECT ... WHERE headline LIKE '%\%%';
  1. 缓存与查询集
    每个QuerySet都包含一个缓存,用于减少对数据库的实际操作。理解这个概念,有助于你提高查询效率。

对于新创建的QuerySet,它的缓存是空的。当QuerySet第一次被提交后,数据库执行实际的查询操作,Django会把查询的结果保存在QuerySet的缓存内,随后的对于该QuerySet的提交将重用这个缓存的数据。

要想高效的利用查询结果,降低数据库负载,你必须善于利用缓存。看下面的例子,这会造成2次实际的数据库操作,加倍数据库的负载,同时由于时间差的问题,可能在两次操作之间数据被删除或修改或添加,导致脏数据的问题:

print([e.headline for e in Entry.objects.all()])
print([e.pub_date for e in Entry.objects.all()])

为了避免上面的问题,好的使用方式如下,这只产生一次实际的查询操作,并且保持了数据的一致性:

queryset = Entry.objects.all()
print([p.headline for p in queryset]) # 提交查询
print([p.pub_date for p in queryset]) # 重用查询缓存

何时不会被缓存

有一些操作不会缓存QuerySet,例如切片和索引。这就导致这些操作没有缓存可用,每次都会执行实际的数据库查询操作。例如:

queryset = Entry.objects.all()
print(queryset[5]) # 查询数据库
print(queryset[5]) # 再次查询数据库

但是,如果已经遍历过整个QuerySet,那么就相当于缓存过,后续的操作则会使用缓存,例如:

queryset = Entry.objects.all()
[entry for entry in queryset] # 查询数据库
print(queryset[5]) # 使用缓存
print(queryset[5]) # 使用缓存

下面的这些操作都将遍历QuerySet并建立缓存:

[entry for entry in queryset]
bool(queryset)
entry in queryset
list(queryset)

注意:简单的打印QuerySet并不会建立缓存,因为__repr__()调用只返回全部查询集的一个切片。

四、使用Q对象进行复杂查询

普通filter函数里的条件都是“and”逻辑,如果你想实现“or”逻辑怎么办?用Q查询!

Q来自django.db.models.Q,用于封装关键字参数的集合,可以作为关键字参数用于filter、exclude和get等函数。 例如:

from django.db.models import Q
Q(question__startswith='What')

可以使用“&”或者“|”或“~”来组合Q对象,分别表示与或非逻辑。它将返回一个新的Q对象。

Q(question__startswith='Who')|Q(question__startswith='What')
# 这相当于:
WHERE question LIKE 'Who%' OR question LIKE 'What%'

更多的例子:

Q(question__startswith='Who') | ~Q(pub_date__year=2005)

你也可以这么使用,默认情况下,以逗号分隔的都表示AND关系:

Poll.objects.get(
Q(question__startswith='Who'),
Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
)
# 它相当于
# SELECT * from polls WHERE question LIKE 'Who%'
AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06')

当关键字参数和Q对象组合使用时,Q对象必须放在前面,如下例子:

Poll.objects.get(
Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),question__startswith='Who',)

如果关键字参数放在Q对象的前面,则会报错。

五、比较对象

要比较两个模型实例,只需要使用python提供的双等号比较符就可以了。在后台,其实比较的是两个实例的主键的值。下面两种方法是等同的:

some_entry == other_entry
some_entry.id == other_entry.id

如果模型的主键不叫做“id”也没关系,后台总是会使用正确的主键名字进行比较,例如,如果一个模型的主键的名字是“name”,那么下面是相等的:

some_obj == other_obj
some_obj.name == other_obj.name

六、删除对象

删除对象使用的是对象的delete()方法。该方法将返回被删除对象的总数量和一个字典,字典包含了每种被删除对象的类型和该类型的数量。如下所示:

e.delete()
(1, {'weblog.Entry': 1})

也可以批量删除。每个QuerySet都有一个delete()方法,它能删除该QuerySet的所有成员。例如:

Entry.objects.filter(pub_date__year=2005).delete()
(5, {'webapp.Entry': 5})

需要注意的是,有可能不是每一个对象的delete方法都被执行。如果你改写了delete方法,为了确保对象被删除,你必须手动迭代QuerySet进行逐一删除操作。

当Django删除一个对象时,它默认使用SQL的ON DELETE CASCADE约束,也就是说,任何有外键指向要删除对象的对象将一起被删除。例如:

b = Blog.objects.get(pk=1)
# 下面的动作将删除该条Blog和所有的它关联的Entry对象
b.delete()
这种级联的行为可以通过的ForeignKey的on_delete参数自定义。

注意,delete()是唯一没有在管理器上暴露出来的方法。这是刻意设计的一个安全机制,用来防止你意外地请求类似Entry.objects.delete()的动作,而不慎删除了所有的条目。如果你确实想删除所有的对象,你必须明确地请求一个完全的查询集,像下面这样:

Entry.objects.all().delete()

七、复制模型实例

虽然没有内置的方法用于复制模型的实例,但还是很容易创建一个新的实例并将原实例的所有字段都拷贝过来。最简单的方法是将原实例的pk设置为None,这会创建一个新的实例copy。示例如下:

blog = Blog(name='My blog', tagline='Blogging is easy')
blog.save() # blog.pk == 1
#
blog.pk = None
blog.save() # blog.pk == 2

但是在使用继承的时候,情况会变得复杂,如果有下面一个Blog的子类:

class ThemeBlog(Blog):
    theme = models.CharField(max_length=200)

django_blog = ThemeBlog(name='Django', tagline='Django is easy', theme='python')
django_blog.save() # django_blog.pk == 3

基于继承的工作机制,你必须同时将pk和id设为None:

django_blog.pk = None
django_blog.id = None
django_blog.save() # django_blog.pk == 4

对于外键和多对多关系,更需要进一步处理。例如,Entry有一个ManyToManyField到Author。 复制条目后,您必须为新条目设置多对多关系,像下面这样:

entry = Entry.objects.all()[0] # some previous entry
old_authors = entry.authors.all()
entry.pk = None
entry.save()
entry.authors.set(old_authors)

对于OneToOneField,还要复制相关对象并将其分配给新对象的字段,以避免违反一对一唯一约束。 例如,假设entry已经如上所述重复:

detail = EntryDetail.objects.all()[0]
detail.pk = None
detail.entry = entry
detail.save()

八、批量更新对象

使用update()方法可以批量为QuerySet中所有的对象进行更新操作。

# 更新所有2007年发布的entry的headline
Entry.objects.filter(pub_date\_\_year=2007).update(headline='Everything is the same')

只可以对普通字段和ForeignKey字段使用这个方法。若要更新一个普通字段,只需提供一个新的常数值。若要更新ForeignKey字段,需设置新值为你想指向的新模型实例。例如:

b = Blog.objects.get(pk=1)
# 修改所有的Entry,让他们都属于b
Entry.objects.all().update(blog=b)

update方法会被立刻执行,并返回操作匹配到的行的数目(有可能不等于要更新的行的数量,因为有些行可能已经有这个新值了)。唯一的约束是:只能访问一张数据库表。你可以根据关系字段进行过滤,但你只能更新模型主表的字段。例如:

b = Blog.objects.get(pk=1)
# Update all the headlines belonging to this Blog.
Entry.objects.select_related().filter(blog=b).update(headline='Everything is the same')

要注意的是update()方法会直接转换成一个SQL语句,并立刻批量执行。它不会运行模型的save()方法,或者产生pre_save或post_save信号(调用save()方法产生)或者服从auto_now字段选项。如果你想保存QuerySet中的每个条目并确保每个实例的save()方法都被调用,你不需要使用任何特殊的函数来处理。只需要迭代它们并调用save()方法:

for item in my_queryset:
    item.save()

update方法可以配合F表达式。这对于批量更新同一模型中某个字段特别有用。例如增加Blog中每个Entry的pingback个数:

Entry.objects.all().update(n_pingbacks=F('n_pingbacks') + 1)

然而,与filter和exclude子句中的F()对象不同,在update中你不可以使用F()对象进行跨表操作,你只可以引用正在更新的模型的字段。如果你尝试使用F()对象引入另外一张表的字段,将抛出FieldError异常:

# THIS WILL RAISE A FieldError
Entry.objects.update(headline=F('blog__name'))

九、关系的对象

利用本节一开始的模型,一个Entry对象e可以通过blog属性e.blog获取关联的Blog对象。反过来,Blog对象b可以通过entry_set属性b.entry_set.all()访问与它关联的所有Entry对象。

  1. 一对多(外键)
    正向查询:

直接通过圆点加属性,访问外键对象:

e = Entry.objects.get(id=2)
e.blog # 返回关联的Blog对象

要注意的是,对外键的修改,必须调用save方法进行保存,例如:

e = Entry.objects.get(id=2)
e.blog = some_blog
e.save()

如果一个外键字段设置有null=True属性,那么可以通过给该字段赋值为None的方法移除外键值:

e = Entry.objects.get(id=2)
e.blog = None
e.save() # "UPDATE blog_entry SET blog_id = NULL ...;"

在第一次对一个外键关系进行正向访问的时候,关系对象会被缓存。随后对同样外键关系对象的访问会使用这个缓存,例如:

e = Entry.objects.get(id=2)
print(e.blog)  # 访问数据库,获取实际数据
print(e.blog)  # 不会访问数据库,直接使用缓存的版本

请注意QuerySet的select_related()方法会递归地预填充所有的一对多关系到缓存中。例如:

e = Entry.objects.select_related().get(id=2)
print(e.blog)  # 不会访问数据库,直接使用缓存
print(e.blog)  # 不会访问数据库,直接使用缓存

反向查询:

如果一个模型有ForeignKey,那么该ForeignKey所指向的外键模型的实例可以通过一个管理器进行反向查询,返回源模型的所有实例。默认情况下,这个管理器的名字为FOO_set,其中FOO是源模型的小写名称。该管理器返回的查询集可以用前面提到的方式进行过滤和操作。

b = Blog.objects.get(id=1)
b.entry_set.all() # Returns all Entry objects related to Blog.
# b.entry_set is a Manager that returns QuerySets.
b.entry_set.filter(headline__contains='Lennon')
b.entry_set.count()

你可以在ForeignKey字段的定义中,通过设置related_name来重写FOO_set的名字。举例说明,如果你修改Entry模型blog = ForeignKey(Blog, on_delete=models.CASCADE, related_name=’entries’),那么上面的例子会变成下面的样子:

b = Blog.objects.get(id=1)
b.entries.all() # Returns all Entry objects related to Blog.
# b.entries is a Manager that returns QuerySets.
b.entries.filter(headline__contains='Lennon')
b.entries.count()

使用自定义的反向管理器:

默认情况下,用于反向关联的RelatedManager是该模型默认管理器的子类。如果你想为一个查询指定一个不同的管理器,你可以使用下面的语法:

from django.db import models
class Entry(models.Model):
    #...
    objects = models.Manager()  # 默认管理器
    entries = EntryManager()    # 自定义管理器

b = Blog.objects.get(id=1)
b.entry_set(manager='entries').all()

当然,指定的自定义反向管理器也可以调用它的自定义方法:

b.entry_set(manager='entries').is_published()

处理关联对象的其它方法:

除了在前面定义的QuerySet方法之外,ForeignKey管理器还有其它方法用于处理关联的对象集合。下面是每个方法的概括。

add(obj1, obj2, …):添加指定的模型对象到关联的对象集中。

create(**kwargs):创建一个新的对象,将它保存并放在关联的对象集中。返回新创建的对象。

remove(obj1, obj2, …):从关联的对象集中删除指定的模型对象。

clear():清空关联的对象集。

set(objs):重置关联的对象集。

若要一次性给关联的对象集赋值,使用set()方法,并给它赋值一个可迭代的对象集合或者一个主键值的列表。例如:

b = Blog.objects.get(id=1)
b.entry_set.set([e1, e2])

在这个例子中,e1和e2可以是完整的Entry实例,也可以是整数的主键值。

如果clear()方法可用,那么在将可迭代对象中的成员添加到集合中之前,将从entry_set中删除所有已经存在的对象。如果clear()方法不可用,那么将直接添加可迭代对象中的成员而不会删除所有已存在的对象。

这节中的每个反向操作都将立即在数据库内执行。所有的增加、创建和删除操作也将立刻自动地保存到数据库内。

  1. 多对多
    多对多关系的两端都会自动获得访问另一端的API。这些API的工作方式与前面提到的“反向”一对多关系的用法一样。

唯一的区别在于属性的名称:定义ManyToManyField的模型使用该字段的属性名称,而“反向”模型使用源模型的小写名称加上’_set’ (和一对多关系一样)。

e = Entry.objects.get(id=3)
e.authors.all() # Returns all Author objects for this Entry.
e.authors.count()
e.authors.filter(name__contains='John')
a = Author.objects.get(id=5)
a.entry_set.all() # Returns all Entry objects for this Author.
与外键字段中一样,在多对多的字段中也可以指定related_name名。

(注:在一个模型中,如果存在多个外键或多对多的关系指向同一个外部模型,必须给他们分别加上不同的related_name,用于反向查询)

  1. 一对一
    一对一非常类似多对一关系,可以简单的通过模型的属性访问关联的模型。

    class EntryDetail(models.Model):

    entry = models.OneToOneField(Entry, on_delete=models.CASCADE)
    details = models.TextField()
    

    ed = EntryDetail.objects.get(id=2)
    ed.entry # Returns the related Entry object.

不同之处在于反向查询的时候。一对一关系中的关联模型同样具有一个管理器对象,但是该管理器表示一个单一的对象而不是对象的集合:

e = Entry.objects.get(id=2)
e.entrydetail # 返回关联的EntryDetail对象

如果没有对象赋值给这个关系,Django将抛出一个DoesNotExist异常。 可以给反向关联进行赋值,方法和正向的关联一样:

e.entrydetail = ed

  1. 反向关联是如何实现的?
    一些ORM框架需要你在关系的两端都进行定义。Django的开发者认为这违反了DRY (Don’t Repeat Yourself)原则,所以在Django中你只需要在一端进行定义。

那么这是怎么实现的呢?因为在关联的模型类没有被加载之前,一个模型类根本不知道有哪些类和它关联。

答案在app registry!在Django启动的时候,它会导入所有INSTALLED_APPS中的应用和每个应用中的模型模块。每创建一个新的模型时,Django会自动添加反向的关系到所有关联的模型。如果关联的模型还没有导入,Django将保存关联的记录并在关联的模型导入时添加这些关系。

由于这个原因,将模型所在的应用都定义在INSTALLED_APPS的应用列表中就显得特别重要。否则,反向关联将不能正确工作。

  1. 通过关联对象进行查询
    涉及关联对象的查询与正常值的字段查询遵循同样的规则。当你指定查询需要匹配的值时,你可以使用一个对象实例或者对象的主键值。

例如,如果你有一个id=5的Blog对象b,下面的三个查询将是完全一样的:

Entry.objects.filter(blog=b) # 使用对象实例
Entry.objects.filter(blog=b.id) # 使用实例的id
Entry.objects.filter(blog=5) # 直接使用id

十、使用原生SQL语句

如果你发现需要编写的Django查询语句太复杂,你可以回归到手工编写SQL语句。Django对于编写原生的SQL查询有许多选项。

最后,需要注意的是Django的数据库层只是一个数据库接口。你可以利用其它的工具、编程语言或数据库框架来访问数据库,Django没有强制指定你非要使用它的某个功能或模块。

坚持原创技术分享,您的支持将鼓励我继续创作!
------ 本文结束 ------

版权声明

LangZi_Blog's by Jy Xie is licensed under a Creative Commons BY-NC-ND 4.0 International License
由浪子LangZi创作并维护的Langzi_Blog's博客采用创作共用保留署名-非商业-禁止演绎4.0国际许可证
本文首发于Langzi_Blog's 博客( http://langzi.fun ),版权所有,侵权必究。

0%