django之模型层(models)

django之模型层(models)

模型层搭建

模型层连接数据库

django默认连接sqlite3,但是这个数据库对时间字段不敏感,所以一般还是选择别的数据库测试。

一般是选择常见的数据库mysql去连接,而这是我们就需要更改一些设置:

# 修改配置文件
DATABASES = {
    'default': {
            'ENGINE': 'django.db.backends.mysql',  # django后端数据引擎
            'NAME': 'navicat_test1',  # mysql这个属性用数据库即可
            'HOST': '127.0.0.1',  # ip
            'PORT': 3306,
            'USER': 'root',  # mysql用户名,当然一般的项目中你都不是root
            'PASSWORD': '111',
            'CHARSET': 'utf8'
        }
}

ps:第一次连接mysql数据库时可能会报错,需要按照提示下载模块,连接mysql需要下载mysqlclient

模型层编写模型类

我们可以通过ORM来使用类和对象映射我们的表和记录,而这些对应表的类就称作模型类,它一开始存储在我们的models.py文件中,由我们自己编写:

from django.db import models

# Create your models here.

class UserInfo(models.Model):
    # 字段名 = 字段类型 + 约束条件
    id = models.AutoField(primary_key=True)  # 主键类型
    name = models.CharField(max_length=32)   # 字符字段
    age = models.IntegerField()  # 整型字段

这个类所对应的表,其大致属性为:表名-UserInfo,含三个字段,其中有一个主键自增字段。

我们在终端中运行manage命令(可以通过IDE的tools找到直接运行manage.py的环境)

image

python38 manage.py makemigrations  # 记录数据库相关
python38 manage.py migrate  # 将操作同步到数据库
# 第一次会创建很多表、其中一个是当前新增的模型表

# 如果在manage.py task环境下则只需要输入命令即可,而且还会补全
manage.py@jicheng > makemigrations
manage.py@jicheng > migrate

独立测试环境搭建

默认不允许单独测试某个py文件,尤其我们测试模型层时,为了测试模型数据而搭建一整个项目太过麻烦。

我们可以通过pycharm console即时测试(无法保存代码)。

也可以自行搭建

我们可以在它默认提供的tests.py(或者我们自己在相同位置自己编写一个py文件也可)中编写:

import os

def main():
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'jicheng.settings')  # 添加了环境
    import django
    django.setup()
    -测试代码-

if __name__ == '__main__':
    main()

这样我们就可以在tests.py文件中测试代码了。

ORM的基础使用

查看ORM的底层sql

  • orm查询语句一般都会返回一个QuerySet对象,这个对象的query属性记录了这次的sql语句

    import os
    
    def main():
        os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'jicheng.settings')  # 添加了环境
        import django
        django.setup()
        res = models.UserInfo.objects.filter()
        print(res.query)
    
    if __name__ == '__main__':
        main()
        
    # 运行结果:
    # SELECT `app01_userinfo`.`id`, `app01_userinfo`.`name`, `app01_userinfo`.`age` FROM `app01_userinfo`
    

    注:有些orm方法返回的并不是QuerySet对象,并不能使用这种方法

  • 在配置中添加以下代码,那么终端就会在每次数据操作时在终端显示sql语句:

    终端打印sql配置
    LOGGING = {
        'version': 1,
        'disable_existing_loggers': False,
        'handlers': {
            'console':{
                'level':'DEBUG',
                'class':'logging.StreamHandler',
            },
        },
        'loggers': {
            'django.db.backends': {
                'handlers': ['console'],
                'propagate': True,
                'level':'DEBUG',
            },
        }
    }
    

    image

ORM基础方法

基础方法总结

增删改查

方法 返回值
create(字段名=数据) 刚创建的数据记录对象
filter(筛选条件) QuerySet列表对象
filter().update(修改内容) 受影响的行数
filter().delete() 受影响的行数即各表受影响的行数

返回QuerySet对象的方法(大多通过模型类.objects.方法调用)

QuerySet对象形似存储了一个个记录对象的列表,但拥有一些特殊的属性,如query。

