客製化 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 預設的使用者名稱/密碼認證方式,這是最簡單的方法。
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)!
- 新增電話號碼欄位
- 新增生日欄位
- 新增個人簡介欄位
這種方式會保留所有 Django 預設的欄位,包括:
| 欄位 | 說明 |
|---|---|
username |
使用者名稱(唯一) |
password |
密碼(加密儲存) |
email |
電子郵件 |
first_name |
名字 |
last_name |
姓氏 |
is_active |
是否啟用 |
is_staff |
是否為管理員 |
is_superuser |
是否為超級使用者 |
date_joined |
註冊日期 |
last_login |
最後登入時間 |
你只需要新增自己需要的欄位即可。
何時使用這個方法?
如果你的需求是:
- ✅ 保留 Django 預設的認證方式(使用 username 登入)
- ✅ 只是想新增額外的欄位
- ✅ 不想處理複雜的 UserManager
那麼這個方法最適合你!
方法二:繼承 AbstractBaseUser¶
如果你需要完全控制 User Model 的欄位和認證方式,例如使用電子郵件而非使用者名稱登入,則需要繼承 AbstractBaseUser 和 PermissionsMixin。
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
- 自訂 UserManager,處理使用者的建立
- 繼承
AbstractBaseUser和PermissionsMixin - 使用 email 作為主要識別欄位(必須是 unique)
is_active是必要的欄位is_staff是必要的欄位(用於 Admin 存取)- 指定使用自訂的 Manager
- 指定使用哪個欄位作為登入的識別欄位
- 建立超級使用者時必須輸入的欄位(不包含
USERNAME_FIELD和password)
這種方式需要自己定義所有必要的欄位,提供最大的彈性,但也最複雜。
何時使用這個方法?
如果你的需求是:
- ✅ 完全自訂欄位(不需要
first_name、last_name等預設欄位) - ✅ 需要完全控制認證流程
那麼這個方法適合你!但要注意,這個方法需要更多的設定。
AbstractBaseUser vs AbstractUser
| 特性 | AbstractUser | AbstractBaseUser |
|---|---|---|
| 保留預設欄位 | ✅ | ❌ |
| 需要自訂 Manager | ❌ | ✅ |
| 彈性 | 低 | 高 |
| 複雜度 | 低 | 高 |
| 適合場景 | 只需新增欄位 | 需要完全客製化 |
如何實作客製化 User Model¶
無論選擇哪種方法,都需要完成以下步驟:
步驟 1:建立 accounts app¶
通常我們會建立一個專門處理使用者相關功能的 app:
步驟 2:定義自訂的 User Model¶
在 accounts/models.py 中定義你的 User Model(參考前面的兩種方法)。
步驟 3:在 settings.py 中指定 AUTH_USER_MODEL¶
這是最關鍵的一步,告訴 Django 使用你的自訂 User Model:
INSTALLED_APPS = [
# ...
"django.contrib.auth",
# ...
"accounts", # (1)!
]
# ...
AUTH_USER_MODEL = "accounts.CustomUser" # (2)!
- 將 accounts app 加入 INSTALLED_APPS
- 指定自訂的 User Model,格式為
'app名稱.Model名稱'
必須在第一次 migrate 之前設定
AUTH_USER_MODEL 的設定必須在執行第一次 migrate 之前完成。
一旦執行了 migrate,Django 就會在資料庫中建立相關的表格和外鍵關係,之後就很難更改了。
步驟 4:建立並執行 migrations¶
步驟 5:更新 Admin 設定¶
如果你使用 Django Admin,需要註冊自訂的 User Model:
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,需要更多的設定:
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"),
},
),
)
- 在列表頁面顯示的欄位
- 編輯使用者時的欄位分組
- 新增使用者時的欄位(注意:需要包含 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,就需要:
- 刪除所有與舊 User Model 相關的表格
- 重新建立所有的關聯
- 遷移現有的使用者資料
- 處理可能的資料遺失問題
實際會遇到的問題¶
當你的專案已經有使用者資料時,要更換 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:
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
pass # (1)!
- 目前不新增任何欄位,但未來可以隨時新增!
這樣做可以保留未來擴充的彈性,而不會增加任何複雜度。
專案已經開始時的替代方案¶
如果你的專案已經開始運作,無法更換 User Model,可以考慮以下替代方案。
使用 OneToOneField 擴充¶
建立一個與 User 一對一關聯的 Profile Model:
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} 的個人資料"
- 與 User 建立一對一關係
- 在這裡新增額外的欄位
使用方式:
# 建立使用者的 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
- 透過
related_name='profile'存取
OneToOneField 的優缺點
優點:
- ✅ 不需要修改現有的 User Model
- ✅ 不需要處理複雜的 migration
- ✅ 可以保留現有的資料
缺點:
- ❌ 需要透過關聯來存取額外的欄位(
user.profile.phone_number) - ❌ 查詢時需要使用
select_related避免 N+1 問題 - ❌ 不能改變認證方式(仍然使用 username 登入)
使用 Signals 自動建立 Profile¶
為了確保每個使用者都有 Profile,可以使用 signals:
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)!
- 監聽 User 的 post_save 信號
- 只在建立新使用者時執行
- 儲存 Profile(確保 User 和 Profile 同步)
不要忘記在 accounts/apps.py 中載入 signals:
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "accounts"
def ready(self):
import accounts.signals # (1)!
- 載入 signals
任務結束¶
完成!
恭喜你完成了這個章節!現在你已經了解:
- Django 提供了兩種客製化 User Model 的方法:
AbstractUser和AbstractBaseUser -
AbstractUser適合只需新增欄位的情況,保留所有預設功能 -
AbstractBaseUser適合需要完全客製化認證方式的情況 - 客製化 User Model 必須在專案開始前(第一次 migrate 之前)完成
- 專案開始後更換 User Model 會導致資料庫遷移和外鍵關係的複雜問題
- 如果專案已經開始,可以使用 OneToOneField 建立 Profile Model 作為替代方案
重點回顧
客製化 User Model 是 Django 專案中最重要的決策之一,必須在專案初期就做出決定。
即使目前不需要額外欄位,也建議建立一個繼承 AbstractUser 的空 Model,以保留未來擴充的彈性。
記住:一旦執行了第一次 migrate,要改變 User Model 將會非常困難!