Python自动化开发学习23-Django上(Model)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python自动化开发学习23-Django上(Model)相关的知识,希望对你有一定的参考价值。

Model、Form以及ModelForm

Model
数据库操作:

  • 创建表结构
  • 操作数据表
  • 数据验证-弱

Form
强大的数据验证功能

ModelForm
这个还没讲过,是上面两个的合集:

  • 数据库操作
  • 数据验证

非常方便,适合小型项目。或者是和 django 的 admin 相关的操作,admin就是通过ModelForm实现的。
但是,耦合非常强,不可拆分(比如数据库操作和业务操作不可分)。如果以后业务扩展了,这两部分就得拆开,那只能重写。

Model

讲师的博客:http://www.cnblogs.com/wupeiqi/articles/6216618.html

元信息

在定义表结构的类里,再定义一个元类,可以实现自定义表名称、联合索引

class UserInfo(models.Model):
    nid = models.AutoField(primary_key=True)
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=64)
    first_name = models.CharField(max_length=32)
    last_name = models.CharField(max_length=32)

    class Meta:
        # 指定数据库中生成的表的名称。默认是 app名称_类名
        db_table = "table_name"
        # 联合索引
        index_together = (
            ("username", "password"),
        )
        # 联合唯一(索引)
        unique_together = (
            ("first_name", "last_name"),
        )
        # admin中使用的表名称
        verbose_name = "用户信息"
        # admin中最终显示的表名称,默认这个的值是verbose_name的值后面加了个‘s‘
        verbose_name_plural = "用户信息表"

联合索引,是最左前缀模式,可以只匹配部分字段,但是必须是左边的字段。比如 ("username", "email"), 如果只有username可以命中索引,同时有username和email也能命中索引,但是缺少username只有email,无法命中联合索引。
联合唯一,就是在联合索引的基础上,再加上一个唯一约束。至于索引,唯一约束应该都是包含索引的。但是索引不一定要有唯一约束。

一对一、一对多、多对多

多表之间的关联关系有:一对一(OneToOneField)、一对多(ForeignKey) 以及 多对多(ManyToManyField)。
一对一 和 多对多,都是基于 一对多 衍生出来的:

  • 一对一:就是在一对多的基础上,再对id加上一个唯一约束
  • 多对多:就是搞一个第三张表,在第三张表里对另外两张表分别做了一个一对多

一对多

ForeignKey(ForeignObject)

  • to :关联的表名
  • on_delete :当删除关联表中的数据时,当前表与其关联的行的行为
    • models.CASCADE :删除关联数据,与之关联的数据也删除。一般就用这个,以前这个参数可以默认,这个就是默认值
    • models.DO_NOTHING :删除关联数据,引发错误IntegrityError。这个是数据库抛出的异常
    • models.PROTECT :删除关联数据,引发错误ProtectedError。这个是django里抛出的异常
    • models.SET :删除关联数据,与之关联的值设置为指定值,或设置为可执行对象的返回值。models.SET(值或函数名)
    • models.SET_DEFAULT :删除关联数据,与之关联的值设置为默认值(前提这个字段需要设置默认值 default= )
    • models.SET_NULL :删除关联数据,与之关联的值设置为null(前提这个字段需要设置为允许为空 null=True )
  • related_name=None :反向操作时,使用的字段名,用于代替【表名_set】。如:obj.表名_set.all() ,具体看下面的反向操作
  • related_query_name=None :反向操作时,使用的连接前缀,用于替换【表名】。如: models.UserGroup.objects.filter(表名__字段名=1).values(‘表名__字段名‘)
  • limit_choices_to=None :在Admin和ModelForm中,显示关联数据是,对数据进行筛选。默认是一个下拉框,里面是算有的关联数据
  • parent_link=False :在Admin中是否显示关联数据
  • to_field=None :关联的字段名,默认是那张表的主键,一般情况就是id了
  • db_constraint=True :默认创建外键关联的时候,在数据库里的表是有外键约束的。也可以设成False,这样创建的数据库表就没有约束了,同时不影响django的操作

反向操作

默认的反向查找用下面的方式引用:

class Group(models.Model):
    name = models.CharField(max_length=32)

