跳轉到

客製化 User Model

開始之前

注意事項

因為前面章節使用的專案已經 Migrate 過了,會無法客製化 User Model 如果想要執行這個章節的範例,請建立新的專案。

從下個章節開始我們就會回到舊的專案範例中了。

任務目標

在這個章節中,我們會了解:

  • 為什麼需要客製化 User Model
  • Django 提供的兩種客製化方法
  • 如何實作客製化 User Model
  • 為什麼專案開始後就無法再客製化

為什麼需要客製化 User Model?

Django 預設的 User Model 提供了基本的使用者認證功能,包含使用者名稱、密碼、電子郵件等欄位。但在實際專案中,我們常常會遇到以下需求:

  • 預設的 User Model 沒有電話號碼欄位
  • 預設的 User Model 沒有生日、地址等個人資料欄位
  • 預設使用 username 登入,但我想用 email 登入
  • 預設的欄位太多,我只需要其中幾個

我們希望:

  • 可以新增需要的欄位
  • 可以改變認證方式(例如使用 email 登入)
  • 可以移除不需要的欄位
  • 保留未來擴充的彈性

這就是「客製化 User Model」要解決的問題。

Django 官方的強烈建議

即使你目前不需要自訂任何欄位,Django 官方文件也強烈建議在專案開始時就設定自訂的 User Model。

為什麼?因為一旦專案開始運作並產生了資料,要改變 User Model 將會非常困難

Django 提供的兩種客製化方法

Django 提供了兩種客製化 User Model 的方法,選擇哪一種取決於你的需求。

方法一:繼承 AbstractUser

如果你只是想在現有的 User Model 上新增額外欄位,但保留 Django 預設的使用者名稱/密碼認證方式,這是最簡單的方法。

accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models


class CustomUser(AbstractUser):
    phone_number = models.CharField(max_length=20, blank=True)  # (1)!
    birth_date = models.DateField(null=True, blank=True)  # (2)!
    bio = models.TextField(blank=True)  # (3)!
  1. 新增電話號碼欄位
  2. 新增生日欄位
  3. 新增個人簡介欄位

這種方式會保留所有 Django 預設的欄位,包括:

欄位 說明
username 使用者名稱(唯一)
password 密碼(加密儲存)
email 電子郵件
first_name 名字
last_name 姓氏
is_active 是否啟用
is_staff 是否為管理員
is_superuser 是否為超級使用者
date_joined 註冊日期
last_login 最後登入時間

你只需要新增自己需要的欄位即可。

何時使用這個方法?

如果你的需求是:

  • ✅ 保留 Django 預設的認證方式(使用 username 登入)
  • ✅ 只是想新增額外的欄位
  • ✅ 不想處理複雜的 UserManager

那麼這個方法最適合你!

方法二:繼承 AbstractBaseUser

如果你需要完全控制 User Model 的欄位和認證方式,例如使用電子郵件而非使用者名稱登入,則需要繼承 AbstractBaseUserPermissionsMixin

accounts/models.py
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.db import models


class CustomUserManager(BaseUserManager):  # (1)!
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError('使用者必須提供電子郵件地址')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        return self.create_user(email, password, **extra_fields)