方法 含义 补充
all() 拿到表中所有记录
filter() 拿到表中所有符合条件的记录 括号中需要写筛选条件
exclude() 拿到表中所有不符合条件的记录 括号中需要写筛选条件
order_by() 按照某个字段进行排序 括号中需要写字段名称
reverse() 将排序好的字段反转排序 只能对排过序的QuerySet对象操作
distinct() 对QuerySet对象进行去重 一般搭配value(*field)使用,传入筛选过字段的记录集合QuerySet对象
raw(sql) 执行原生的sql语句 注意实际的表名与模型表的类名区别

返回特殊QuerySet对象

方法 含义 返回值
values() 筛选字段 <QuerySet[{},{},{}]>内部是字典
values_list() 筛选字段 <QuerySet[(),(),()]>内部是元组

返回具体记录对象

方法 含义 返回值
create(**kwargs) 向表中插入记录表 刚创建的记录对象
QuerySet对象.first() 拿到结果的第一个记录 如果没有返回None
QuerySet对象.last() 拿到结果的最后一个记录 如果没有返回None
QuerySet对象.get(筛选条件) 拿到结果中符合条件的记录 如果没有符合条件则报错

具体测试

  1. 模型类.objects.create(字段名=值1,...)

    res = models.UserInfo.objects.create(name='jason', age=28)
    print(res)
    print(res.name)
    print(res.age)
    

    image

  2. 模型类.objects.filter(筛选条件).update(字段名=值1,...)

    res = models.UserInfo.objects.filter(name="jason").update(name='zhang', age=38)
    print(res)  # 1   返回受影响的行数
    
  3. 模型类.objects.filter(筛选条件).delete(字段名=值1,...)

    res = models.UserInfo.objects.filter().delete()
    print(res)  # (3, {'app01.UserInfo': 3})   返回受影响的行数和每张表受影响的行数
    
  4. 模型类.objects.all()

    res = models.UserInfo.objects.all()

    image

  5. 模型类.objects.filter(筛选条件) (附first())

    res = models.UserInfo.objects.filter()
    res1 = models.UserInfo.objects.filter().first()
    res2 = models.UserInfo.objects.filter(id=1)
    

    image

  6. 模型类.objects.exclude(筛选条件)

    res = models.UserInfo.objects.exclude(id=1)  #  得到了id不为1的两个记录
    

    image

  7. 模型类.objects.order_by(排序依据)

    res1 = models.UserInfo.objects.order_by("age")  # 按照年龄排序
    res2 = models.UserInfo.objects.order_by("-age")  # 降序排
    res3 = models.UserInfo.objects.order_by("age").reverse()  # 降序排
    

    image

  8. 模型类.objects.values(字段名)

    res = models.UserInfo.objects.all().values("id", "name")  # 所有记录的id,name字段
    res1 = models.UserInfo.objects.filter(id=1).values("id", "name")
    res2 = models.UserInfo.objects.exclude(id=1).values("id", "name")
    

    image

  9. 模型类.objects.values(字段名).distinct()

    res = models.UserInfo.objects.all().values("id", "name").distinct()  # 所有字段联合去重
    

编写原生sql语句

自己编写sql来查询模型类有两种方式,在自己编写时需要注意实际的表名与模型表的类名区别:

  • raw()方法 (不要用)

    res = models.User.objects.raw("select * from app01_user;")   # 拿到个sql语句列表
    for i in res:   
        print(i)  # 逐个打印查到的对象
    
  • 游标 (类似pymysql)

    from django.db import connection   # 导入connection
    cursor = connection.cursor()
    cursor.execute('select name from app01_user;')
    print(cursor.fetchall())
    

ORM筛选条件

基础筛选

我们在编写filter的筛选条件时,默认是使用=表示相等,并且多个相等条件的判断默认是与关系。

而实际上,sql中却应该支持我们一些大于、小于等筛选条件,这类方法该如何编写呢:

django中将字段后加上__条件的方式让关键字参数拥有除等号外的其他含义。

  1. 大于、小于 gt、lt、gte、lte

    res = models.User.objects.filter(age__gt=18)   # 年龄字段大于18的用户
    res = models.User.objects.filter(age__lt=18)   # 小于
    res = models.User.objects.filter(age__gte=18)   # 大于等于
    res = models.User.objects.filter(age__lte=18)   # 小于等于
    
  2. 成员运算 in

    res = res = models.User.objects.filter(age__in=(18,32,34))  # 年龄字段是18,32,34的记录
    
  3. 范围 range

    res = models.User.objects.filter(age__range=(18, 38))   # 相当于between
    
  4. 模糊查询 contains icontains

    res = models.User.objects.filter(name__contains="l")  # 区分大小写  匹配"%l%"
    res = models.User.objects.filter(name__contains="l")  # 不区分大小写
    
  5. 时间匹配

    res = models.User.objects.filter(add_time__year=2022)   # 年份
    res = models.User.objects.filter(add_time__month=12)  # 月份
    