class User(models.Model):
    name = models.CharField(max_length=32)
    gp = models.ForeignKey(to=‘Group‘, on_delete=models.CASCADE)

# views.py 里
groups = models.Group.objects.all()
for group in groups:
    users = group.user_set.all()  # 用 表名_set 进行反向查找

models.Group.objects.all().values(‘name‘, ‘user__name‘)  # 用双下划线进行反向查找

这里也可以自定制反向查找使用的字段名:

class Group(models.Model):
    name = models.CharField(max_length=32)

class User(models.Model):
    name = models.CharField(max_length=32)
    gp = models.ForeignKey(to=‘Group‘, on_delete=models.CASCADE,
                           related_name=‘user1‘,
                           related_query_name=‘user2‘)

# views.py 里
groups =  models.Group.objects.all()
for group in groups:
    users = group.user1.all()

 models.Group.objects.all().values(‘name‘, ‘user2__name‘)

表的自关联

这种场景下,必须要指定反向引用的参数。大概的应用场景比如,人员表和人员表里其他的人做关联,被关联的就是当然人的领导。这里就不能使用User表名来引用关联数据的话,必须要指定一个新的表名(虽然还是自己)。这部分只是顺便讲了一下,下面的代码没验证过:

class User(models.Model):
    name = models.CharField(max_length=32)
    leader = models.ForeignKey(to=‘self‘, on_delete=models.CASCADE,
                               related_name=‘leader_set‘,
                               related_query_name=‘leader‘)

数据筛选(limit_choices_to)

默认会在下拉列表里显示所有关联的数据,使用这个参数后,只会显示经过筛选后获得的数据:

limit_choices_to={‘nid__gt‘: 5}  # 简单的逻辑
limit_choices_to=lambda : {‘nid__gt‘: 5}  # 使用匿名函数
# 符合逻辑得导入Q
from django.db.models import Q
limit_choices_to=Q(nid__gt=10)
limit_choices_to=Q(nid=8) | Q(nid__gt=10)
limit_choices_to=lambda : Q(Q(nid=8) | Q(nid__gt=10)) & Q(caption=‘root‘)

一对一

一对一的类继承了一对多的类,所以ForeignKey里的参数这里都一样
OneToOneField(ForeignKey)
源码的构造函数如下:

    def __init__(self, to, on_delete, to_field=None, **kwargs):
        kwargs[‘unique‘] = True
        super().__init__(to, on_delete, to_field=to_field, **kwargs)

这里额外的加了一个 unique 约束。

多对多

ManyToManyField(RelatedField)

  • to
  • related_name=None
  • related_query_name=None
  • limit_choices_to=None
    上面这几个和一对多是一样的,主要看下面这些多对多特有的:
  • symmetrical=None :仅用于多对多自关联时,symmetrical用于指定内部是否创建反向操作的字段
  • through=None :自定义第三张表时,使用字段用于指定关系表。下面有展开
  • through_fields=None :自定义第三张表时,使用字段用于指定关系表中那些字段做多对多关系表
  • db_constraint=True :是否在数据库中创建外键约束
  • db_table=None :默认创建第三张表时,数据库中表的名称。就是指定django自动创建的第三张表的表名
  • swappable=True :没有讲,博客里也没有

多对多自关联(symmetrical)

对于多对多的自关联,做如下操作时,不同的symmetrical会有不同的可选字段:

class BB(models.Model):
    code = models.CharField(max_length=12)
    m1 = models.ManyToManyField(‘self‘, symmetrical=False)

# views.py 里
models.BB.objects.filter(...)
# 这里的filter里的可选字段只有:code,id,m1
# 如果 symmetrical=False
# 那么filter里的可选字段有:code,id,m1,bb。多一个表名

这里只是讲了一下参数的作用,貌似一般应用的时候用不到。通过关联的字段m1应该都是可以查找到所有的数据的。

多对多自定义第三张表(through、through_fields)

