Django ORM 多表查询

4,251 阅读4分钟

www.python3.vip/tut/webdev/…

ORM 对关联表的操作

# 国家表
class Country(models.Model):
    name = models.CharField(max_length=100)

# 学生表, country 字段是国家表的外键,形成一对多的关系
class Student(models.Model):
    name    = models.CharField(max_length=100)
    grade   = models.PositiveSmallIntegerField()
    country = models.ForeignKey(Country,
                                on_delete=models.PROTECT)

国家表记录了国家名字,学生表记录了学生名、年级和所在的国家。

外键表字段访问(s1=子表对象,s1.country.name就可以得到国家名)

如果你已经获取了一个student对象,要得到他的国家名称只需这样

s1 = Student.objects.get(name='白月')
s1.country.name

外键表字段过滤(查找一年级中国的学生:Student.objects.filter(grade=1,country__name='中国').values())

如果,我们要查找Student表中所有 一年级 学生,很简单

Student.objects.filter(grade=1).values()

如果现在,我们要查找Student表中所有 一年级中国 学生,该怎么写呢?

不能这么写:

Student.objects.filter(grade=1,country='中国')

因为,Student表中 country 并不是国家名称字符串字段,而是一个外键字段,其实是对应 Country 表中 id 字段 。

可能有的朋友会这样想:我可以先获取中国的国家id,然后再通过id去找,像这样

cn = Country.objects.get(name='中国')
Student.objects.filter(grade=1,country_id=cn.id).values()

注意外键字段的id是通过后缀 _id 获取的。(注意是单杠)

或者这样,也是可以的

cn = Country.objects.get(name='中国')
Student.objects.filter(grade=1,country=cn).values()

上面的方法,写起来麻烦一些,有两步操作。而且需要发送两次数据请求给数据库服务,性能不高。

其实,Django ORM 中,对外键关联,有更方便的语法。

可以这样写

Student.objects.filter(grade=1,country__name='中国').values()

写起来简单,一步到位,而且只需要发送一个数据库请求,性能更好。

如果返回结果只需要 学生姓名 和 国家名两个字段,可以这样指定values内容

Student.objects.filter(grade=1,country__name='中国')\
     .values('name','country__name')

但是这样写有个问题:选择出来的记录中,国家名是 country__name 。 两个下划线比较怪。

有时候,前后端接口的设计者,定义好了接口格式,如果要求一定是 countryname 这样怎么办?

可以使用 annotate 方法将获取的字段值进行重命名,像下面这样

from django.db.models import F

# annotate 可以将表字段进行别名处理
Student.objects.annotate(
    countryname=F('country__name'),
    studentname=F('name')
    )\
    .filter(grade=1,countryname='中国').values('studentname','countryname')

这样,就将原来的country__name别名为countryname,原来的name别名为studentname,然后后面的filter和values也是用的这个别名.

外键表反向访问

如果你已经获取了一个Country对象,如何访问到所有属于这个国家的学生呢?

cn = Country.objects.get(name='中国')
cn.student_set.all()

通过 表Model名转化为小写 ,后面加上一个 _set获取所有的反向外键关联对象

这里的语义是,根据中国的cn来反向获得所有的中国学生. Django还给出了一个方法,可以更直观的反映 关联关系——在定义Model的时候,外键字段使用 related_name 参数,像这样:

# 国家表
class Country(models.Model):
    name = models.CharField(max_length=100)

# country 字段是国家表的外键,形成一对多的关系
class Student(models.Model):
    name    = models.CharField(max_length=100)
    grade   = models.PositiveSmallIntegerField()
    country = models.ForeignKey(Country,
                on_delete = models.PROTECT,
                # 指定反向访问的名字
                related_name='students')

就可以使用更直观的属性名,像这样

cn = Country.objects.get(name='中国')
cn.students.all()

外键表反向过滤(获取所有 具有一年级学生 的国家Country.objects.filter(student__grade=1).values())

如果我们要获取所有 具有一年级学生 的国家名,该怎么写?

当然可以这样:

# 先获取所有的一年级学生id列表
country_ids = Student.objects.filter(grade=1).\
values_list('country', flat=True)

# 再通过id列表使用  id__in  过滤
Country.objects.filter(id__in=country_ids).values()

但是这样同样存在 麻烦 和性能的问题。

Django ORM 可以这样写

Country.objects.filter(students__grade=1).values()

这里之所以是students,是因为我们前面的反向访问中已经规定了related_name为students. 这里就表示student表中的grade字段=1去过滤. 总结:反向的操作,不管是反向访问还是过滤,都是用的给子表取的related_name. 注意, 因为,我们定义表的时候,用 related_name='students' 指定了反向关联名称 students ,所以这里是 students__grade 。 使用了反向关联名字。

如果定义时,没有指定related_name, 则应该使用 表名转化为小写 ,就是这样

Country.objects.filter(student__grade=1).values()

但是,我们发现,这种方式,会有重复的记录产生,如下:

<QuerySet [{'id': 1, 'name': '中国'}, {'id': 1, 'name': '中国'},
{'id': 2, 'name': '美国'}, {'id': 2, 'name': '美国'}]>

(?????????迷惑?????怎么会有重复啊???之后我去测试一下)

可以使用 .distinct() 去重

Country.objects.filter(students__grade=1).values().distinct()

注意:据说 .distinct()对MySQL数据库无效,我没有来得及验证。实测 SQLite,Postgresql有效。

总结

  • 过滤,无论是正向过滤还是反向过滤,Django中的写法是完全一样的,就是表名小写__字段名怎么怎么样。
  • 访问: 正向访问,即直接用外键去访问那个外键表,就是简单的直接访问:
s1 = Student.objects.get(name='白月')
s1.country.name

反向访问,就是用这个外键去反向访问那个子表:

cn = Country.objects.get(name='中国')
cn.student_set.all()

这里的写法就是:外键对象.子表名小写_set.all()