Django 中 ORM 外鍵使用
外鍵 (Foreign Key)是用于建立和加強(qiáng)兩個(gè)表數(shù)據(jù)之間的鏈接的一列或多列。通過將保存表中主鍵值的一列或多列添加到另一個(gè)表中,可創(chuàng)建兩個(gè)表之間的連接,這個(gè)列就成為第二個(gè)表的外鍵。外鍵的作用如下:
保持?jǐn)?shù)據(jù)一致性,完整性,主要目的是控制存儲(chǔ)在外鍵表中的數(shù)據(jù)。 使兩張表形成關(guān)聯(lián),就是當(dāng)你對(duì)一個(gè)表的數(shù)據(jù)進(jìn)行操作,和他有關(guān)聯(lián)的一個(gè)或更多表的數(shù)據(jù)能夠同時(shí)發(fā)生改變。
外鍵可以是一對(duì)一的,一個(gè)表的記錄只能與另一個(gè)表的一條記錄連接,或者是一對(duì)多的,一個(gè)表的記錄與另一個(gè)表的多條記錄連接。
在 MySQL 種想使用外鍵需要具備一定條件的:
- MySQL 重需要關(guān)聯(lián)的表必須都使用 InnoDB 引擎創(chuàng)建,MyISAM 表暫時(shí)不支持外鍵;
- 外鍵列必須建立了索引,MySQL 4.1.2 以后的版本在建立外鍵時(shí)會(huì)自動(dòng)創(chuàng)建索引,但如果在較早的版本則需要顯式建立;
- 外鍵關(guān)系的兩個(gè)表的列必須是數(shù)據(jù)類型相似,也就是可以相互轉(zhuǎn)換類型的列,比如 int 和 tinyint 可以,而 int和char 則不可以。
最后我們來了解下在 MySQL 中創(chuàng)建外鍵的用法,如下:
[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 時(shí)使用,如果不指定 CONSTRAINT symbol,MySQL 會(huì)自動(dòng)生成一個(gè)名字。其中 ON DELETE、ON UPDATE 表示事件觸發(fā)限制,可設(shè)參數(shù):
- RESTRICT:限制外表中的外鍵改動(dòng),默認(rèn)值;
- CASCADE:跟隨外鍵改動(dòng);
- SET NULL:設(shè)空值;
- SET DEFAULT:設(shè)默認(rèn)值;
- NO ACTION:無動(dòng)作,默認(rèn)的。
例如下面的 SQL 語句是由 Django 來幫我們自動(dòng)生成 nember 和 vip_level 的:
CREATE TABLE `member` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(30) NOT NULL,
`age` varchar(30) NOT NULL,
`sex` smallint(6) NOT NULL,
`occupation` varchar(30) NOT NULL,
`phone_num` varchar(14) NOT NULL,
`email` varchar(254) NOT NULL,
`city` varchar(30) NOT NULL,
`register_date` datetime(6) NOT NULL,
`vip_level_id` int(11) 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=4 DEFAULT CHARSET=utf8;
1. Django ORM 中外鍵的使用
為了能演示 ORM 中外鍵的使用,我們?cè)谇懊娴臅?huì)員 Member 的基礎(chǔ)上新增一個(gè)關(guān)聯(lián)表:會(huì)員等級(jí)表(vip_level)。這個(gè)會(huì)員等級(jí)有 VIP、VVIP 以及超級(jí) VIP 的 VVVIP 三個(gè)等級(jí),我們?cè)?models.py 中添加如下模型類,并在會(huì)員表中添加對(duì)應(yīng)的外鍵字段,連接到會(huì)員等級(jí)表中:
# hello_app/models.py
# ...
class VIPLevel(models.Model):
name = models.CharField('會(huì)員等級(jí)名稱', max_length=20)
price = models.IntegerField('會(huì)員價(jià)格,元/月', default=10)
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 表刪除,同時(shí)刪除遷移記錄文件,操作如下:
(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
此外,還需要將數(shù)據(jù)庫中的原 member 表、django_migrations 表刪除,即還原到最初狀態(tài)。接下來,我們使用數(shù)據(jù)庫遷移命令:
(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 后面不帶應(yīng)用會(huì)生成許多 Django 內(nèi)置應(yīng)用的表,比如權(quán)限表、用戶表、Session表等。
上面我們可以看到,我們生成的會(huì)員表中相比之前對(duì)了一個(gè) vip_level_id 字段,這個(gè)字段關(guān)聯(lián)的是 vip_level 表的 id 字段。現(xiàn)在我們首先在 vip_level 中新建三條記錄,分別表示 VIP、VVIP 以及 VVVIP:
(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 VIPLevel
>>> vip = VIPLevel(name='vip', remark='普通vip', price=10)
>>> vip.save()
>>> vvip = VIPLevel(name='vvip', remark='高級(jí)vip', price=20)
>>> vvip.save()
>>> vvvip = VIPLevel(name='vvvip', remark='超級(jí)vip', price=30)
>>> vvvip.save()
>>> VIPLevel.objects.all()
<QuerySet [<VIPLevel: <vip>>, <VIPLevel: <vvip>>, <VIPLevel: <vvvip>>]>
接下來,我們操作 member 表,生成幾條記錄并關(guān)聯(lián)到 vip_level 表:
>>> from hello_app.models import Member
>>> m1 = Member(name='會(huì)員1', age=29, sex=0, occupation='python', phone_num='18054299999', city='guangzhou')
>>> m1.vip_level = vip
>>> m1.save()
>>> m2 = Member(name='會(huì)員2', age=30, sex=1, occupation='java', phone_num='18054299991', city='shanghai')
>>> m2.vip_level = vvip
>>> m2.save()
>>> m3 = Member(name='會(huì)員3', age=35, sex=0, occupation='c/c++', phone_num='18054299992', city='beijing')
>>> m3.vip_level = vvvip
>>> m3.save()
查看會(huì)員表中生成的數(shù)據(jù)如下:
可以看到,這里我們并沒有直接寫 vip_level_id 值,而是將 Member 的 vip_level 屬性值直接賦值,然后保存。最后 Django 的 ORM 模型在這里會(huì)自動(dòng)幫我們處理這個(gè)關(guān)聯(lián)字段的值,找到關(guān)聯(lián)記錄的 id 值,并賦值給該字段。接下來,我們看下外鍵關(guān)聯(lián)的查詢操作:
>>> Member.objects.get(age=29).vip_level
<VIPLevel: <vip>>
>>> type(Member.objects.get(age=29).vip_level)
<class 'hello_app.models.VIPLevel'>
>>> vip = VIPLevel.objects.get(name='vip')
>>> vip.member_set.all()
<QuerySet [<Member: <會(huì)員1, 18054299999>>]>
>>> type(vip.member_set)
<class 'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager.<locals>.RelatedManager'>
上面的操作示例中我們給出了關(guān)聯(lián)表 vip_level (往往成為主表) 和 member (往往成為子表) 之間的正向和反向查詢。在 Django 默認(rèn)每個(gè)主表都有一個(gè)外鍵屬性,這個(gè)屬性值為:從表_set,通過這個(gè)屬性值我們可以查到對(duì)應(yīng)的從表記錄,比如上面的 vip.member_set.all()
語句就是查詢所有 vip 會(huì)員。當(dāng)然這個(gè)外鍵屬性是可以修改的,我們需要在 member 表中的外鍵字段那里加上一個(gè)屬性值:
class Member(models.Model):
...
vip_level = models.ForeignKey('VIPLevel', related_name="new_name", on_delete=models.CASCADE, verbose_name='vip level')
...
這樣我們想再次通過主表查詢子表時(shí),就要變成如下方式了:
>>> 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 1, in <module>
AttributeError: 'VIPLevel' object has no attribute 'member_set'
>>> vip.new_name.all()
<QuerySet [<Member: <會(huì)員1, 18054299999>>]>
>>>
前面在定義外鍵時(shí),我們添加了一個(gè) on_delete
屬性,這個(gè)屬性控制著在刪除子表外鍵連接的記錄時(shí),對(duì)應(yīng)字表的記錄會(huì)如何處理,它有如下屬性值:
CASCADE:級(jí)聯(lián)操作。如果外鍵對(duì)應(yīng)的那條記錄被刪除了,那么子表中所有外鍵為那個(gè)記錄的數(shù)據(jù)都會(huì)被刪除。對(duì)于例中,就是如果我們將會(huì)員等級(jí) vip 的記錄刪除,那么所有 vip 會(huì)員會(huì)被一并刪除;
# 前面使用的正是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()
(2, {'hello_app.Member': 1, 'hello_app.VIPLevel': 1})
>>> Member.objects.all()
<QuerySet [<Member: <會(huì)員2, 18054299991>>, <Member: <會(huì)員3, 18054299992>>]>
PROTECT:受保護(hù)。即只要子表中有記錄引用了外鍵的那條記錄,那么就不能刪除外鍵的那條記錄。如果我們強(qiáng)行刪除,Django 就會(huì)報(bào) 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 1, in <module>
File "/root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/db/models/base.py", line 918, 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 224, 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 22, in PROTECT
raise 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: <會(huì)員2, 18054299991>>]>)
SET_NULL:設(shè)置為空。如果外鍵的那條數(shù)據(jù)被刪除了,那么子表中所有外鍵為該條記錄的對(duì)應(yīng)字段值會(huì)被設(shè)置為 NULL,前提是要指定這個(gè)字段可以為空,否則也會(huì)報(bào)錯(cuò);
# hello_app/models.py
vip_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='會(huì)員2').vip_level_id is None
True
注意:注意加上null=True是不夠的,因?yàn)閿?shù)據(jù)庫在使用遷移命令時(shí)候已經(jīng)默認(rèn)是不可為空,這里測(cè)試時(shí)還需要手動(dòng)調(diào)整下表 vip_level 字段屬性,允許為 null。
SET_DEFAULT:設(shè)置默認(rèn)值。和上面類似,前提是字表的這個(gè)字段有默認(rèn)值;
SET():如果外鍵的那條數(shù)據(jù)被刪除了。那么將會(huì)獲取SET函數(shù)中的值來作為這個(gè)外鍵的值。SET函數(shù)可以接收一個(gè)可以調(diào)用的對(duì)象(比如函數(shù)或者方法),如果是可以調(diào)用的對(duì)象,那么會(huì)將這個(gè)對(duì)象調(diào)用后的結(jié)果作為值返回回去;
# hello_app/models.py
# 新增一個(gè)設(shè)置默認(rèn)值函數(shù)
def default_value():
# 刪除記錄時(shí)會(huì)調(diào)用,在這里可以做一些動(dòng)作
# ...
# 返回臨時(shí)指向一條記錄的id,返回不存在的id時(shí)會(huì)報(bào)錯(cuò);返回?cái)?shù)字也會(huì)報(bào)錯(cuò),要注意
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='會(huì)員3').vip_level_id
3
# 新建一個(gè)臨時(shí)過渡vip記錄
>>> tmp_vip=VIPLevel(name='等待升級(jí)vip', price=30, remark='臨時(shí)升級(jí)過渡')
>>> tmp_vip.save()
>>> tmp_vip.id
4
# 刪除vvvip記錄
>>> VIPLevel.objects.all().get(name='vvvip').delete()
(1, {'hello_app.VIPLevel': 1}
# 可以看到,會(huì)員表中曾經(jīng)指向?yàn)関vvip的記錄被重新指向了臨時(shí)過渡vip
>>> Member.objects.get(name='會(huì)員3').vip_level_id
4
DO_NOTHING:什么也不做,你刪除你的,我保留我的,一切全看數(shù)據(jù)庫級(jí)別的約束。在 MySQL 中,這種情況下無法執(zhí)行刪除動(dòng)作。
2. 小結(jié)
本小節(jié)中我們描述了外鍵的相關(guān)概念,然后在 Django 的 shell 模式下使用會(huì)員表和會(huì)員等級(jí)表來進(jìn)行外鍵的操作,重點(diǎn)演示了關(guān)聯(lián)表之間的創(chuàng)建、相互查詢以及刪除等相關(guān)的操作。