1. 程式人生 > Django入門教學 >19 Django 中 ORM 外來鍵使用

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表等。

圖片描述

生成的 member 表

上面我們可以看到,我們生成的會員表中相比之前對了一個 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。

圖片描述

允許 vip_level_id 為 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 模式下使用會員表和會員等級表來進行外來鍵的操作,重點演示了關聯表之間的建立、相互查詢以及刪除等相關的操作。