如果是自定义的第三张表,那么很多操作就要自己写了。
但是自动生成的第三张表,只能有3列(算上id)。如果既要自定义第三张表,又想用django原生的部分方法,就需要按下面的方法来定义了。不过也只能做查的操作,依然不能做增删改的操作(因为自定义的第三张表往往超过3列,有其他自定义的字段,比如在新增数据的时候,那些字段的值是无法传过去的。查的操作是肯定可以的,其他操作用到的时候再测试吧,原因就是有额外的字段存在),不过能做查也已经很方便了:

class Person(models.Model):
    name = models.CharField(max_length=16)

class Group(models.Model):
    name = models.CharField(max_length=16)
    members = models.ManyToManyField(
        to=‘Person‘,
        through=‘Membership‘,
        through_fields=[‘group‘, ‘person‘]
    )

# 自定义的第三张表
class Membership(models.Model):
    group = models.ForeignKey(‘Group‘, on_delete=models.CASCADE)
    person = models.ForeignKey(‘Person‘, on_delete=models.CASCADE)

原本models.ManyToManyField()是会自动生成第三张表的,这里用through参数,指定了不要自动创建,而是连接到自定义的第三表去。要么不定义ManyToMany完全自己写操作,要么定义的时候加上through参数
through_fields,则是指定第三张表里关联的是哪2个字段,如果自定义的第三张有更多的自动,就得用这个参数来指定需要关联的那2个字段了。

ORM操作

基本操作(回顾)

只有查里面的一个剔除的方法是之前没有用过的

models.Tb1.objects.create(c1=‘abc‘, c2=‘123‘)
obj = models.Tb1(c1=‘abc‘, c2=‘123‘)
obj.save()

models.Tb1.objects.get(id=1)  # 不存在则会报错
models.Tb1.objects.all()
models.Tb1.objects.filter(c1=‘abc‘)
models.Tb1.objects.exclude(c1=‘abc‘)  # 和filter类似,但是逻辑是剔除

models.Tb1.objects.filter(c1=‘abc‘).delete()

models.Tb1.objects.filter(c1=‘abc‘).update(c2=‘321‘)
obj = models.Tb1.objects.get(id=1)
obj.c2 = ‘321‘
obj.save()

进阶操作

获取记录的个数

models.Tb1.objects.filter(c1=‘abc‘).count()
models.Tb1.objects.filter(id__gt=1, id__lt=10)  # 多个条件是与的关系

大于、小于

models.Tb1.objects.filter(id__gt=1)
  • __gt:大于
  • __gte:大于等于
  • __lt:小于
  • __lte:小于等于

in 属于

models.Tb1.objects.filter(id__in=[1, 3, 5])  # 获取id是 1 或 3 或 5 的记录
models.Tb1.objects.exclude(id__in=[1, 3, 5])  # not in

isnull 是否为空

models.Tb1.objects.filter(c2__isnull=True)  # 查找 c2 字典为空的记录

contains 包含

models.Tb1.objects.filter(c1__contains=‘ab‘)  # 包含
models.Tb1.objects.filter(c1__icontains=‘AB‘)  # 前面加个i,不区分大小写
models.Tb1.objects.exclude(c1__icontains=‘ab‘)  # 不包含

rang 范围

models.Tb1.objects.filter(id__rang=[1, 2])  # 范围,SQL语句的 between and

其他(不举例了)

  • __startswith:以什么什么开头
  • __istartswith:忽略大小写
  • __endswith:以什么什么结尾
  • __iendswith:忽略大小写

order by 排序

models.Tb1.objects.all().order_by(‘id‘)  # 升序 ASC
models.Tb1.objects.all().order_by(‘-id‘)  # 降序 DESC,就前面加个减号
models.Tb1.objects.all().order_by(‘id‘, ‘-c2‘)  # 可以加多个参数,第一个一样的,再按第二个排序

limit offset

models.Tb1.objects.all()[10:20]  # 相当于SQL里的 OFFSET 10 LIMIT 20,直接就按照列表切片更好理解嘛

正则匹配

models.Tb1.objects.filter(c1__regex=r‘^(An?|The) +‘)  # 正则匹配
models.Tb1.objects.filter(c1__iregex=r‘^(an?|the) +‘)  # 正则匹配,忽略大小写

日期和时间

models.Tb1.objects.filter(c3__date=datetime.date(2005, 1, 1))

