Django ORM 外键使用
外键 (Foreign Key)是用于建立和加强两个表数据之间的链接的一列或多列。通过将保存表中主键值的一列或多列添加到另一个表中,可创建两个表之间的连接,这个列就成为第二个表的外键。外键的作用如下:
保持数据一致性,完整性,主要目的是控制存储在外键表中的数据。 使两张表形成关联,就是当你对一个表的数据进行操作,和他有关联的一个或更多表的数据能够同时发生改变。
外键可以是一对一的,一个表的记录只能与另一个表的一条记录连接,或者是一对多的,一个表的记录与另一个表的多条记录连接。
在 MySQL 种想使用外键需要具备一定条件的:
MySQL 重需要关联的表必须都使用 InnoDB 引擎创建,MyISAM 表暂时不支持外键;
外键列必须建立了索引,MySQL 4.1.2 以后的版本在建立外键时会自动创建索引,但如果在较早的版本则需要显式建立;
外键关系的两个表的列必须是数据类型相似,也就是可以相互转换类型的列,比如 int 和 tinyint 可以,而 int和char 则不可以。
最后我们来了解下在 MySQL 中创建外键的用法,如下:
[CONSTRAINT symbol] FOREIGN KEY [id] (index_col_name, ...)REFERENCES tbl_name (index_col_name, ...)[ON DELETE {RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT}][ON UPDATE {RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT}]
该语法可以在 CREATE TABLE 和 ALTER TABLE 时使用,如果不指定 CONSTRAINT symbol,MySQL 会自动生成一个名字。其中 ON DELETE、ON UPDATE 表示事件触发限制,可设参数:
RESTRICT:限制外表中的外键改动,默认值;
CASCADE:跟随外键改动;
SET NULL:设空值;
SET DEFAULT:设默认值;
NO ACTION:无动作,默认的。
例如下面的 SQL 语句是由 Django 来帮我们自动生成 nember 和 vip_level 的:
CREATE TABLE `member` ( `id` int() NOT NULL AUTO_INCREMENT, `name` varchar() NOT NULL, `age` varchar() NOT NULL, `sex` smallint() NOT NULL, `occupation` varchar() NOT NULL, `phone_num` varchar() NOT NULL, `email` varchar() NOT NULL, `city` varchar() NOT NULL, `register_date` datetime() NOT NULL, `vip_level_id` int() DEFAULT NULL, PRIMARY KEY (`id`), KEY `member_vip_level_id_44ba3146_fk_vip_level_id` (`vip_level_id`), CONSTRAINT `member_vip_level_id_44ba3146_fk_vip_level_id` FOREIGN KEY (`vip_level_id`) REFERENCES `vip_level` (`id`)) ENGINE=InnoDB AUTO_INCREMENT= DEFAULT CHARSET=utf8;
1. Django ORM 中外键的使用
为了能演示 ORM 中外键的使用,我们在前面的会员 Member 的基础上新增一个关联表:会员等级表(vip_level)。这个会员等级有 VIP、VVIP 以及超级 VIP 的 VVVIP 三个等级,我们在 models.py 中添加如下模型类,并在会员表中添加对应的外键字段,连接到会员等级表中:
# hello_app/models.py# ...class VIPLevel(models.Model):name = models.CharField('会员等级名称', max_length=)price = models.IntegerField('会员价格,元/月', default=)remark = models.TextField('说明', default=暂无信息)def __str__(self):return <%s> % (self.name)class Meta:db_table = 'vip_level'class Member(models.Model):# ...# 添加外键字段vip_level = models.ForeignKey('VIPLevel', on_delete=models.CASCADE, verbose_name='vip level')# ...# ...
首先,我们需要把前面生成的 Member 表删除,同时删除迁移记录文件,操作如下:
(django-manual) [root@server first_django_app]# pwd /root/django-manual/first_django_app # 删除迁移记录表 (django-manual) [root@server first_django_app]# rm -f hello_app/migrations/0001_initial.py
此外,还需要将数据库中的原 member 表、django_migrations 表删除,即还原到最初状态。接下来,我们使用数据库迁移命令:
(django-manual) [root@server first_django_app]# python manage.py makemigrations Migrations for 'hello_app': hello_app/migrations/0001_initial.py - Create model VIPLevel - Create model Member (django-manual) [root@server first_django_app]# python manage.py migrate hello_app Operations to perform: Apply all migrations: hello_app Running migrations: Applying hello_app.0001_initial... OK
注意: 如果 migrate 后面不带应用会生成许多 Django 内置应用的表,比如权限表、用户表、Session表等。
上面我们可以看到,我们生成的会员表中相比之前对了一个 vip_level_id 字段,这个字段关联的是 vip_level 表的 id 字段。现在我们首先在 vip_level 中新建三条记录,分别表示 VIP、VVIP 以及 VVVIP:
(django-manual) [root@server first_django_app]# python manage.py shellPython . (default, Dec , ::) [GCC . (Red Hat .-)] on linux Type help, copyright, credits or license for more information.(InteractiveConsole)>>> from hello_app.models import VIPLevel>>> vip = VIPLevel(name='vip', remark='普通vip', price=)>>> vip.save()>>> vvip = VIPLevel(name='vvip', remark='高级vip', price=)>>> vvip.save()>>> vvvip = VIPLevel(name='vvvip', remark='超级vip', price=)>>> vvvip.save()>>> VIPLevel.objects.all()<QuerySet [<VIPLevel: <vip>>, <VIPLevel: <vvip>>, <VIPLevel: <vvvip>>]>
接下来,我们操作 member 表,生成几条记录并关联到 vip_level 表:
>>> from hello_app.models import Member>>> m1 = Member(name='会员1', age=, sex=, occupation='python', phone_num='18054299999', city='guangzhou')>>> m1.vip_level = vip>>> m1.save()>>> m2 = Member(name='会员2', age=, sex=, occupation='java', phone_num='18054299991', city='shanghai')>>> m2.vip_level = vvip>>> m2.save()>>> m3 = Member(name='会员3', age=, sex=, occupation='c/c++', phone_num='18054299992', city='beijing')>>> m3.vip_level = vvvip>>> m3.save()
查看会员表中生成的数据如下:
可以看到,这里我们并没有直接写 vip_level_id 值,而是将 Member 的 vip_level 属性值直接赋值,然后保存。最后 Django 的 ORM 模型在这里会自动帮我们处理这个关联字段的值,找到关联记录的 id 值,并赋值给该字段。接下来,我们看下外键关联的查询操作:
>>> Member.objects.get(age=).vip_level<VIPLevel: <vip>>>>> type(Member.objects.get(age=).vip_level)<class 'hello_app.models.VIPLevel'>>>> vip = VIPLevel.objects.get(name='vip')>>> vip.member_set.all()<QuerySet [<Member: <会员, >>]>>>> type(vip.member_set)<class 'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager.<locals>.RelatedManager'>
上面的操作示例中我们给出了关联表 vip_level (往往成为主表) 和 member (往往成为子表) 之间的正向和反向查询。在 Django 默认每个主表都有一个外键属性,这个属性值为:从表_set,通过这个属性值我们可以查到对应的从表记录,比如上面的 vip.member_set.all()
语句就是查询所有 vip 会员。当然这个外键属性是可以修改的,我们需要在 member 表中的外键字段那里加上一个属性值:
class Member(models.Model):...vip_level = models.ForeignKey('VIPLevel', related_name=new_name, on_delete=models.CASCADE, verbose_name='vip level')...
这样我们想再次通过主表查询子表时,就要变成如下方式了:
>>> from hello_app.models import VIPLevel>>> from hello_app.models import Member>>> vip = VIPLevel.objects.get(name='vip')>>> vip.member_set.all()Traceback (most recent call last): File <console>, line , in <module>AttributeError: 'VIPLevel' object has no attribute 'member_set'>>> vip.new_name.all()<QuerySet [<Member: <会员, >>]>>>>
前面在定义外键时,我们添加了一个 on_delete
属性,这个属性控制着在删除子表外键连接的记录时,对应字表的记录会如何处理,它有如下属性值:
CASCADE:级联操作。如果外键对应的那条记录被删除了,那么子表中所有外键为那个记录的数据都会被删除。对于例中,就是如果我们将会员等级 vip 的记录删除,那么所有 vip 会员会被一并删除;
# 前面使用的正是CASCADE>>> from hello_app.models import VIPLevel>>> from hello_app.models import Member>>> VIPLevel.objects.get(name='vip')<VIPLevel: <vip>>>>> VIPLevel.objects.get(name='vip').delete()(, {'hello_app.Member': , 'hello_app.VIPLevel': })>>> Member.objects.all()<QuerySet [<Member: <会员, >>, <Member: <会员, >>]>
PROTECT:受保护。即只要子表中有记录引用了外键的那条记录,那么就不能删除外键的那条记录。如果我们强行删除,Django 就会报 ProtectedError 异常;
# 修改外键连接的 on_delete 属性值为 PROTECT>>> from hello_app.models import VIPLevel>>> from hello_app.models import Member>>> VIPLevel.objects.get(name='vvip').delete()Traceback (most recent call last): File <console>, line , in <module> File /root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/db/models/base.py, line , in delete collector.collect([self], keep_parents=keep_parents) File /root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/db/models/deletion.py, line , in collect field.remote_field.on_delete(self, field, sub_objs, self.using) File /root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/db/models/deletion.py, line , in PROTECTraise ProtectedError(django.db.models.deletion.ProtectedError: (Cannot delete some instances of model 'VIPLevel' because they are referenced through a protected foreign key: 'Member.vip_level', <QuerySet [<Member: <会员, >>]>)
SET_NULL:设置为空。如果外键的那条数据被删除了,那么子表中所有外键为该条记录的对应字段值会被设置为 NULL,前提是要指定这个字段可以为空,否则也会报错;
# hello_app/models.pyvip_level = models.ForeignKey('VIPLevel', related_name=new_name, on_delete=models.SET_NULL, verbose_name='vip level', null=True)>>> from hello_app.models import VIPLevel>>> from hello_app.models import Member>>> VIPLevel.objects.get(name='vvip').delete()>>> Member.objects.get(name='会员2').vip_level_id is NoneTrue
注意:注意加上null=True是不够的,因为数据库在使用迁移命令时候已经默认是不可为空,这里测试时还需要手动调整下表 vip_level 字段属性,允许为 null。
SET_DEFAULT:设置默认值。和上面类似,前提是字表的这个字段有默认值;
SET():如果外键的那条数据被删除了。那么将会获取SET函数中的值来作为这个外键的值。SET函数可以接收一个可以调用的对象(比如函数或者方法),如果是可以调用的对象,那么会将这个对象调用后的结果作为值返回回去;
# hello_app/models.py# 新增一个设置默认值函数def default_value():# 删除记录时会调用,在这里可以做一些动作# ...# 返回临时指向一条记录的id,返回不存在的id时会报错;返回数字也会报错,要注意return '4'# ...class Member(models.Model):# ...vip_level = models.ForeignKey('VIPLevel', related_name=new_name, on_delete=models.SET(default_value), verbose_name='vip level', null=True)# ...
>>> from hello_app.models import VIPLevel>>> from hello_app.models import Member>>> VIPLevel.objetcs.get(name='会员3').vip_level_id# 新建一个临时过渡vip记录>>> tmp_vip=VIPLevel(name='等待升级vip', price=, remark='临时升级过渡')>>> tmp_vip.save()>>> tmp_vip.id# 删除vvvip记录>>> VIPLevel.objects.all().get(name='vvvip').delete()(, {'hello_app.VIPLevel': } # 可以看到,会员表中曾经指向为vvvip的记录被重新指向了临时过渡vip>>> Member.objects.get(name='会员3').vip_level_id
DO_NOTHING:什么也不做,你删除你的,我保留我的,一切全看数据库级别的约束。在 MySQL 中,这种情况下无法执行删除动作。
2. 小结
本小节中我们描述了外键的相关概念,然后在 Django 的 shell 模式下使用会员表和会员等级表来进行外键的操作,重点演示了关联表之间的创建、相互查询以及删除等相关的操作。