F和Q查询方法

模型表添加字段

在介绍这两个方法之前,我们可以先做一些数据准备,在书籍表中添加两个字段:库存和卖出字段

首先,这个过程需要在模型表中添加两个字段:

store = models.IntegerField(null=True) 
sale_num = models.IntegerField(default=0)

注:字段中填写了两个参数,一个是null,即允许为空(默认情况下为非空),一个是default默认值,即在记录中这个字段如果没填写则自动填充的值。如果不设置这两个参数,那么在添加这两个字段的时候,因为表中已经有数据了,所以原本记录的这两个字段即要求非空又没有默认值就无法迁移。

然后,涉及到我们的数据迁移,需要执行那两条命令makemigrations migrate

我们再在表中手动添加一些数据便于测试即可。

image

F方法

为了解决这么一类查询问题:查询卖出数量大于库存的书籍

这种将两个字段进行比较的筛选条件,我们在ORM中就需要借助F方法。

from django.db.models import F
res = models.Book.objects.filter(sale_num__gt=F("store")).values("title")

因为我们对字段的数据进行拿取时,默认是通过字段名的方式直接拿的,如果不借助F方法,它在=后是无法被识别到的。

借助F方法后就可以在=后面也能抓取每条记录的数据,用于作为筛选条件等。

Q方法

在筛选条件中,还有一种或、非的关系需要借助Q方法来实现。

尝试解决:查询主键是1或者价格大于2000的书籍名

我们filter中的逗号隔开的条件默认是and的关系,我们可以通过Q方法来让修改这个关系为或、非

from django.db.models import Q
res = models.Book.objects.filter(Q(pk=1) | Q(store__gt=50)).values("title")

# 查询主键不为1的书籍名
res = models.Book.objects.filter(~Q(pk=1)).values("title")

Q方法使用总结:

  • 两个条件是或关系Q(条件1) | Q(条件2)
  • 两个条件是非关系~Q(条件)

Q查询进阶操作

我们可以提前对Q查询做整体的拼接,产生一个Q对象存起来。

q_obj = Q()
q_obj.connector = 'or'  # 默认多个条件是and,可以修改为or
q_obj.children.append(("pk", 1))  # children是子条件,用逻辑运算连接
q_obj.children.append(("price__gt", 50))  # 可以添加多个子条件,以元组输入
res = models.Book.objects.filter(q_obj)  # q对象可以直接做筛选条件。
print(res)

进阶操作中,添加判断条件可以通过字符串的方式,也就意味着,可以让用户交互,进行筛选条件的选择与添加。

ORM外键

模型表创建外键

建立外键得按照我们三种表关系来建立,有一对多、一对一、多对多的关系。

class Book(models.Model):
    title = models.CharField(max_length=32)
    price = models.FloatField()
	# 建立一对多关系,多条记录的一方建立外键,关键字为ForeignKey
    publish = models.ForeignKey(to="Publish", on_delete=models.CASCADE)
	# 建立多对多关系,django中支持将外键建立在双方的一方
    author = models.ManyToManyField(to="Author")

class Publish(models.Model):
    name = models.CharField(max_length=32)
    addr = models.CharField(max_length=64)
    
class Author(models.Model):
    name = models.CharField(max_length=32)
	# 建立一对一关系,我们一般将其建立在查询频率高的一方
    detail = models.OneToOneField(to="AuthorDetail", on_delete=models.CASCADE)

class AuthorDetail(models.Model):
    tel = models.CharField(max_length=16)
    addr = models.CharField(max_length=64)