一共有下面这些:

  • __date
  • __year
  • __month
  • __day
  • __week_day
  • __hour
  • __minute
  • __second

group by

models.Tb1.objects.filter(c1=‘abd‘)  # 先做数据的筛选
models.Tb1.objects.filter(c1=‘abd‘).values(‘id‘)  # 只显示id字段,后面如果跟annotate就不一样了
models.Tb1.objects.filter(c1=‘abd‘).values(‘id‘).annotate()  # 根据id进行分组
from django.db.models import Count, Min, Max, Sum
models.Tb1.objects.filter(c1=‘abd‘).values(‘id‘).annotate(Count)  # 可以用 Count Min Max Sum 这些
models.Tb1.objects.filter(c1=‘abd‘).values(‘id‘).annotate(c=Count)  # 结果生成新的一列,字段名是c,相当去SQL的 AS "c"
models.Tb1.objects.filter(c1=‘abd‘).values(‘id‘).annotate(c=Max(‘id‘))  # 具体聚合处理哪个字段

# 更加复杂的例子
# SELECT u_id, COUNT(ui) AS `uid` FROM UserInfo GROUP BY u_id
v = models.UserInfo.objects.values(‘u_id‘).annotate(uid=Count(‘u_id‘))
# SELECT u_id, COUNT(ui_id) AS `uid` FROM UserInfo GROUP BY u_id having count(u_id) > 1
v = models.UserInfo.objects.values(‘u_id‘).annotate(uid=Count(‘u_id‘)).filter(uid__gt=1)
# SELECT u_id, COUNT( DISTINCT ui_id) AS `uid` FROM UserInfo GROUP BY u_id having count(u_id) > 1
v = models.UserInfo.objects.values(‘u_id‘).annotate(uid=Count(‘u_id‘,distinct=True)).filter(uid__gt=1)
# 上面这句distinct的效果是去重

在annotate前面的filter,相当于SQL的WHERE子句。
在annotate后面的filter,则相当于SQL的HAVING子句。

  • SQL中增加HAVING子句原因是,WHERE关键字无法与聚合函数一起使用,HAVING子句可以让我们筛选分组后的各组数据。*

其他操作

这里讲QuerySet类型,用的最多的.all() 和 .filter() 是返回一个QuerySet类型的。并且只要是QuerySet类型就可以继续使用 .all() 或 .filter() 等这些方法。因为这些方法都是QuerySet里定义的方法。
这里找源码去翻一下,或者直接找到这个文件 django/db/models/query.py ,里面有个类 class QuerySet 。这里面有看看有哪些方法。往下翻,找到all方法:

    ##################################################################
    # PUBLIC METHODS THAT ALTER ATTRIBUTES AND RETURN A NEW QUERYSET #
    ##################################################################

    def all(self):
        """
        Return a new QuerySet that is a copy of the current one. This allows a
        QuerySet to proxy for a model manager in some cases.
        """
        return self._chain()

    def filter(self, *args, **kwargs):
        """
        Return a new QuerySet instance with the args ANDed to the existing
        set.
        """
        return self._filter_or_exclude(False, *args, **kwargs)

下面还有更多方法,就是我们可以用的

  • def all(self): :获取所有的数据对象
  • def filter(self, *args, **kwargs): :条件查询,条件可以是:参数、字典、Q
  • def exclude(self, *args, **kwargs): :条件查询,条件可以是:参数、字典、Q
  • def select_related(self, *fields): :性能相关,下面会展开
  • def prefetch_related(self, *lookups): :性能相关,下面会展开
  • def annotate(self, *args, **kwargs): :用于实现聚合group by查询,上一小节有例子
  • def distinct(self, *field_names): :用于distinct去重
  • def order_by(self, *field_names): :用于排序
  • def extra(self, select=None, where=None, params=None, tables=None, order_by=None, select_params=None): :下面会展开
  • def reverse(self): :倒序,要先order_by之后才有效果。models.User.objects.all().order_by(‘id‘).reverse() 。单独用无效
  • def defer(self, *fields): :映射中排除某列数据,和下面那个相反。只取几列,那么之后就只能用那几个字段,但是要再取其他字段也是可以的,不过django会再去发起一个SQL请求,这样性能就差了。
  • def only(self, *fields): :仅取某个表中的数据。models.UserInfo.objects.only(‘username‘,‘id‘)
  • def using(self, alias): :指定使用的数据库,参数为别名(setting中的设置)。不指定就是用默认数据库。下面有例子

