iT邦幫忙

2022 iThome 鐵人賽

DAY 25
0
Modern Web

傳承D的意志~ 邁向Django的偉大航道系列 第 25

[Day 25] 激戰篇: 如何將選項(Choices)欄位變更為外來鍵(FK)

  • 分享至 

  • xImage
  •  

嗨大家好,我是Sean! 不知道大家連假,過得怎樣啊?
昨天我們講完Migration error的部分,我們來講解一下另外一種會造成migration error的情況,並解決它。

資料庫欄位


相信大家在初期設計資料庫時,或是要新增資料庫欄位時,一定都會遇到一個問題。
那就是,如果欄位設計錯時,該怎麼辦? 再者,如果想更改的欄位是foreign key可能會有fk constraint的問題。

那麼,我們今天來講解若原本的欄位是選項,要如何把它變更為foreign key呢?

Changing choices to a ForeignKey


首先,我們會看到我們的欄位大概會長得像這樣:

class People(models.Model):
    ...
    AREA_CHOICES = [
        ('AREA1', 'East_Blue',),
        ('AREA2', 'West_Blue',),
        ('AREA3', 'North_Blue',),
        ('AREA4', 'South_Blue',),
    ]
    
    area = models.CharField(max_length=100, choices=AREA_CHOICES)
    ...

我們想要達到的結果像是以下這樣:

class Area(models.Model):
    name = models.CharField(max_length=100)

class People(models.Model):
    ...
    area = models.ForeignKey(Area, on_delete=models.PROTECT, db_column='name')
    ...

好的讓我們開始執行這段轉換的過程。

加入外來鍵(ForeignKey)以及model


首先第一步,我們在我們最原始的model樣子,也就是choice寫在class People裡的圖一,加入我們外來鍵。
加入外來鍵的前提是,我們同時新增他的model。
所以我們第一步增加的東西,如下所示:


# 我們新增的model
class Area(models.Model)
    name = models.CharField(max_length=100)

class People(models.Model):
    ...
    AREA_CHOICES = [
        ('AREA1', 'East_Blue',),
        ('AREA2', 'West_Blue',),
        ('AREA3', 'North_Blue',),
        ('AREA4', 'South_Blue',),
    ]
    
    area = models.CharField(max_length=100, choices=AREA_CHOICES)
    ...
    # 我們新增的foreign key欄位
    area_fk = models.ForeignKey(Area, on_delete=models.PROTECT, db_column='area_fk', null=True)
    ...

新增的東西有Area的model,以及area_fk的外來鍵欄位。
特別值得一提的是,我們在加入外來鍵的area_fk欄位裡,給了兩個比較特別的參數,第一個是db_column,而第二個則是null=True。
他們的意義是,db_column可以使該欄位在db中的欄位強制更改為參數中的名字。
聽起來很像廢話,對吧? 明明在開頭的變數名稱就可以直接變為欄位名稱啊!

為什麼還需要db_column這個參數呢?

原因是因為當欄位被設定為foreign key時,他在db中的欄位會自動被加入 _id 的尾綴。

加入這樣的尾綴後,會導致後續我們在使用欄位的時候:

發生欄位名稱與資料庫中的欄位名稱不相符的錯誤!

接著,我們的第二個參數為null=True。其目的為讓我們的欄位可以為無,也就是null狀態。

允許在該欄位的model(Area),沒有任何資料的情況下,外來鍵可以為空
不會因為找不到資料關聯的情況,而發生了error!

接著我們用以上的model來做一次資料遷移,也就是使用:

python manage.py makemigrations

python manage.py migrate

接下來,我們再使用較為特殊的migration,來達成我們想變換foreign key的目的。

makemigrations --empty core


我們直接來使用以下指令,來看看他的效果:

python manage.py makemigrations --empty core

他新增了一個migration檔案,我們來看看他大概長得像怎樣。

from django.db import migrations

class Migration(migrations.Migration):

  dependencies = [
      ('core', '0002_auto_20221010_2211'),
  ]

  operations = [
  ]

他會像是個完全空白的migration檔案,其中的operation並無任何的執行事項。
而就如同我們在之前的文章所提到的,我們可以經由主動改寫migration的檔案,來達到我們的目的。

如同我們下面的code所示:

from django.db import migrations

def fill_area_fk(apps, schema_editor):
  People = apps.get_model('core', 'People')
  Area = apps.get_model('core', 'Area')
  for people in People.objects.all():
    people.category_fk, created = area.objects.get_or_create(name=people.area)
    people.save()

class Migration(migrations.Migration):

  dependencies = [
      ('core', '0002_auto_20210715_0836'),
  ]

  operations = [
      migrations.RunPython(fill_area_fk),
  ]

我們可以在operation的地方,加入我們的功能來完成。

首先,我們先得到了People與Area兩個model。
接著,我們經由原本People中的area欄位裡的選項,來生成model Area中的資料!
再儲存每筆People的資料,就可以達到不需要額外匯入資料,我們每筆資料的fk,就與我們原本的選項相同了!

我們再經由:

python manage.py migrate

遷移我們剛剛寫好的migration檔案就可以,完成上述的目標了!

Remove choice and rename fk


最後,我們只需要移除choice的欄位以及變更我們的fk欄位的名字,就大功告成了!

class People(models.Model):

    ...
    # 我們移除choice,以及變更foreign key欄位名稱後的樣子
    
    area = models.ForeignKey(Area, on_delete=models.PROTECT, db_column='area', null=True)
    ...

經過makemigrations後,請記得再確認一下

migration檔案的內容是否為移除欄位以及變更欄位名字!

他很有可能會變成以下這樣:

class Migration(migrations.Migration):

  dependencies = [
      ('core', '0003_auto_20221010_2236'),
  ]

  operations = [
      migrations.RemoveField(
          model_name='people',
          name='area_fk',
      ),
      migrations.AlterField(
          model_name='people',
          name='area',
          field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='core.Area'),
      ),
  ]

AlterField為變更欄位屬性,這麼一來就會發生最一開始,直接變更欄位屬性的錯誤了!

正確的應該為下面這樣:

class Migration(migrations.Migration):

  dependencies = [
      ('core', '0003_auto_20221010_2236'),
  ]

  operations = [
      migrations.RemoveField(
          model_name='people',
          name='area',
      ),
      migrations.RenameField(
          model_name='sample',
          old_name='category_fk',
          new_name='category',
      ),
  ]

Remove 選項欄位,以及Rename 外來鍵欄位!

想要達成這樣Migration檔案,你可以:

  1. 直接在migration檔案裡做修改
  2. 分批做資料遷移!
    先把選項欄位刪除後做一次遷移,再修改外來鍵欄位名稱後,再做一次資料遷移!

好的!這麼一來,我們由選項欄位更改為外來鍵的任務就大功告成了!

那麼今天的文章就到此結束! 感謝大家的收看!
我是Sean,你各位海上的人,我們明天見!
https://ithelp.ithome.com.tw/upload/images/20221010/20151096hmIpSSUbfk.jpg


上一篇
[Day 24] 激戰篇: 正面對決啊 Migration error(下)
下一篇
[DAY 26] 激戰篇: 如何序列化複數個model
系列文
傳承D的意志~ 邁向Django的偉大航道30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言