需要注意的是:

  1. 创建一对多关系

    和sql语句一样,外键建立到多的那张表上,不同的是,我们可以不讲究关联表和被关联表的建立顺序。字段类为ForeignKey

    在django2.x版本以上,建立一对多关系时需要指定on_delete参数为CASCADE,不加会报错,不过也不一定就是CASCADE,可能为其他实参,这里不展开。

    建立外键时,系统会自动加上_id后缀作为字段名。

  2. 创建多对多关系

    sql中是将两张表建立好后,将外键字段创建在第三张表中,而django为我们省去了这一步骤,我们可以在多对多关系双方的一个模型表中直接建立一个虚拟外键,ManyToManyField

    在底层,sql依旧创建了第三张表来存储两表的多对多关系,但是在orm操作中我们就可以将模型表中的外键当做实实在在的联系,因为在查询时,我们感受不到第三张的表的存在。

    多对多关系的外键没有on_delete关键字参数。

  3. 创建一对多关系

    一对一的字段类为OneToOneField,建议建立在查询频率高的一方。

    建立一对一关系时需要指定on_delete参数,否则报错。

利用外键设置表关系

在django中,当两张表的记录有外键的关联,它们就拥有了关系,可以在连表、子查询时省去很多麻烦。

  1. 一对多和一对一

    一对多和一对一外键属于实际在表中的外键字段,这种关系可以直接通过对表的外键字段增删改来实现修改。而外键字段默认绑定所绑定的表的主键字段。

    # 一对一  Author关联AuthorDetail
    models.AuthorDetail.objects.create(tel='1111', addr="合肥")
    models.Author.objects.create(name="leethon", detail_id=1)   # 可以为被关联表有的主键字段
    res = models.AuthorDetail.objects.filter(pk=2).first()
    models.Author.objects.filter(name="leethon").update(detail_id=res)  # 可以为一个AuthorDetail对象
    
    # 一对多  Publish对Book
    models.Publish.objects.create(name="上海出版社", addr="上海")  
    res = models.Publish.objects.create(name="西安出版社", addr="西安")   # 创建出版社记录,并返回了数据对象
    models.Book.objects.create(title='80天环绕寝室', price="99", publish=res)  # 可以通过数据对象来关联
    models.Book.objects.create(title='进击的养殖鸡', price="99", publish_id=1)  # 可以通过被关联表主键关联
    

    一对一关系中需要注意只能一一对应,也就是外键字段有独一性的约束条件

  2. 多对多

    多对多外键属于实际不在模型表中的虚拟字段,多对多关系则需要django提供给我们的方法来实现增删改关系。

    拿到设立多对多外键的模型表的对象,用它点出外键属性,可以进行add、set、remove方法,这些方法都是这条记录对象的操作。

    # 多对多 Book对Author
    ## add 添加关系
    book_obj = models.Book.objects.filter(pk=1).first()   # 拿到book对象
    book_obj.author  # 这个是所有book记录都有的author外键属性
    book_obj.author.add(1, 5)   # 直接通过主键数字来添加关系,可以一次添加多个
    author_obj = models.Author.objects.filter(pk=7).first()   # 拿到author对象
    book_obj.author.add(author_obj)   # 添加author对象也可以添加关系
    
    ## set 设置关系
    book_obj.author.set([6,7,8])   # 将原本的关系删除,设置为新关系
    book_obj.author.set((author_obj1,author_obj2))  # 参数是可迭代对象,元素可以是author对象
    
    ## remove 移除关系
    book_obj.author.remove(1)  # 通过主键数字来删除关系
    book_obj.author.remove(author_obj)   # 通过author对象删除关系
    

ORM跨表查询

建立外键的模型表到其外键关联的模型表,如Book到Publish,这个方向称为正向的表关系,反之则为反向表关系。

我们试着使用不同的方法去查询

  1. 出版了书籍《霸道总裁爱上谁》的作者的名字。(正向查询:书籍含外键)
  2. 作者leethon出版的书籍名。(反向查询,书籍含外键)

基于对象的正反向查询

# 正向查询:
# 1. 拿到对象
book_obj = models.Book.objects.filter(title="霸道总裁爱上谁").first()
# 2. 通过对象拿到外键字段
book_obj.author.all().values('name')   # 结果形式 <QuerySet [{}, {}, {}]>

# 反向查询:
# 1. 拿到作者对象(没有外键的被关联表)
author_obj = models.Author.objects.filter(name="leethon").first()
# 2. 通过作者对象反向查询时,需要将关联表的名称小写加上_set
author_obj.book_set.all().values("title")
  • 正向查询:数据对象可以点出它有的外键字段的名称属性
  • 反向查询:数据对象可以点出关联它的表名小写_set的名称属性