using 方法

settings.py 里可以设置多个库:

DATABASES = {
    ‘default‘: {
        ‘ENGINE‘: ‘django.db.backends.sqlite3‘,
        ‘NAME‘: os.path.join(BASE_DIR, ‘db.sqlite3‘),
    }
    ‘db1‘: {
        ‘ENGINE‘: ‘django.db.backends.sqlite3‘,
        ‘NAME‘: os.path.join(BASE_DIR, ‘db.sqlite3‘),
    }
}

默认就是操作default这个库,可以通过using来指定操作其他库。这样就可以手工的实现读写分离。不过django还有自己的实现读写分离的方法

extra 方法

def extra(self, select=None, where=None, params=None, tables=None, order_by=None, select_params=None): 构造额外的查询条件或者映射。
可以定制WHERE子句,wherr参数和params参数。
另外还可以定制SELECT子查询,select参数和select_params参数。
tables参数是选择那张表进行操作,这里的例子前面都已经传表了,所以用不到这个
order_by参数,就和order_by()方法一样排序的。不用这个参数,在后面再加一个order_by()方法来排序也是一样的。

# 下面的%s是占位符,逗号后面的select_params是替代select里的占位符
models.User.objects.extra(select={‘new_id‘: "select col from sometable where othercol > %s"}, select_params=(1,))
# params是替代where里的占位符。之前经常用的filter方法也是实现WHERE子句的功能。
models.User.objects.extra(where=[‘headline=%s‘], params=[‘Lennon‘])
# 用逗号隔开是and关系,OR就是or关系
models.User.objects.extra(where=["foo=‘a‘ OR bar = ‘a‘", "baz = ‘a‘"])
models.User.objects.extra(select={‘new_id‘: "select id from tb where id > %s"}, select_params=(1,), order_by=[‘-nid‘])

跨表的效率

跨表操作会有效率问题,看下面的情况:

users = models.User.objects.all()  # 这里还不会发起SQL请求,只是定义了这个对象
for user in users:
    print(user.name)  # 这个时候会发起一次SQL请求,这次请求获取到的user里面,并没有跨表的user.group的数据
    print(user.group.group_name)  # 这里是跨表了,但是之前的请求user里面并没有跨表的数据,这里还要发起一次SQL请求

# 所以上面的方法在效率上是不高的,要两次SQL请求。使用已经掌握的方法,可以这样做
users = models.User.objects.all().values(‘name‘, ‘group__group_name‘)  # 这样用values指定获取哪些字段,包括跨表的字段,就是一次SQL请求了

values()方法有缺点,因为返回的是字典。如果要继续返回QuerySet对象的话,就不行了。
select_related()方法

users1 = models.User.objects.all()  # 默认值获取当前这张表的数据
users2 = models.User.objects.all().select_ralated()  # 把与当前表关联的数据也一次性都获取到了
# 上面虽然方便,但是把不需要的关联数据也获取到同样不好,所以可以加参数,指定要获取哪个关联数据
users3 = models.User.objects.all().select_ralated(‘group’)

prefetch_related()方法
上面的方法只发起了一次SQL请求,通过这个来提高效率的。通过使用prefetch_related()方法也可以提高效率,实现的方法和上面不同:

users3 = models.User.objects.all().prefetch_related(‘group’)

首先会发起一次SQL请求获取User表的数据,这里是不跨表的。
然后再发起一次SQL请求,把获取到的数据里的Group表里的id,去Group表里获取一次。
如果参数不是一个,而是多个,那么每个关联表都会发起一次SQL请求来获取关联的数据的id。
上面一共是2次SQL请求,不跨表,所有相关的数据都放内存里了,之后就不会再发起SQL请求了。
小结:上面的2个提高效率的方法,用哪一个或者一个都不用,不影响我们的代码的实现。只是在获取关联表的数据的时候,有不同的SQL请求的策略。最终获取到的数据还是一样的,用起来也一样。

