iT邦幫忙

2023 iThome 鐵人賽

DAY 21
0

前言

前兩天我們學會了如何做篩選,今天讓我們來看看如何在已存在的 Model 中加上欄位吧!雖然前面已經有在 Task Model 中加上 is_finish 欄位了,但是那是在有預設值的情況下,在沒有時會有一些些不同讓我們一起來看看吧!

今天我們會模擬幾個情況

  1. 加入欄位可以為 null
  2. 加入欄位可以為 blank
  3. 加入必填欄位並指定單次的預設值

P.S. 加入有預設值的欄位之前已經加過 is_finish 了這邊就不再模擬,有需要可以回看 Day06

讓我們開始吧!

加入可空欄位

讓我們編輯 server/app/todo/models.py 這個檔案

# ...... 以上省略 ......
class Tag(models.Model):
    name = models.CharField(max_length=255, unique=True)
+   description = models.TextField(blank=True)

    def __str__(self):
        return self.nam

# ...... 以下省略 ......

上面我們在 Tag Model 中加入了一個 description 文字欄位,並設定他為可空(blank)

接著讓我們來產生資料庫遷移檔(記得要啟動虛擬環境)

python manage.py makemigrations

應該會看到產生了 0005 版本的遷移檔案在 server/app/todo/migrations 資料夾下

接著讓我們將遷移檔案套用到資料庫中

python manage.py migrate

現在我們就可以看到資料庫的 tag 表出現了 description 欄位了

加入可 null 欄位

接著讓我們來加入一個可 null 欄位,大家可能會好奇 null 跟 blank 到底有何不同,等等我會跟大家說明,先讓我們看下去。

讓我們繼續編輯 server/app/todo/models.py 檔案

# ...... 以上省略 ......

class Task(models.Model):
    title = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    is_finish = models.BooleanField(default=False)
    tags = models.ManyToManyField(Tag)
+   end_at = models.DateTimeField(null=True)

    def __str__(self):
        return self.title

我們加入了一個欄位(型態為日期與時間)名為 end_at 代表的是這個任務的結束時間,並設定為可 null

接著讓我們產生遷移檔案

python manage.py makemigrations

現在應該會看到產生了 0006 版本的遷移檔案在 server/app/todo/migrations 資料夾下

接著讓我們套用遷移檔

python manage.py migrate

現在應該會看到 task 表出現了 end_at 欄位

欄位 null 與 blank 的差異

在修改完前面的設定後我們也都順利將欄位加入表中了,現在讓我們看看 null 與 blank 有何不同

首先看 blank 這個代表的是允許欄位的值可以是空的字串,我們試著用 POST 方法請求 http://127.0.0.1:8000/api/todo/tags

HTTP Body 如下

{
    "name": "",
}

P.S. 可不傳入 description 是因為設定為 blank 後序列化會將它標記為選填欄位就可以不傳入

可以發現會收到回應

{
    "name": [
        "此欄位不可為空白。"
    ]
}

代表 API 拒絕一個名稱為空字串的標籤被建立,但是大家如果將 HTTP Body 變成下方的樣子

{
    "name": "Taaaaaaag"
}

就會發現可以正常建立了,讓我們在傳入資料時允許傳入空字串就是 blank 的目的

接著我們看看 null 這個代表的是允許欄位中可以存放 null 這個值代表是空的意思,我們先透過呼叫 API 看看效果

我們使用 POST 呼叫 http://127.0.0.1:8000/api/todo/tasks

HTTP Body 如下

{
    "title": "Hello",
    "tag_ids": [
        1
    ]
}

會收到回傳如下

{
    "id": 28,
    "tags": [
        {
            "id": 1,
            "name": "T01",
            "description": ""
        }
    ],
    "title": "Hello",
    "description": "",
    "is_finish": false,
    "end_at": null
}

可以看到 end_at 因為被我們設定成可以 null 所以就算我們的傳入沒有輸入任何值,他也能順利的呼叫,且回傳的 end_at 值為 null

這時候大家會想說,我們全都用 null 或全都用空字串不就好了嗎?其實是不行的讓我們想一下如果今天我們的 end_at 設定為可以是空字串,且我們真的將空字串塞進去了,是不是就代表說資料庫中的這個欄位中有一個欄位是有值(空字串)但不符合日期格式的,同理如果今天有數字型態的欄位也會遇到一樣的問題。

而且其實 blank 在 Django 中除了判斷這個欄位是否可以存放空字串以外,還有一個功能,讓我們打開 http://127.0.0.1:8000/admin 並到新增 Task 的畫面。輸入其他欄位的值但是不要數入 end_at 欄位再按下送出。

應該會發現它出現了 end_at 欄位為必填的錯誤訊息,但我們都將他設定為 null 了應該代表他是選填啊。原因是因為對 Django 來說你設定 null=True 代表這個欄位可以存入除了對的型態(這邊是日期)以外還可以存入 null,但不代表這個是選填的意思(雖然對序列化來說是,但這邊 Admin 系統用的是表單而不是序列化)所以我們需要另外設定一下讓他變成選填的,讓我們編輯 server/app/todo/models.py 檔案

# ...... 以上省略 ......

class Task(models.Model):
    title = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    is_finish = models.BooleanField(default=False)
    tags = models.ManyToManyField(Tag)
-   end_at = models.DateTimeField(null=True)
+   end_at = models.DateTimeField(null=True, blank=True)

    def __str__(self):
        return self.title

接著讓我們產生遷移檔並套用

python manage.py makemigrations
python manage.py migrate