基于字段的正反向查询

# 正向查询
models.Book.objects.filter(title="霸道总裁爱上谁").values("author__name")
# 反向查询
models.Author.objects.filter(name="leethon").values("book__title")
  • 正向查询:字段属性可以直接填写它的外键字段名__被关联表字段名
  • 反向查询:字段属性可以直接填写它的关联表表名__关联表的字段名

基于筛选条件的正反向查询

# 反向查询
models.Author.objects.filter(book__title="霸道总裁爱上谁").values("name")
# 正向查询
models.Book.objects.filter(author__name="leethon").values("title")
  • 正向查询:筛选属性可以通过外键字段名__被关联表字段直接将被关联表的字段作为判断条件
  • 反向查询:筛选属性可以通过关联表表名__关联表字段直接将关联表的字段作为判断条件

了解了上述方法,我们可以尝试:

查询出版了书籍《80天环绕寝室》的作者其电话号码是多少?

这个题目中涉及了三张表,用>来表示正向则 book>author>authordetail

# 方法一:全正向
models.Book.objects.filter(title="80天环绕寝室").values("author__detail__tel")
# 方法二:全逆向
models.AuthorDetail.objects.filter(author__book__title="80天环绕寝室").values('tel')
# 方向三:正逆向都掺和点
models.Author.objects.filter(book__title="80天环绕寝室").values('detail__tel')

多对多关系补充

多对多关系字段有三种建立方式,其中第一种我们上文已经介绍了,就是ManyToManyField字段,也体会到它的方便之处,它可以自动的创建第三张关系表,而且在进行关联表和被关联表的相互查询和关系修改时也十分方便,我们可以使用正向方向查询和set\add\remove\clear等修改关系。

但是全自动建立的外键,它所建立的第三张表,是不可拓展的,如果我们有一些业务逻辑就是在关系表上,我们就无法通过第三张表完成了。

所以我们还需要介绍手动创建的方法,来满足扩展字段的需求。

  • 全手动方法

    class Book(models.Model):
        title = models.CharField(max_length=32)
    class Author(models.Model):
        name = models.CharField(max_length=32)
    class Book2Author(models.Model):
        book = models.ForeignKey(to='Book')
        author = models.ForeignKey(to='Author')
        others = models.CharField(max_length=32)
        join_time = models.DateField(auto_now_add=True)
    

    这种方式几乎和sql建立多对多关系相对应,很明显这个是有很强的拓展性的,我们可以建立字段来存储建立关系的时间等业务逻辑。

    但是作为模型表,我们无法通过set\add\remove\clear等修改关系,正反向查询的逻辑也比较乱,需要通过中间表转接一次。

  • 手动创建优化

    django在为手动创建的表增加了一层优化,让我们可以通过ORM的正反向查询直接链接到两张表:

    class Book(models.Model):
            title = models.CharField(max_length=32)
            authors = models.ManyToManyField(to='Author',   # 被关联表
                                             through='Book2Author',   # 关系表
                                             through_fields=('book','author')  # 关系表对应外键字段
                                            )
    class Author(models.Model):
        name = models.CharField(max_length=32)
    class Book2Author(models.Model):
        book = models.ForeignKey(to='Book', on_delete=models.CASCADE)
        author = models.ForeignKey(to='Author', on_delete=models.CASCADE)
        others = models.CharField(max_length=32)
        join_time = models.DateField(auto_now_add=True)
    

    这层优化,需要我们在一张模型表中设置ManyToManyField字段,并且将第三张表的表名和两张表的对应外键告知它。这个操作是在book中设置虚拟外键,让我们仍然可以通过正反向查询直接访问关联的表。但是仍然无法使用set\add\remove\clear的方法。

聚合函数和分组查询

聚合函数

对应sql中的聚合函数,聚合函数可以在分组前后使用,而ORM中需要将两种情况分开来看,如果在分组前来看,我们需要通过模型表对象.aggregate(聚合函数)的方式来使用。如:

from django.db.models import Max, Min, Sum, Count, Avg
# 首先这些聚合函数需要从db.models文件中导入
res = models.Book.objects.aggregate(Max('price'), Count('pk'), 最小价格=Min('price'), allPrice=Sum('price'),平均价格=Avg('price'))
print(res)   # {'最小价格': 99.0, 'allPrice': 396.0, '平均价格': 99.0, 'price__max': 99.0, 'pk__count': 4}