其他操作2

接着上面的其他操作里的源码继续往下看,截取一段如下:

    ##################################################
    # PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS #
    ##################################################

    def raw(self, raw_query, params=None, translations=None, using=None):
        if using is None:
            using = self.db
        return RawQuerySet(raw_query, model=self.model, params=params, translations=translations, using=using)

具体就是有一下的方法:

  • def raw(self, raw_query, params=None, translations=None, using=None): :执行原生SQL
  • def values(self, *fields, **expressions): :获取每行数据为字典格式
  • def values_list(self, *fields, flat=False, named=False): :获取每行数据为元祖
  • def dates(self, field_name, kind, order=‘ASC‘): :根据时间的某一部分进行去重查找并截取指定内容
  • def datetimes(self, field_name, kind, order=‘ASC‘, tzinfo=None): :根据时间的某一部分进行去重查找并截取指定内容,将时间转换为指定时区时间
  • def none(self): :返回一个空的 QuerySet 对象,貌似没啥用

日期和时间(dates)

def dates(self, field_name, kind, order=‘ASC‘): 函数里的各个参数如下:

  • field_name :数据库表里的字段名
  • kind :只能是"year"(年), "month"(年-月), "day"(年-月-日)
  • order :只能是,"ASC" 或 "DESC" ,就是对结果进行排序
    models.DatePlus.objects.dates(‘ctime‘, ‘day‘, ‘DESC‘)

    上面的只有日期,没有时间,下面的可以做更细力度的处理
    def datetimes(self, field_name, kind, order=‘ASC‘, tzinfo=None): 额外的参数如下:

  • kind :只能是 "year", "month", "day", "hour", "minute", "second"
  • tzinfo :时区对象
    models.UserInfo.objects.datetimes(‘ctime‘, ‘hour‘, tzinfo=pytz.UTC)  # UTC时间,这个不重要
    models.UserInfo.objects.datetimes(‘ctime‘, ‘hour‘, tzinfo=pytz.timezone(‘Asia/Shanghai‘))  # 使用我们这边的时区

    上面用到了pytz这个模块,如果没有这个模块的还得安装一下。不过貌似安装django的时候默认就把这个也安装上了。如果不知道自己的时区具体是什么字符串,下面的方法可以列出所有的时区的字符串(稍微有点多):

    >>> import pytz
    >>> pytz.all_timezones

执行原生SQL语句(raw)

def raw(self, raw_query, params=None, translations=None, using=None):

# 执行原生SQL
models.UserInfo.objects.raw(‘select * from userinfo‘)

现在有一张User表,有3列数据:id、usrname、password
还有一张UserInfo表,有这些列:uid、name、email

# 如果SQL是其他表时,必须将名字设置为当前UserInfo对象的主键列名
models.Tb.objects.raw(‘select id as nid from 其他表‘)
# 对应上面的两张表的情况可以这样写
models.User.objects.raw(‘select uid as id, name as username, email as password from userinfo‘)
# 同样的效果也可以这样实现
name_map = {‘id‘: ‘uid‘, ‘username‘: ‘name‘, ‘password‘: ‘email‘}
models.User.objects.raw(‘select * from userinfo‘, translations=name_map)

上面的代码,就是去查UserInfo表里的数据,然后把里面的字段名设置一个对应User表里的别名,这样查到的数据就可以作为User表的数据对象了。之后save()一下应该就可以添加到User表里了,或者是其他的方法。
另外还支持使用占位符(%s)来添加参数:

name_map = {‘id‘: ‘uid‘, ‘username‘: ‘name‘, ‘password‘: ‘email‘}
models.UserInfo.objects.raw(‘select * from userinfo where uid>%s‘, params=[12,], translations=name_map)

using=None :这个参数是指定数据库,之前已经讲过了

其他操作3