class CustomUser(AbstractBaseUser, PermissionsMixin):  # (2)!
    email = models.EmailField(unique=True)  # (3)!
    name = models.CharField(max_length=100)
    is_active = models.BooleanField(default=True)  # (4)!
    is_staff = models.BooleanField(default=False)  # (5)!
    date_joined = models.DateTimeField(auto_now_add=True)

    objects = CustomUserManager()  # (6)!

    USERNAME_FIELD = 'email'  # (7)!
    REQUIRED_FIELDS = ['name']  # (8)!

    def __str__(self):
        return self.email
  1. 自訂 UserManager,處理使用者的建立
  2. 繼承 AbstractBaseUserPermissionsMixin
  3. 使用 email 作為主要識別欄位(必須是 unique)
  4. is_active 是必要的欄位
  5. is_staff 是必要的欄位(用於 Admin 存取)
  6. 指定使用自訂的 Manager
  7. 指定使用哪個欄位作為登入的識別欄位
  8. 建立超級使用者時必須輸入的欄位(不包含 USERNAME_FIELDpassword

這種方式需要自己定義所有必要的欄位,提供最大的彈性,但也最複雜。

何時使用這個方法?

如果你的需求是:

  • ✅ 完全自訂欄位(不需要 first_namelast_name 等預設欄位)
  • ✅ 需要完全控制認證流程

那麼這個方法適合你!但要注意,這個方法需要更多的設定。

AbstractBaseUser vs AbstractUser

特性 AbstractUser AbstractBaseUser
保留預設欄位
需要自訂 Manager
彈性
複雜度
適合場景 只需新增欄位 需要完全客製化

如何實作客製化 User Model

無論選擇哪種方法,都需要完成以下步驟:

步驟 1:建立 accounts app

通常我們會建立一個專門處理使用者相關功能的 app:

uv run manage.py startapp accounts

步驟 2:定義自訂的 User Model

accounts/models.py 中定義你的 User Model(參考前面的兩種方法)。

步驟 3:在 settings.py 中指定 AUTH_USER_MODEL

這是最關鍵的一步,告訴 Django 使用你的自訂 User Model:

core/settings.py
INSTALLED_APPS = [
    # ...
    "django.contrib.auth",
    # ...
    "accounts",  # (1)!
]

# ...

AUTH_USER_MODEL = "accounts.CustomUser"  # (2)!
  1. 將 accounts app 加入 INSTALLED_APPS
  2. 指定自訂的 User Model,格式為 'app名稱.Model名稱'

必須在第一次 migrate 之前設定

AUTH_USER_MODEL 的設定必須在執行第一次 migrate 之前完成

一旦執行了 migrate,Django 就會在資料庫中建立相關的表格和外鍵關係,之後就很難更改了。

步驟 4:建立並執行 migrations

uv run manage.py makemigrations
uv run manage.py migrate

步驟 5:更新 Admin 設定

如果你使用 Django Admin,需要註冊自訂的 User Model:

accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from accounts.models import CustomUser


@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
    # 如果使用 AbstractUser,通常不需要額外設定
    pass

如果你使用 AbstractBaseUser,需要更多的設定:

accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from accounts.models import CustomUser


@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
    list_display = ["email", "name", "is_staff", "is_active"]  # (1)!
    list_filter = ["is_staff", "is_active"]
    search_fields = ["email", "name"]
    ordering = ["email"]

    fieldsets = (  # (2)!
        (None, {"fields": ("email", "password")}),
        ("個人資料", {"fields": ("name",)}),
        ("權限", {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}),
        ("重要日期", {"fields": ("last_login", "date_joined")}),
    )

    add_fieldsets = (  # (3)!
        (
            None,
            {
                "classes": ("wide",),
                "fields": ("email", "name", "password1", "password2"),
            },
        ),
    )
  1. 在列表頁面顯示的欄位
  2. 編輯使用者時的欄位分組
  3. 新增使用者時的欄位(注意:需要包含 password1 和 password2)

為什麼專案開始後就無法再客製化?

這是最重要的問題!讓我們深入了解為什麼 Django 官方如此強調要在專案開始前就設定好 User Model。

資料庫關聯的複雜性

Django 的 User Model 是整個認證系統的核心,許多內建的功能和第三方套件都會與 User Model 建立關聯。

erDiagram
    USER ||--o{ ARTICLE : "作者"
    USER ||--o{ COMMENT : "評論者"
    USER ||--o{ PERMISSION : "擁有"
    USER ||--o{ GROUP : "屬於"
    USER ||--o{ SESSION : "登入"
    USER ||--o{ LOG_ENTRY : "操作記錄"

    USER {
        int id PK
        string username
        string email
        string password
    }

    ARTICLE {
        int id PK
        int author_id FK
        string title
        text content
    }

    COMMENT {
        int id PK
        int user_id FK
        text content
    }

如上圖所示,User Model 與許多其他 Model 都有外鍵關係。如果要更換 User Model,就需要:

  1. 刪除所有與舊 User Model 相關的表格
  2. 重新建立所有的關聯
  3. 遷移現有的使用者資料
  4. 處理可能的資料遺失問題

實際會遇到的問題

當你的專案已經有使用者資料時,要更換 User Model 會面臨以下問題:

1. 資料遷移困難

舊 User Model (django.contrib.auth.User)
├── username: "john"
├── email: "john@example.com"
├── first_name: "John"
└── last_name: "Doe"

新 User Model (accounts.CustomUser)
├── email: "john@example.com"
├── name: "John Doe"  ← 如何合併 first_name 和 last_name?
├── phone_number: ""  ← 舊資料沒有這個欄位,怎麼辦?
└── birth_date: None  ← 舊資料沒有這個欄位,怎麼辦?

2. 外鍵關係需要重新建立

所有指向 User 的 ForeignKey 都需要更新:

# 舊的 Article Model
class Article(models.Model):
    author = models.ForeignKey('auth.User', ...)  # ← 指向舊的 User

# 需要改成
class Article(models.Model):
    author = models.ForeignKey('accounts.CustomUser', ...)  # ← 指向新的 User

但問題是,資料庫中已經有資料了!外鍵關係已經建立,要如何改變?

3. Migration 衝突

Django 的 migration 系統會記錄所有的資料庫變更。如果要改變 User Model,會產生非常複雜的 migration 衝突:

❌ Migration 衝突:
    - 0001_initial 建立了 auth.User
    - 0023_article 建立了 Article,並參照 auth.User
    - 現在想要改成 accounts.CustomUser?
    - 但是 0023_article 已經執行過了,怎麼辦?

4. 第三方套件相容性

許多第三方套件(如 django-allauth、django-rest-framework)都會與 User Model 建立關聯。如果改變 User Model,這些套件可能會出現問題。

視覺化說明

flowchart TD
    Start[開始專案] --> Decision{設定自訂 User Model?}
    Decision -->|是| CustomUser[建立 CustomUser]
    Decision -->|否| DefaultUser[使用預設 User]

    CustomUser --> Migrate1[執行 migrate]
    DefaultUser --> Migrate2[執行 migrate]

    Migrate1 --> Future1[未來可以輕鬆擴充]
    Migrate2 --> Future2[未來難以更改]

    Future2 --> Problem1[需要資料遷移]
    Future2 --> Problem2[外鍵關係複雜]
    Future2 --> Problem3[Migration 衝突]
    Future2 --> Problem4[第三方套件問題]

    Problem1 --> Pain[😱 痛苦的改變過程]
    Problem2 --> Pain
    Problem3 --> Pain
    Problem4 --> Pain

最佳實踐

因此,Django 官方建議:

最佳實踐

在執行第一次 migrate 之前,就決定好是否要客製化 User Model。

即使目前不需要額外欄位,也建議建立一個簡單的自訂 User Model:

accounts/models.py
from django.contrib.auth.models import AbstractUser


class CustomUser(AbstractUser):
    pass  # (1)!
  1. 目前不新增任何欄位,但未來可以隨時新增!

這樣做可以保留未來擴充的彈性,而不會增加任何複雜度。

專案已經開始時的替代方案

如果你的專案已經開始運作,無法更換 User Model,可以考慮以下替代方案。

使用 OneToOneField 擴充

建立一個與 User 一對一關聯的 Profile Model:

accounts/models.py
from django.contrib.auth.models import User
from django.db import models


class UserProfile(models.Model):
    user = models.OneToOneField(  # (1)!
        User,
        on_delete=models.CASCADE,
        related_name="profile",
    )
    phone_number = models.CharField(max_length=20, blank=True)  # (2)!
    birth_date = models.DateField(null=True, blank=True)
    bio = models.TextField(blank=True)

    def __str__(self):
        return f"{self.user.username} 的個人資料"
  1. 與 User 建立一對一關係
  2. 在這裡新增額外的欄位

使用方式:

# 建立使用者的 Profile
user = User.objects.get(username="john")
profile = UserProfile.objects.create(
    user=user,
    phone_number="0912345678",
    birth_date="1990-01-01"
)

# 存取使用者的額外資料
phone = user.profile.phone_number  # (1)!
birth_date = user.profile.birth_date
  1. 透過 related_name='profile' 存取

OneToOneField 的優缺點

優點

  • ✅ 不需要修改現有的 User Model
  • ✅ 不需要處理複雜的 migration
  • ✅ 可以保留現有的資料

缺點

  • ❌ 需要透過關聯來存取額外的欄位(user.profile.phone_number
  • ❌ 查詢時需要使用 select_related 避免 N+1 問題
  • ❌ 不能改變認證方式(仍然使用 username 登入)

使用 Signals 自動建立 Profile

為了確保每個使用者都有 Profile,可以使用 signals:

accounts/signals.py
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

from accounts.models import UserProfile


@receiver(post_save, sender=User)  # (1)!
def create_user_profile(sender, instance, created, **kwargs):
    if created:  # (2)!
        UserProfile.objects.create(user=instance)


@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()  # (3)!
  1. 監聽 User 的 post_save 信號
  2. 只在建立新使用者時執行
  3. 儲存 Profile(確保 User 和 Profile 同步)

不要忘記在 accounts/apps.py 中載入 signals:

accounts/apps.py
from django.apps import AppConfig


class AccountsConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "accounts"

    def ready(self):
        import accounts.signals  # (1)!
  1. 載入 signals

任務結束

完成!

恭喜你完成了這個章節!現在你已經了解:

  • Django 提供了兩種客製化 User Model 的方法:AbstractUserAbstractBaseUser
  • AbstractUser 適合只需新增欄位的情況,保留所有預設功能
  • AbstractBaseUser 適合需要完全客製化認證方式的情況
  • 客製化 User Model 必須在專案開始前(第一次 migrate 之前)完成
  • 專案開始後更換 User Model 會導致資料庫遷移和外鍵關係的複雜問題
  • 如果專案已經開始,可以使用 OneToOneField 建立 Profile Model 作為替代方案

重點回顧

客製化 User Model 是 Django 專案中最重要的決策之一,必須在專案初期就做出決定。

即使目前不需要額外欄位,也建議建立一個繼承 AbstractUser 的空 Model,以保留未來擴充的彈性。

記住:一旦執行了第一次 migrate,要改變 User Model 將會非常困難!