其中,可以通过关键字传参的方式给字段取别名。

而使用了aggregate意味着整张表会作为一个整体,其单独的数据属性是不能作为我们查询字段的,这个对应了我们sql的严格模式,分组后只能插叙组的数据统合,而不再查看组中某条具体记录。

方法 含义
Max()|Min() 字段的最大最小值
Count() 记录的数量
Sum() 该字段数据的和
Avg() 平均值
Concat() 字符拼接

分组查询

分组后的查询所用的关键字是:annotate

在ORM中,有两种分组依据,一个是与sql一样按照字段分组,一种则是依据外键对应的表来进行分组的。

  • 通过字段分组

    先按照字段取结果,再使用annotate方法:表对象.values(分组字段).annotate(聚合函数)

  • 通过表来分组

    需要先找到分组依据的表对象再使用annotate方法:表对象.annotate(聚合函数).values(聚合函数别名)

通过几个例子来看一下:

1.统计出每个出版社卖的最便宜的书的价格

分析:分组依据是出版社,目标字段是书的价格

# 按字段分组,先拿目标字段所在表对象即可
res = models.Book.objects.values("publish_id").annotate(Min("price"))
# 按表分组,先拿分组依据表对象
res = models.Publish.objects.annotate(min_price=Min("book__price")).values("name", "min_price")

ps:如果sql_mode配置中含only_group_by,则会导致第二种方式的values中不能添加非表字段,如我们的Publish的name字段就无法显示了。

2.筛选出作者个数大于1的书籍

分析:分组依据是书籍,目标字段是作者_pk(任何一个字段都可以,count的结果都一样)

# 按字段分组
res = models.Author.objects.values("book__title").annotate(author_count=Count("pk"))\
        .filter(author_count__gt=1)
# 按表分组
res=models.Book.objects.annotate(author_count=Count('author__pk')).\
filter(author_count__gt=1).values("title", "author_count")

这里的filter就相当于sql中的having,因为它是分组后查询的。

ORM查询优化

在ORM中可以自动的或者手动做一些查询优化,让我们查询的流程或者效率更高。而总体的原理在于,并不是产生queryset对象时执行sql,而是在触发打印、循环、取字段值时触发sql语句的执行。

查询默认惰性查询

惰性查询即,当语法检测到后续没有用我拿的查询结果做任何操作如展示,则不查询。但是每次使用这个结果都会执行一次查询语句。

res = models.Book.objects.filter()
# print(res)  # 没有这句则底层查询,可以通过sql配置查看语句验证。

自带分页处理

我们查询sql语句在执行打印操作时,最后默认会加上limit的条件,没有写,则按默认值分页。

image

ps:如果不是打印相关操作,就不会带分页,如查询结果会for循环处理每一个数据就要保证每个数据被处理,所以就不带分页。

only和defer

  • 我们使用values去取字段时,queryset对象结果中的一个个元素是以字典方式存储的。

    <queryset [{字段名:值},{},{}]>

  • 我们用only(查询字段)或者defer(不查询字段)的方式拿到的queryset对象,内部则会存储具体数据对象。

    <queryset [数据对象,数据对象2,数据对象3]>

res = models.Book.objects.only("title", "publish_time")
for data in res:
    print(data.title)
    # print(data.price)

执行上述代码,如果用数据对象点出only内指定的字段,则不会重复查询,但是如果想用这个数据对象取其他的字段数据,则会再构筑一条sql执行查询。

而defer是指定不进行查询的字段,相当于对字段取反的only操作。

这两个操作是针对数据对象取外键操作的查询优化,即当数据对象所包含的字段默认是自己表中的字段(如all()的queryset结果中的数据对象),这样的数据对象取自己的字段不必重复查询,但是取外键链接的字段则需要重复查询。

res = models.Book.objects.all()
for data in res:
    print(data.publish.name)   # 每次都会重新查询
    
# 使用select_related
res = models.Book.objects.select_related("publish")
for data in res:  # 直接执行连表查询
    print(data.publish.name)  # 每次循环不会再重复执行
    
# 使用prifetch_related
res = models.Book.objects.prefetch_related("publish")
for data in res:  # 直接执行子查询
    print(data.publish.name)  # 每次循环不会再重复执行