現在再讓我們回到 Admin 系統應該可以發現 end_at 欄位變為選填的了。

這邊幫大家總結一下 blank 代表的是可以存入空字串,同時也代表表單的欄位是選填的,而 null 代表的是這個欄位可以存入 null 這個值,但不代表這個欄位在表單是選填的,如果需要可以搭配 blank 使用

這邊額外跟大家分享一下我的習慣,如果是文字型態的欄位(例如 CharField, TextField 等等)要表示選填,我會習慣null=True 純用 blank=True 這樣當我需要找這個欄位沒有值的時候就只需要找空字串即可,不用額外再找 null 這個值(當然前提是需求 null 與空字串都代表沒填入),如果是非文字型態的欄位我就會給 null=True 並看表單是否也需要是選填再來給 blank=True,不過這單純是我的習慣,大家可以參考參考就好了。

加入必填欄位

從前面兩個範例我們可以發現,因為我們新增的欄位都是可以為空或是可以為 null 的,所以 Django 在幫我們套用 migrations 時可以順利建立欄位,他只需要將已經存在的資料的新欄位設為空或是 null 即可,前面我 Day06 在加入 is_finish 時也是一樣他會將前面的舊資料設定為預設值。但事情不會每次都這麼順利有時候我們需要加入一個必填的欄位那就需要做一些調整了,讓我們看下去。

我們先編輯 server/app/todo/models.py 這個檔案

# ...... 以上省略 ......

class Task(models.Model):
    title = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    is_finish = models.BooleanField(default=False)
    tags = models.ManyToManyField(Tag)
    end_at = models.DateTimeField(null=True, blank=True)
+   created_at = models.DateTimeField(auto_now_add=True)
+   updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title

這邊是在表中加入時間戳記,created_at 代表建立的時間 updated_at 代表最後更新的時間,auto_now_add 會在資料建立時自動將這個欄位設為現在時間,auto_now 會在資料儲存(包含新增與更新)時將值設為當下時間。

接著讓我們來建立遷移檔案吧!

python manage.py makemigrations

接著你應該會看到一個像下方的訊息跟之前的不一樣

It is impossible to add the field 'created_at' with 'auto_now_add=True' to task without providing a default. This is because the database needs something to populate existing rows.
 1) Provide a one-off default now which will be set on all existing rows
 2) Quit and manually define a default value in models.py.
Select an option:

他的意思是他不知道舊資料的 created_at 欄位要存放什麼,他知道 updated_at 是因為 updated_at 有 auto_now 的設定,存擋時會變成當下時間,所以他現在給你兩個選擇

  1. 設定一個一次性的預設值,只針對舊資料
  2. 退出,重新調整 model 的設定

那這邊我們選選擇一,所以我們輸入 1 後按下 enter,接著應該會看到下方的輸出

Please enter the default value as valid Python.
Accept the default 'timezone.now' by pressing 'Enter' or provide another value.
The datetime and django.utils.timezone modules are available, so it is possible to provide e.g. timezone.now as a value.
Type 'exit' to exit this prompt
[default: timezone.now] >>>

這個是讓你在這邊輸入你想要的預設值,假設你需要一個特殊的值,你就可以在這邊輸入並按下 enter 到時候他就會針對舊資料進行預設值的套用,但如果你沒輸入就按下 enter 的話,他會使用 timezone.now 也就是等等遷移檔被套用的時間,這邊我們就用當下時間(因為我們也沒其他更好的選擇),所以我們就按下 enter 吧!

接著應該會看到下方的輸出

Migrations for 'todo':
  server/app/todo/migrations/0008_task_created_at_task_updated_at.py
    - Add field created_at to task
    - Add field updated_at to task

就代表你的遷移檔建立好了。我們可以偷看一下遷移檔的內容,應該會看到有下面一段

# ...... 以上省略 ......

migrations.AddField(
    model_name="task",
    name="created_at",
    field=models.DateTimeField(
        auto_now_add=True,
        default=django.utils.timezone.now,
    ),
    preserve_default=False,
)

# ...... 以下省略 ......

可以觀察到雖然欄位的設定有 default 但是因為有 preserve_default=False 這個設定所以後續的資料就不會吃到這個預設值。

現在讓我們來套用遷移檔吧!

python manage.py migrate

現在應該可以看到表多了 created_atupdated_at 這兩個欄位了,現在大家也可以依照之前的方法呼要一下新增或編輯的 Task 的 API 可以看到兩個時間戳都有變化了

大家可能會發現你在新增的時候會發現 created_atupdated_at 的值會有極小的差距,是因為這兩個欄位使用的時間是當物件被建立時兩個欄位分別被填入的時間點,如果希望超級精準兩者完全想同的話需要進行欄位的客製化,這個我們後面再來說明。

結語

今天我們學習了如何在 Model 中新增欄位,並說明了 blank 與 null 的不同。但還有一個情境我們沒有模擬到,就是如何加入一個必填但是無法設定單次預設值的欄位,這會需要客製化遷移檔來達成,我明天再繼續講。

結束前別忘了檢查一下今天的程式碼有沒有問題,並排版好喔。

ruff check --fix .
black .
pyright .

今天的內容就到這邊了,讓我們期待明天的內容吧。

P.S. 今天的檔案更新可以參考我的 Git Commit 大家可以搭配服用


上一篇
Day20 - 進階篩選(Filter)
下一篇
Day22 - 客製化遷移檔(migration)
系列文
Django REST 大冒險:探索精彩紛呈的 API 開發世界30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言