19 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(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 中外來鍵的使用,我們在前面的會員 Member 的基礎上新增一個關聯表:會員等級表(vip_level)。這個會員等級有 VIP、VVIP 以及超級 VIP 的 VVVIP 三個等級,我們在 models.py 中新增如下模型類,並在會員表中新增對應的外來鍵欄位,連線到會員等級表中:
# hello_app/models.py
# ...
class VIPLevel(models.Model):
name = models.CharField('會員等級名稱', max_length=20)
price = models.IntegerField('會員價格,元/月', 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 表刪除,同時刪除遷移記錄檔案,操作如下:
(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 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='高階vip', price=20)
>>> vvip.save()
>>> vvvip = VIPLevel(name='vvvip', remark='超級vip', price=30)
>>> 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=29, sex=0, occupation='python', phone_num='18054299999', city='guangzhou')
>>> m1.vip_level = vip
>>> m1.save()
>>> m2 = Member(name='會員2', age=30, sex=1, occupation='java', phone_num='18054299991', city='shanghai')
>>> m2.vip_level = vvip
>>> m2.save()
>>> m3 = Member(name='會員3', age=35, sex=0, 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=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: <會員1, 18054299999>>]>
>>> 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 1, in <module>
AttributeError: 'VIPLevel' object has no attribute 'member_set'
>>> vip.new_name.all()
<QuerySet [<Member: <會員1, 18054299999>>]>
>>>
前面在定義外來鍵時,我們添加了一個 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()
(2, {'hello_app.Member': 1, 'hello_app.VIPLevel': 1})
>>> Member.objects.all()
<QuerySet [<Member: <會員2, 18054299991>>, <Member: <會員3, 18054299992>>]>
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 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: <會員2, 18054299991>>]>)
SET_NULL:設定為空。如果外來鍵的那條資料被刪除了,那麼子表中所有外來鍵為該條記錄的對應欄位值會被設定為 NULL,前提是要指定這個欄位可以為空,否則也會報錯;
# 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='會員2').vip_level_id is None
True
注意:注意加上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
3
# 新建一個臨時過渡vip記錄
>>> tmp_vip=VIPLevel(name='等待升級vip', price=30, remark='臨時升級過渡')
>>> tmp_vip.save()
>>> tmp_vip.id
4
# 刪除vvvip記錄
>>> VIPLevel.objects.all().get(name='vvvip').delete()
(1, {'hello_app.VIPLevel': 1}
# 可以看到,會員表中曾經指向為vvvip的記錄被重新指向了臨時過渡vip
>>> Member.objects.get(name='會員3').vip_level_id
4
DO_NOTHING:什麼也不做,你刪除你的,我保留我的,一切全看資料庫級別的約束。在 MySQL 中,這種情況下無法執行刪除動作。
2. 小結
本小節中我們描述了外來鍵的相關概念,然後在 Django 的 shell 模式下使用會員表和會員等級表來進行外來鍵的操作,重點演示了關聯表之間的建立、相互查詢以及刪除等相關的操作。