最后还有一部分方法,开始的位置如下:

    ####################################
    # METHODS THAT DO DATABASE QUERIES #
    ####################################

    def _iterator(self, use_chunked_fetch, chunk_size):
        yield from self._iterable_class(self, chunked_fetch=use_chunked_fetch, chunk_size=chunk_size)
  • def aggregate(self, *args, **kwargs): :聚合函数,获取字典类型聚合结果。这个是对整个表做聚合
  • def count(self): :获取个数
  • def get(self, *args, **kwargs): :获取单个对象
  • def create(self, **kwargs): :创建对象
  • def bulk_create(self, objs, batch_size=None): :批量插入,batch_size表示一次插入的个数
  • def get_or_create(self, defaults=None, **kwargs): :如果存在,则获取,否则,创建。defaults 指定创建时,其他字段的值
  • def update_or_create(self, defaults=None, **kwargs): :如果存在,则更新,否则,创建。defaults 指定创建时,其他字段的值
  • def first(self): :获取第一个
  • def last(self): :获取最后一个
  • def in_bulk(self, id_list=None, *, field_name=‘pk‘): :根据主键ID进行查找。
  • def delete(self): :删除
  • def update(self, **kwargs): :更新
  • def exists(self): :是否存在

例子

聚合函数

from django.db.models import Count, Avg, Max, Min, Sum
result = models.UserInfo.objects.aggregate(n=Count(‘nid‘))  # 相当于下面这句原生SQL的效果
# SELECT COUNT(nid) AS n FROM userinfo
# 也可以统计多个数据,返回的是字典
result = models.UserInfo.objects.aggregate(k=Count(‘u_id‘, distinct=True), n=Count(‘nid‘))  # 前面那个是去重的,就是先出重再聚合
>>> {‘k‘: 3, ‘n‘: 4}

批量插入
def bulk_create(self, objs, batch_size=None):
批量插入的好处是减少连接数据库的次数,原本可能是插入一条数据要连接一次数据库,现在一次连接可以插入多条数据

objs = {
    models.UserInfo(name=‘Adam‘, email=‘[email protected]‘),
    models.UserInfo(name=‘Bob‘, email=‘[email protected])
}
models.UserInfo.objects.bulk_create(objs, 10)

batch_size参数是限制一次插入的记录数,例子中要插入的记录小于batch_size,就是一次操作。如果大于batch_size,那么一次插入一批,然后剩下的再连接一次数据库再插入一批,知道全部完成。避免一次插入过多的数据。
get_or_create 和 update_or_create
如果记录存在,就是普通的查询,返回查询获取到的数据。
如果记录不存在,则插入一条新数据并获取到这条新数据返回。新插入的数据的其他字段的值写在defaults里。
这里有2个返回值,第一个是获取到的对象。第二个是这个对象是否是创建的。如果记录存在,返回的是False,这条不是创建的。

obj, created = models.UserInfo.objects.get_or_create(username=‘Tom‘, defaults={‘email‘: ‘[email protected]‘,‘u_id‘: 2})
obj, created = models.UserInfo.objects.update_or_create(username=‘Tom‘, defaults={‘email‘: ‘[email protected]‘,‘u_id‘: 2})

根据主键id进行查找
def in_bulk(self, id_list=None, *, field_name=‘pk‘): 旧版本的django可能是这样的def in_bulk(self, id_list=None): ,下面是旧版的例子,不过第一个参数没变:

id_list = [1, 2 ,3]
models.User.objects.in_bulk(id_list)
# 相当于下面的这句,所以不会也没关系
models.User.objects.filter(id__in=id_list)

数据验证及钩子

model本身也有一部分的数据验证的功能,这部分的功能比较弱,但是还是来了解一下
首先建一个比较简单的表结构,

# models.py 文件
class UserInfo(models.Model):
    name = models.CharField(max_length=32)
    email = models.EmailField()  # 这里的EmailFiled是希望之后能够做数据验证的

然后就是往表里添加数据了,添加数据有2中方法:

models.UserInfo.objects.create(name=‘Bob‘, email=‘bob‘)  # 这种方法无法进行数据验证
obj = models.UserInfo(name=‘Bob‘, email=‘bob‘)  # 先创建一个对象,然后在送去save()之前,先调用下面的方法进行数据验证
obj.full_clean()  # 对这个数据对象进行验证,这里验证不通过会抛出异常,所以用的话要写一个try
# 会抛出一个异常,在下一行
# django.core.exceptions.ValidationError: {‘email‘: [‘Enter a valid email address.‘]}
obj.save()  # 创建数据

