Python / 18 Django ORM 模型的基本增删改查操作

Django ORM 模型的基本增删改查操作

本小节将介绍 Django 的 ORM 模型中对表的增删改查操作,主要针对的是 MySQL 数据库,且操作的表是前面创建的 Member 表。所有的操作将在 Django 的 shell 模式下进行,只需要在 settings.py 中配置好对应的数据库信息即可。

1. Django ORM 模型的增删改查操作

话不多说,直接进入 django 的交互命令模式:

[root@server ~]# pyenv activate django-manual 
pyenv-virtualenv: prompt changing will be removed from future release. configure `export PYENV_VIRTUALENV_DISABLE_PROMPT=1' to simulate the behavior.
(django-manual) [root@server ~]# cd django-manual/first_django_app/
(django-manual) [root@server first_django_app]# python manage.py shell
Python 3.8.1 (default, Dec 24 2019, 17:04:00) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

前面在 hello_app 应用目录下的 models.py 中定义了 Member 模型类,导入进来。然后我们实例化 Member 类,并给实例的属性赋值,最后调用模型类的 save() 方法,将该实例保存到表中:

>>> from hello_app.models import Member
>>> from hello_app.models import Member
>>> m1 = Member()
>>> m1.name = 'spyinx'
>>> m1.age = 29
>>> m1.sex = 0
>>> m1.occupation = "程序员"
>>> m1.phone_num = '18054293763'
>>> m1.city = 'guangzhou'
>>> m1.save()

通过 mysql 客户端可以查看该保存的记录,如下:

[root@server first_django_app]# mysql -u store -pstore.123@ -h 180.76.152.113 -P 9002
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 73555
Server version: 5.7.26 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> use django_manual
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MySQL [django_manual]> select * from member where 1=1\G;
*************************** 1. row ***************************
           id: 1
         name: spyinx
          age: 29
          sex: 0
   occupation: 程序员
    phone_num: 18054293763
        email: 
         city: guangzhou
register_date: 2020-04-05 07:30:45.043377
1 row in set (0.00 sec)

ERROR: No query specified

MySQL [django_manual]>

接下来是查询的操作介绍,为了能更好的演示查询操作,我们通过如下代码在 member 中添加100条记录:

from datetime import datetime
import random
import MySQLdb

occupations = ['web', 'server', 'ops', 'security', 'teacher', 'ui', 'product', 'leader']
cities = ['beijing', 'guangzhou', 'shenzhen', 'shanghai', 'wuhan']

def gen_phone_num():
    phone_num = "18"
    for i in range(9):
        phone_num += str(random.randint(0, 9))
    return phone_num

conn = MySQLdb.connect(host='180.76.152.113', port=9002, user='store', passwd='store.123@', db='django_manual')
conn.autocommit(True)
data = (('spyinx-%d' % i,        \
         random.randint(20, 40), \
         random.randint(0, 1),   \
         occupations[random.randint(0, len(occupations) - 1)], \
         gen_phone_num(),        \
         '22%d@qq.com' % i,      \
         cities[random.randint(0, len(cities) - 1)], \
         datetime.now().strftime("%Y-%m-%d %H:%M:%S")) for i in range(100))

try:
    cursor = conn.cursor()
    cursor.executemany('insert into member(`name`, `age`, `sex`, `occupation`, `phone_num`, `email`, `city`, `register_date`) values (%s, %s, %s, %s, %s, %s, %s, %s);', data)
    print('批量插入完成')
except Exception as e:
    print('插入异常,执行回滚动作: {}'.format(str(e)))
    conn.rollback()
finally:
    if conn:
        conn.close()

执行 python 代码后,我们通过 mysql 客户端确认100条数据已经成功插入到数据库中:

MySQL [django_manual]> select count(*) from member where 1=1\G;
*************************** 1. row ***************************
count(*): 101
1 row in set (0.00 sec)

我们执行如下操作:

>>> type(Member.objects)
<class 'django.db.models.manager.Manager'>
>>> Member.objects.get(name='spyinx')
<Member: <spyinx, 18054293763>>
>>> Member.objects.all().count()
101
>>> type(Member.objects.all())
<class 'django.db.models.query.QuerySet'>

上面的语句中 Member.objects.get(name='spyinx') 中,objects 是一个特殊的属性,通过它来查询数据库,它是模型的一个 Manager。首先来看看这个 Manager 类提供的常用方法:

  • all():查询所有结果,返回的类型为 QuerySet 实例;
  • filter(**kwargs):根据条件过滤查询结果,返回的类型为 QuerySet 实例;
  • get(**kwargs):返回与所给筛选条件相匹配的记录,只返回一个结果。如果符合筛选条件的记录超过一个或者没有都会抛出错误,返回的类型为模型对象实例;
  • exclude(**kwargs):和 filter() 方法正好相反,筛选出不匹配的结果,返回的类型为 QuerySet 实例;
  • values(*args):返回一个ValueQuerySet,一个特殊的QuerySet,运行后得到的并不是一系列 model 的实例化对象,而是一个可迭代的字典序列;
  • values_list(*args):它与 values() 类似,只不过 values_list() 返回的是一个元组序列,而 values() 返回的是一个字典序列;
  • order_by(*args):对结果按照传入的字段进行排序,返回的类型为 QuerySet 实例;
  • reverse():对查询结果反向排序,返回的类型为 QuerySet 实例;
  • distinct():去掉查询结果中重复的部分,返回的类型为 QuerySet 实例;
  • count():返回数据库中匹配查询的记录数,返回类型为 int;
  • first():返回第一条记录,结果为模型对象实例;;
  • last():返回最后一条记录,结果为模型对象实例;
  • exists():如果 QuerySet 包含数据,就返回 True,否则返回 False。

如果上述这些方法的返回结果是一个 QuerySet 实例,那么它也同样具有上面这些方法,因此可以继续调用,形成链式调用,示例如下:

>>> Member.objects.all().count()
101
>>> Member.objects.all().reverse().first()
<Member: <spyinx-99, 18022422977>>

此外,在 filter() 方法中还有一些比较神奇的双下划线辅助我们进一步过滤结果:

MySQL [django_manual]> select id, name, phone_num from member where name like 'spyinx-2%';
+----+-----------+-------------+
| id | name      | phone_num   |
+----+-----------+-------------+
| 24 | spyinx-2  | 18627420378 |
| 42 | spyinx-20 | 18687483216 |
| 43 | spyinx-21 | 18338528387 |
| 44 | spyinx-22 | 18702966393 |
| 45 | spyinx-23 | 18386787195 |
| 46 | spyinx-24 | 18003292724 |
| 47 | spyinx-25 | 18160946579 |
| 48 | spyinx-26 | 18517339819 |
| 49 | spyinx-27 | 18575613014 |
| 50 | spyinx-28 | 18869175798 |
| 51 | spyinx-29 | 18603950130 |
+----+-----------+-------------+
11 rows in set (0.00 sec)
>>> Member.objects.all().filter(name__contains='spyinx-2')
<QuerySet [<Member: <spyinx-2, 18627420378>>, <Member: <spyinx-20, 18687483216>>, <Member: <spyinx-21, 18338528387>>, <Member: <spyinx-22, 18702966393>>, <Member: <spyinx-23, 18386787195>>, <Member: <spyinx-24, 18003292724>>, <Member: <spyinx-25, 18160946579>>, <Member: <spyinx-26, 18517339819>>, <Member: <spyinx-27, 18575613014>>, <Member: <spyinx-28, 18869175798>>, <Member: <spyinx-29, 18603950130>>]>
>>> Member.objects.all().filter(name__contains='spyinx-2').filter(id__lt=47, id__gt=42)
<QuerySet [<Member: <spyinx-21, 18338528387>>, <Member: <spyinx-22, 18702966393>>, <Member: <spyinx-23, 18386787195>>, <Member: <spyinx-24, 18003292724>>]>

这种双下划线的过滤字段有:

  • contains/icontains:过滤字段的值包含某个字符串的结果;

  • in:和 SQL 语句中的 in 类似,过滤字段的值在某个列表内的结果,比如:

    >>> Member.objects.all().filter(name__contains='spyinx-2').filter(id__in=[42, 43])
    <QuerySet [<Member: <spyinx-20, 18687483216>>, <Member: <spyinx-21, 18338528387>>]>
    
  • lt/gt:过滤字段值小于或者大于某个值的结果;

  • range:过滤字段值在某个范围内的结果;

    >>> Member.objects.all().filter(name__contains='spyinx-2').filter(id__range=[48, 50])
    <QuerySet [<Member: <spyinx-26, 18517339819>>, <Member: <spyinx-27, 18575613014>>, <Member: <spyinx-28, 18869175798>>]>
    
  • startswith/istartswith:匹配字段的值以某个字符串开始,前面的 i 标识是否区分大小写;

  • endswith/iendswiths:匹配字段的值以某个字符串结束;

    >>> Member.objects.all().filter(id__gt=90).filter(name__endswith='2')
    <QuerySet [<Member: <spyinx-72, 18749521006>>, <Member: <spyinx-82, 18970386795>>, <Member: <spyinx-92, 18324708274>>]>
    

F查询和Q查询

前面我们构造的过滤器都只是将字段值与某个常量做比较。如果我们要对两个字段的值做比较,就需要使用 Django 提供 F() 来做这样的比较。F() 的实例可以在查询中引用字段,来比较同一个 model 实例中两个不同字段的值:

>>> from django.db.models import Q
# 找出id值大于age*4的记录
>>> Member.objects.all().filter(id__gt=F('age')*4)
<QuerySet [<Member: <spyinx-70, 18918359267>>, <Member: <spyinx-77, 18393464230>>, <Member: <spyinx-90, 18272147421>>, <Member: <spyinx-91, 18756265752>>, <Member: <spyinx-92, 18324708274>>, <Member: <spyinx-97, 18154031313>>]>

# 将所有记录中age字段的值加1
>>> Member.objects.all().update(age=F('age')+1)
101

此外,前面的多个 filter() 方法实现的是过滤条件的 “AND” 操作,如果想实现过滤条件 “OR” 操作呢,就需要使用到 Django 为我们提供的 Q() 方法:

>>> from django.db.models import Q
# 过滤条件的 OR 操作
>>> Member.objects.all().filter(Q(name='spyinx-22') | Q(name='spyinx-11'))
<QuerySet [<Member: <spyinx-11, 18919885274>>, <Member: <spyinx-22, 18702966393>>]>
# 过滤条件的 AND 操作
>>> Member.objects.all().filter(Q(name__contains='spyinx-2') & Q(name__endswith='2'))
<QuerySet [<Member: <spyinx-2, 18627420378>>, <Member: <spyinx-22, 18702966393>>]>

对于记录的更新和删除操作,我们同样有对应的 update() 方法以及 delete() 方法:

# 删除name=spyinx的记录
>>> Member.objects.all().filter(Q(name='spyinx')).delete()
(1, {'hello_app.Member': 1})
>>> Member.objects.all().count()
100
# 所有记录的年龄字段加1
>>> Member.objects.all().update(age=F('age')+1)
101

2. Django 自定义管理器

前面我们提到 objects 是一个特殊的属性, 它是模型的一个 Manager。接下来我们操作下如何自定义 Manager 以及自定义查询方法。我们在 hello_app 应用目录下的 models.py 文件中添加一个 MemberManager 类:

# hello_app/models.py
from django.db import models

# Create your models here.
class MemberManager(models.Manager):
    def middle_age(self, age=30):
        return self.filter(age__gt=age)

class Member(models.Model):
    sex_choices = (
        (0, '男'),
        (1, '女'),
    )
    name = models.CharField('姓名', max_length=30)
    age = models.CharField('年龄', max_length=30)
    sex = models.SmallIntegerField('申请状态', choices=sex_choices, default=0)
    ...
    
    # 使用新的 Manager
    objects = MemberManager()

    ...

这样子,我们来使用下这个新增的方法,如下:

(django-manual) [root@server first_django_app]# python manage.py shell
Python 3.8.1 (default, Dec 24 2019, 17:04:00) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from hello_app.models import Member
# 筛选出年龄大于40的记录
>>> Member.objects.middle_age(40)
<QuerySet [<Member: <spyinx-0, 18103841793>>, <Member: <spyinx-3, 18586383292>>, <Member: <spyinx-4, 18492437671>>, <Member: <spyinx-17, 18681960581>>, <Member: <spyinx-19, 18211435798>>, <Member: <spyinx-25, 18160946579>>, <Member: <spyinx-31, 18880660482>>, <Member: <spyinx-47, 18337991495>>, <Member: <spyinx-48, 18191766331>>, <Member: <spyinx-51, 18213698092>>, <Member: <spyinx-55, 18093331199>>, <Member: <spyinx-67, 18100571566>>, <Member: <spyinx-87, 18053563269>>]>

此外,我们也可以对这个管理器进行命名和重写 Manager 中查询的 QuerySet:

from django.db import models

class MemberManager(models.Manager):
    def get_queryset(self):
        return super(MemberManager, self).get_queryset().filter(name__contains='spyinx-2')

    def middle_age(self, age=30):
        return self.filter(age__gt=age)

class Member(models.Model):
    sex_choices = (
        (0, '男'),
        (1, '女'),
    )
    name = models.CharField('姓名', max_length=30)
    age = models.CharField('年龄', max_length=30)
    ...

    objects = models.Manager()
    # 自定义Manager
    custom_objects = MemberManager()
    
    ...

此时模型有两个 Manager, 一个是 objects,另一个是我们自定义的 custom_objects。使用如下:

>>> from hello_app.models import Member
>>> Member.objects.all().count()
100
>>> Member.custom_objects.all().count()
11
>>>

这里也可以看到,我们自定义的 get_queryset() 方法也生效了。

3. 小结

本节中我们介绍了 Django 内嵌 ORM 模型的增删改查操作,主要是针对查询操作介绍了各种过滤方式,包括 F 查询和 Q 查询。接下来还介绍了如何自定义管理器,对经常使用的查询进一步封装,进一步释放重复代码。