ORM批量操作数据

我们尝试通过ORM对100000条数据进行创建,这100000条数据就假设存在一个列表中,那么我们应该怎么操作?

下述代码是简单的逻辑实现,但是效率很低。

from apps import models
from xxx import data_list   # 假设导进来一个拥有十万条数据的数据列表
# 列表中每个元素的存储形式为{"aaa":v1,"bbb":v2...}

def create_data(request):
    for data in data_list:
        models.Data.objects.create(aaa=data.get('aaa'),bbb=data.get('bbb'))
        ...

上述代码就相当于每条数据组织一个sql去插入数据,所以效率低下。

而针对于重复的插入数据操作,ORM中也提供了相应的方法。

from apps import models
from xxx import data_list   # 假设导进来一个拥有十万条数据的数据列表
# 列表中每个元素的存储形式为{"aaa":v1,"bbb":v2...}

def create_data(request):
    # 循环产生数据对象,并不执行sql语句
    data_obj = (models.Data(aaa=data.get('aaa'),bbb=data.get('bbb'))  for data in data_list)
    # 批量将数据对象插入数据库,此时才执行sql
    models.Data.object.bulk_create(data_list)

上述批量插入的方法,效率得到了极大的提升,我们只执行了一句sql,对应insert into 表名 values()的values后有多个数据值,就可以插入所有数据,至少在ORM中,我们已经做到了优化。

ORM事务操作

sql事务操作直达

ACID

原子性、一致性、隔离性、持续性

sql关键字

start transanction;  # 开启事务
rollback;  # 回滚
commit;# 确认
savepoint 回滚点名; # 新建回滚点

一些重要概念

  • 脏读:未提交读
  • 幻读:可重复读
  • 不可重复读:提交读
  • MVCC多版本控制:解决幻读的问题

mysql补充概念

ORM事务操作

  1. 全局开启事务

    在配置数据库时即DATABASE的default中,添加一个键值对配置,"ATOMIC_REQUESTS":TRUE

    每次涉及ORM操作的同属于一个事务。

  2. 装饰器

    局部有效,一般是针对视图函数的,当我们这个视图函数中涉及ORM操作,并且没有执行完时出现报错,那么就会自动触发回滚,返回视图函数开始的状态。

    没有添加事务的转账操作,一方已经加钱,另一方全因为错误没有扣钱,这时应该加事务在遇到错误时回滚。

    def transfer(request):
        from django.db.models import F
        models.User.objects.filter(pk=1).update(account=F("account")+30)   # 这步已经生效
        sdlkfj  # 一个错误
        models.User.objects.filter(pk=2).update(account=F("account")-30)  # 这步没有成功
        return HttpResponse("转账成功")
    

    添加事务操作后,就整个视图函数中遇到错误时自动回滚到未执行的状态

    from django.db import transanction  # 需要导入一个事务模块
    
    @transanction.atomic   # 原子装饰器
    def transfer(request):
        from django.db.models import F
        models.User.objects.filter(pk=1).update(account=F("account")+30)   # 这步暂时生效
        sdlkfj  # 一个错误直接回滚,上一步操作作废,回到最初状态。
        models.User.objects.filter(pk=2).update(account=F("account")-30) 
        return HttpResponse("转账成功")
    

    如果没有那个错误,则执行成功,读者可自行尝试。

  3. with上下文

    with则将原子操作的位置划分的更小,可以是任意一段代码,将其放到with的子代码中即可,在with开始开启事务,在遇到错误时回滚,在执行完毕后确认。

    with transaction.atomic():
        models.Book.objects.filter(pk=1).update(price=F("price")+30)
        。。
        models.Book.objects.filter(pk=2).update(price=F("price")-30)
    

ps:注意在视图函数中,哪怕返回的不是httpresponse对象,对于视图函数本身也是执行完了,会触发事务的确认。

ORM常用字段类型

常用字段 字段常见参数 字段描述
AutoField primary_key 主键字段才会用,可以让其自动创建
CharField max_length 对应varchar字段,存储有限的字符
IntegerField、BigIntergerField 整型字段
DecimalField max_digits/decimal_places 小数字段
DateField、DateTimeField auto_now/auto_now_add 日期、时间字段
BooleanField orm传入True、False存成1和0
TextField 存储大量文本
EmailField 存储邮箱格式数据
FileField upload_to orm传入文件对象,会将文件内容保存到配置指定路径,字段存文件路径
ForeignKeyField、OneToOneField to/to_field/on_delete 实际外键字段,建立一对多和一对一关系
ManyToManyField to/to_field 虚拟外键字段,建立多对多关系