上面进行数据验证是调用一个full_clean()方法。而在这个full_clean()里面,还会调用一个self.clean()方法。上面如何捕获异常正好参考这里

        try:
            self.clean()
        except ValidationError as e:
            errors = e.update_error_dict(errors)

而这个clean()方法里面是空的:

    def clean(self):
        """
        Hook for doing any extra model-wide validation after clean() has been
        called on every field by self.clean_fields. Any ValidationError raised
        by this method will not be associated with a particular field; it will
        have a special-case association with the field defined by NON_FIELD_ERRORS.
        """
        pass

上面的 full_clean() 方法 和 clean() 方法都是Model这个类里提供方法。而这个clean()是django留给我们的一个钩子,让我们自己可以重构这个方法,从而实现自定义的数据验证:

from django.core.exceptions import ValidationError  # 导入这个异常,之后由问题要抛出这个异常
class UserInfo(models.Model):
    name = models.CharField(max_length=32)
    email = models.EmailField()

    # 重构数据验证的clean方法
    def clean(self):
        if self.name == ‘Bob‘:
            # 如果验证不通过我们要抛出ValidationError这个异常
            raise ValidationError(message={‘name‘: "用户名不能使用Bob"})
        if self.email == ‘bob‘:
            # 随便写两个异常,但是上面已经抛出异常了,并不会执行到这里
            raise ValidationError(message={‘email‘: "email能使用bob"})

再次尝试写入数据库,这次抛出的异常的信息如下:

django.core.exceptions.ValidationError: {‘email‘: [‘Enter a valid email address.‘], ‘name‘: [‘用户名不能使用Bob‘]}

full_clean()方法里,是先 try self.clean_fields(exclude=exclude) 对每一个字段进行验证,然后再 try self.clean() 我们自定义的验证的。
另外,try self.clean_fields(exclude=exclude) 验证的时候,不会验证出一个错误后直接抛出异常,而是把所有的验证都完成,包括之后的 try self.clean() 的验证,所有验证都完成之后再抛出异常。把所有的验证不通过的信息一次全部抛出。
但是我们自己写的 self.clean()方法按照上面的写法,则是一旦验证出错误就直接抛出异常了,如果有多个验证也不会验证后面的了。这里可以参考 clean_fields()方法的写法,把我们的clean()方法改的好一点:

from django.core.exceptions import ValidationError  # 导入这个异常,之后由问题要抛出这个异常
class UserInfo(models.Model):
    name = models.CharField(max_length=32)
    email = models.EmailField()

    # 重构数据验证的clean方法
    def clean(self):
        errors = {}  # 这里用来存放我们的异常要抛出的字典
        if self.name == ‘Bob‘:
            errors[‘name‘] = "用户名不能使用Bob"  # 检测到异常先把信息添加到字典里去
        if self.email == ‘bob‘:
            errors[‘email‘] = "email能使用bob"

        # 最后检查errors字典,如果有内容,则抛出异常,message参数的值就是errors字典
        if errors:
            raise ValidationError(errors)

最后抛出的异常,是把所有验证的内容都验证了一遍,然后一次全部抛出的。包括我们自己写的clean()方法的多个验证错误

django.core.exceptions.ValidationError: {‘email‘: [‘Enter a valid email address.‘, ‘email能使用bob‘], ‘name‘: [‘用户名不能使用Bob‘]}

从结果分析,返回的错误信息是一个列表,如果一个字段有多条错误信息,那么是一条一条列在这个列表里的。我们自己重构的clean()方法里,如果对一个字段进行了多次验证,那么字典的value值(取出我们的错误信息)要写成一个列表。即使我们使用的不是列表而是字符串,最后抛出异常的时候也是把字符串转换成一个只有一个字符串成员的列表的。

以上是关于Python自动化开发学习23-Django上(Model)的主要内容,如果未能解决你的问题,请参考以下文章

Python自动化开发学习12-MariaDB

Python自动化开发学习22-Django上

Django与MySQL数据库版本兼容问题

Python自动化开发学习12-堡垒机开发

Django 开发投票系统

Python自动化开发学习18-Web前端补充内容