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()