需要说明的是,这些orm字段并非和sql字段一一对应,有些是封装了一些逻辑功能在字段的创建、存储过程中的。

自定义ORM字段

class MyCharField(models.Field):
    def __init__(self, max_length, *args, **kwargs):
        self.max_length = max_length
        super().__init__(max_length=max_length, *args, **kwargs)

	def db_type(self, connection):
        return 'char(%s)' % self.max_length


class User(models.Model):
    name = models.CharField(max_length=32)
    info = MyCharField(max_length=64)

ORM常用字段参数

一些基础参数

primary_key verbose_name null default unique db_index
主键字段 字段注释 允许为空 默认值 唯一值 添加辅助索引

这些参数都在sql中有对应的关系,不必过多说明,这些参数几乎所有字段都可以添加。

字段独有参数

  1. max_length:最大长度

    是字符类型数据所拥有的参数,对应varchar(64)后面的数字,规定字符类型最大存储字符数。

  2. max_digits:小数总共多少位
    decimal_places:小数点后面的位数
    这两个参数是DecimalField独有的,表示小数位数的存储方式

  3. auto_now:每次操作数据自动更新事件
    auto_now_add:首次创建自动更新事件后续不自动更新
    在时间相关字段的独有参数,设置为True则会自动执行相关功能。

  4. choices:当某个字段的可能性能够被列举完全的情况下使用
    如性别、学历、工作状态、...等

    提前以元组形式定义所有结果的对应关系,当我们想存入值,就会对应的存入元组的0号元素。
    如果我们想要取出值,则可以选择读取实际存储值,或按映射关系拿到元组的1号元素对应值。

    class User(models.Model):
        name = models.CharField(max_length=32)
        gender_choice = (
            (1, '男性'),
            (2, '女性'),
        )
        gender = models.IntergerField(choices=gender_choice,null=True)
        
    user_obj = User.objects.filter(pk=1).first()  # 拿到一个对象
    user_obj.gender # 直接点显示存储的真实数据
    user_obj.get_gender_display()  # 通过这个方法拿显示转义后的选项。
    
  5. upload_to:文件字段,文件不会存在数据库,而会存储到这个参数所指定的文件目录下,数据库只存储文件路径。

外键相关参数

  1. to:关联表
  2. to_field:关联字段(不写默认关联数据主键)
  3. on_delete:当删除关联表中的数据时,当前表与其关联的行的行为(多对多关系无此参数)。
  4. related_name:反向查询名(默认为关联表名小写_set)
  5. related_query_name:反向连表字段名(默认为关联表名小写)
  6. db_constraint:默认为True,如果为False,则不建立数据库的外键约束,但是ORM层面的关联仍然正常使用

on_delete的参数值:

1、models.CASCADE
级联操作,当主表中被连接的一条数据删除时,从表中所有与之关联的数据同时被删除

2、models.SET_NULL
当主表中的一行数据删除时,从表中所有与之关联的数据的相关字段设置为null,此时注意定义外键时,这个字段必须可以允许为空

3、models.PROTECT
当主表中的一行数据删除时,由于从表中相关字段是受保护的外键,所以都不允许删除

4、models.SET_DEFAULT
当主表中的一行数据删除时,从表中所有相关的数据的关联字段设置为默认值,此时注意定义外键时,这个外键字段应该有一个默认值

5、models.SET()
当主表中的一条数据删除时,从表中所有的关联数据字段设置为SET()中设置的值,与models.SET_DEFAULT相似,只不过此时从表中的相关字段不需要设置default参数

6、models.DO_NOTHING
什么都不做,一切都看数据库级别的约束,注数据库级别的默认约束为RESTRICT,这个约束与django中的models.PROTECT相似。这个级联策略只能在没有建立外键约束时才能用 db_constraint=False

ps:主表一般是指在一对一、一对多中一的那一方,当对其进行删除时,另外一张表的所有元素都应该受到影响

posted @ 2022-12-14 21:43  leethon  阅读(149)  评论(0编辑  收藏  举报