2023年10月26日 星期四

Python 學習筆記 : Django 4 用法整理 (四) 資料庫存取

終於來到 Django 的重頭戲~資料庫了, 這是建立資料庫驅動網站的基礎, 我之前學習 Django 2/3 時就是停在這裡, 參考 : 


本次複習的前三篇筆記參考 :



一. 透過 ORM 間接操作資料庫 : 

Django 使用 ORM (Object Relational Mapper) 物件關聯對映機制來操作資料庫, 開發者並非直接使用 SQL 來存取資料庫, 而是使用 Python 模型物件的方法來存取, 具體而言, 就是利用  django.db.models.Model 類別來定義資料表欄位的模型, 以模型做為程式與資料庫之間的中介層, 然後透過 ORM 機制將對 Model 物件之存取轉換為 SQL 指令, 如下圖所示 :




藉由 ORM 將資料庫處理作業抽象化後, 開發者就無須面對 SQL 語言, 只要單純使用 Python 物件就可以間接操作資料表, 因為 ORM 機制會將物件操作轉換成存取後台資料庫的 SQL 指令. ORM 的模型與實體資料庫的對映如下表 : 


 ORM 模型 關聯式資料庫
 類別 (Class) 資料表 (Table)
 物件 (Object) 紀錄 (Row)
 屬性 (Attribute) 欄位 (Field)
 方法 (Method) CRUD 操作


這種作法最大的好處是更換資料庫系統時只要編輯專案設定檔 settings, 將 DATABASE 變數 (字典) default 鍵的值改為要使用的資料庫即可, 預設是 SQLite :

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

連線 MySQL 資料庫的設定範例如下 :

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'mydatabase',
        'USER': 'root',
        'PASSWORD': 'mypassword',
        'HOST': 'localhost',
        'PORT': '3306',
    }
}

連線 Postgre 資料庫的設定範例如下 :

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'mydatabase', 
        'USER': 'tony1966', 
        'PASSWORD': 'mypassword',
        'HOST': '127.0.0.1', 
        'PORT': '5432',
    }
}

這些正式運營使用的伺服器型資料庫系統主要差異是為 ENGINE 與 PORT 這兩個鍵. 

參考 : 


Django 預設使用單檔案的 SQLite 資料庫, 用 Python 存取 SQLite 資料庫必須學習 SQL 語法, 參考 :



二. 建立資料庫驅動的網站 : 

要在 Django 專案中使用資料庫必須先建立 App, 在每個 App 目錄下都會自動建立與資料庫操作相關的檔案與資料夾.   

1. 建立網站 : 

首先以下列指令建立一個專案 mysite :

django-admin startproject mysite  

D:\django\test4>django-admin startproject mysite 

用 tree /f 指令檢視檔案目錄結構 : 

D:\django\test4\mysite>cd .. 
D:\django\test4>tree /f 

mysite
    │  manage.py
    │
    └─mysite
            asgi.py
            settings.py
            urls.py
            wsgi.py
            __init__.py

可見這時並無資料庫相關檔案. 


2. 建立應用程式 : 

接下來建立第一個應用程式 myapp1 為例 : 

python manage.py startapp myapp1 

D:\django\test4\mysite>python manage.py startapp myapp1

用 tree /f  指令檢視檔案目錄結構 : 

D:\django\test4\mysite>cd .. 
D:\django\test4>tree /f 

mysite
    │  manage.py
    │
    ├─mysite
    │  │  asgi.py
    │  │  settings.py
    │  │  urls.py
    │  │  wsgi.py
    │  │  __init__.py
    │  │
    │  └─__pycache__
    │          __init__.cpython-310.pyc
    │          settings.cpython-310.pyc
    │
    └─myapp1
        │  admin.py  
        │  apps.py
        │  models.py   
        │  tests.py
        │  views.py
        │  __init__.py
        │
        └─migrations  
                __init__.py

可見已經新增了應用程式目錄 myapp1, 裡面與資料庫有關的是 admin.py, models.py, 以及 migrations 子目錄, 其中 admin.py 用來把 App 註冊到內建的後台資料庫管理網站 admin 中; models.py 用來定義資料表的模型; 而 migrations 子目錄則用來記錄 migration (模型異動) 的資料檔. 

建立應用程式後首先必須開啟專案設定檔 settings.py, 在 INSTALLED_APPS 變數中註冊此 App, 這動作很重要, 因為後續要生成 migration  (模型異動) 資料檔時, Django 才會知道要去哪些 App 底下找 models.py 模型檔來比對資料表的變化情形 :

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myapp1', 
]


3. 定義資料表的模型 : 

App 目錄下的 models.py 用來定義與實體資料表對映的資料模型, 其預設內容只有兩列, 已預先匯入 django.db.models 模組, 只要在底下編輯各資料表的模型類別定義即可 : 

from django.db import models    

# Create your models here.

建立資料表模型須繼承 django.db.models.Model 類別, 然後呼叫 models 模組的欄位定義函式 models.xxxField() 建立欄位變數即可, 語法結構如下 :

from django.db import models

class table_name(models.Model):
    field_name_1=models.xxxField(param=value)
    field_name_2=models.xxxField(param=value)
    ...

    class Meta:
         ordering=('-field_name_x', )
         ...
    
    def __str__(self):
        return self.field_name_x   

其中方法 __str__() 為必須, 用來傳回要在內建的資料庫後台應用程式 admin 中顯示的資料表欄位; 子類別 Meta 則可有可無, 用來指定與資料庫相關之屬性, 常用的 Meta 類別屬性如下表 :

 
 Meta 類別屬性 說明
 ordering 用 tuple 指定排序欄位 (預設為升冪, 冠 - 為降冪)
 db_table 指定映射之資料表名稱, 預設自動映射至 '應用名_模型名' 資料表'
 app_label 指定此模型類別所屬之 App 名稱
 default_permissions 指定允許之模型操作許可權, 預設為 ('add', 'change', 'delete')
 get_latest_by 依指定之欄位排序以獲得模型之開始或結束紀錄 (日期或整數欄位) 
 unique_together 將多個欄位設定成不重複的欄位組合 (雙層串列, 例如 [['欄1', '欄3']])
 index_together 設定聯合索引之欄位 (雙層串列, 例如 [['欄1', '欄3']])
 required_db_vendor 限定模型維護之底層資料庫類型, 例如 ['SQLite', 'MySQL']
 db_tablespace 映射之表格空間名稱 (僅適用於如 Oracle 等有表格空間之資料庫)
 required_db_features 指定底層資料庫必須具備之特性, 例如 ['gis_enabled']
 order_with_respect_to 設定此模型可依據指定之外鍵引用關係排序
 

其中最常用的是 ordering 屬性, 用來指定資料庫傳回紀錄時要依據那些欄位來排序, 其值為一個欄位名稱組成的 tuple, 預設為升冪, 欄位名稱前面冠 '-' 則為降冪, 例如 ordering=(name, -birthday) 表示要先按欄位 name 升冪排序, 再按 birthday 欄位降冪排序. 

django.db.models 模組提供了許多欄位定義函式, 其名稱格式為 xxxField(), 如下表所示 : 


 欄位型態函式 說明
 BooleanField() 儲存布林值=True/False, 必要參數 : 無, checkbox 輸入用
 CharField() 儲存單行文字輸入內容,必要參數 :
 max_length=最大字元數 (上限 254)
 SlugField 與 CharField() 相同, 但用來儲存 URL 的一部分
 TextField() 儲存多行文字輸入 textarea 內容, 必要參數 : 無
 IntegerField() 儲存整數 (-2147483648 ~ 2147483647), 必要參數 : 無
 BigInteger() 儲存長度 64 位元的大整數
 PositiveIntegerField() 儲存正整數 (0 ~ 2147483647), 必要參數 : 無
 DecimalField() 儲存固定精度之十進位數 (Decimal 物件), 必要參數 :
 max_digits=最大位數
 decimal_places=整數位數
 FloatField() 儲存浮點數, 參數 : 無
 DateField() 儲存日期 (即 datetime.date), 可選參數 : 
 auto_now=物件儲存時自動儲存今日日期
 auto_now_add=只在建立時儲存今日日期
 DateTimeField() 儲存日期時間 (即 datetime.datetime), 可選參數 : 
 auto_now=物件儲存時自動儲存今日日期時間
 auto_now_add=只在建立時儲存今日日期時間
 EmailField() 儲存有效之電子郵件, 可選參數 :
 max_length=最大字元數 (上限 254)
 FileField() 檔案上傳欄位, 必要參數 : 無
 ImageField() 圖檔欄位 (繼承自 FileField, 須配合使用 Pillow 套件)
 URLField() 儲存完整的 URL (繼承自 CharField), 可選參數 :
 max_length=最大字元數 (預設 200)
 AutoField() 自動增量主鍵欄位, 必要參數 primary_key=True
 ForeignKey() 關聯欄位, 用來指向其他資料表的主鍵 (預設=id) :
 第一參數=所指之資料表類別名稱
 第二參數 : on_delete=models.CASCADE


呼叫上表中的欄位定義函式時除了必要參數外, 還可以傳入選項參數, 這些參數都是對映到 SQL 語法的欄位屬性, 常用選項參數如下表 :


 欄位選項參數 說明
 null 欄位值是否可為 null, 值=True/False (預設)
 blank 欄位值是否可為空白, 值=True/False (預設)
 default 欄位預設值 (或可呼叫物件)
 unique 欄位值是否為唯一, 值=True/False (預設)
 primary_key 欄位是否為主鍵, 值=True/False (預設)
 editable 欄位是否顯示於 admin 後台, 值=True (預設) /False 
 choices 設定 select 欄位之選項 (可用 list 或 tuple)
 help_text 顯示於表單元件上的額外資訊
 verbose_name 欄位之人類可讀名稱, 未指定以欄位名稱代替 (底線變空白)


例如定義一個會員資料表 Members 的模型 : 

from django.db import models

class Members(models.Model):
    name=models.CharField(max_length=20, null=False)
    gender=models.CharField(max_length=2, default='男', null=False)
    birthday=models.DateField(null=False)
    email=models.CharField(max_length=100, blank=True, default='')
    phone=models.CharField(max_length=50, blank=True, default='')
    address=models.CharField(max_length=255, blank=True, default='')
    
    def __str__(self):
        return self.name

此例定義了一個包含 6 個欄位的資料表模型 Members, 除了 birthday 欄位是日期欄位外, 其餘都是長度不等之單行文字欄位. 

models.py 編輯完成存檔後可先用 check 指令檢查模型設定, 如果顯示 "System check identified no issues" 表示設定正確 :

python manage.py check    

D:\django\test4\mysite>python manage.py check
System check identified no issues (0 silenced).


4. 建立 migration 資料檔與資料庫同步 : 

 models.py 建立在上層專案目錄下先用 makemigrations 指令產生模型遷移檔 :

python manage.py makemigrations     (對全部 App)

也可以指定要對哪個 App 建立遷移檔 : 

python manage.py makemigrations myapp1    (只對指定之 App)

migrations 指令會依據 App 的 models.py 內容在該 App 的 migrations 資料夾下產生遷移檔, 主要是紀錄資料表的結構與版本, 並與 migrations 資料夾下面的前一版本的遷移檔比較模型是否有異動. 如果未指名 App, 則會去專案設定檔找尋全部已註冊的 App 逐一建立遷移檔. 

D:\django\test4\mysite>python manage.py makemigrations
Migrations for 'myapp1':
  myapp1\migrations\0001_initial.py
    - Create model Members  

可見此指令新增了一個建立 Members 模型的 0001_initial.py 遷移檔, 其中 0001 表示版本 (第二次更改模型時版本就會變成 0002), initial 表示異動說明 (初次建立模型).   

這時用 tree /f 觀察網站檔案目錄結構, 會發現上層專案目錄下新增了一個 SQLite 資料庫檔db.sqlite3. 注意, 這是所有 App 共用的資料庫, 只有在建立第一個 App 時會產生, 之後的 App 不會再產生 : 

mysite
    │  manage.py
    │  db.sqlite3    
    │
    ├─mysite
    │  │  asgi.py
    │  │  settings.py
    │  │  urls.py
    │  │  wsgi.py
    │  │  __init__.py
    │  │
    │  └─__pycache__
    │          __init__.cpython-310.pyc
    │          urls.cpython-310.pyc
    │          settings.cpython-310.pyc
    │
    └─myapp1
        │  admin.py
        │  apps.py
        │  models.py
        │  tests.py
        │  views.py
        │  __init__.py
        │
        ├─migrations
        │  │  __init__.py
        │  │  0001_initial.py   
        │  │
        │  └─__pycache__
        │          __init__.cpython-310.pyc
        │
        └─__pycache__
                __init__.cpython-310.pyc
                apps.cpython-310.pyc
                models.cpython-310.pyc
                admin.cpython-310.pyc

開啟 migration 資料檔 0001_initial.py 可知其內容就是 models.py 的資料表欄位定義, 它只是將 models.Model 類別轉成了 Migration 類別, 以便後續執行 migrate 指令時與實體資料庫同步, 其內容如下 : 

# Generated by Django 4.2.4 on 2023-10-22 16:12

from django.db import migrations, models

class Migration(migrations.Migration):
    initial = True
    dependencies = [
    ]
    operations = [
        migrations.CreateModel(
            name='Members',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=20)),
                ('gender', models.CharField(default='男', max_length=2)),
                ('birthday', models.DateField()),
                ('email', models.CharField(blank=True, default='', max_length=100)),
                ('phone', models.CharField(blank=True, default='', max_length=50)),
                ('address', models.CharField(blank=True, default='', max_length=255)),
            ],
        ),
    ]

然後在上層專案目錄下用下列指令讓模型與資料庫同步 :

python manage.py migrate   

此指令會到每一個 App 目錄的 migrations 子目錄下的遷移紀錄檔, 比較前後版本差異後去更改資料表, 這樣便達成模型定義與資料庫同步的目的. 也可以在 migrate 後面指定要同步的 App, 這樣就只會同步該 App :

 python manage.py migrate myapp1

注意, 每次修改 models.py 後都要重新執行 makemigrations 與 migrate 指令才能維持模型與資料庫的同步. 例如 : 

D:\django\test4\mysite>python manage.py migrate  
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, myapp1, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying myapp1.0001_initial... OK
  Applying sessions.0001_initial... OK

可見 migrate 指令會依照 makemigrations 產生的遷移資料檔 0001_initial.py 去同步資料庫. 

如果將 email 欄位長度由 100 改成 120, 再次執行 makemigrations 與 migrate 指令重新進行遷移與同步 : 

D:\django\test4>cd mysite

D:\django\test4\mysite>python manage.py makemigrations
Migrations for 'myapp1':
  myapp1\migrations\0002_alter_members_email.py 
    - Alter field email on members

D:\django\test4\mysite>python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, myapp1, sessions
Running migrations:
  Applying myapp1.0002_alter_members_email... OK

可見在 myapp1/migrations 資料夾下多出了 0002_alter_members_email.py 這個中介檔, 從名稱可知此次是要異動模型中的 email 欄位, 內容如下 :

# Generated by Django 4.2.5 on 2023-10-26 03:19

from django.db import migrations, models

class Migration(migrations.Migration):

    dependencies = [
        ('myapp1', '0001_initial'),  
    ]

    operations = [
        migrations.AlterField(
            model_name='members',
            name='email',
            field=models.CharField(blank=True, default='', max_length=120),
        ),
    ]

Django 會比較 models.py 與最近一版的 migrations 資料檔 (即 dependencies 變數中的 '0001_initial'), 得出要如何做異動 (即 operations 變數之值). 經過 migrate 後, 模型的最新異動就與資料庫 db.sqlite3 同步了. 


5. 使用資料庫後台管理程式 admin : 

Django 內建了一個用來管理後台資料庫的應用程式 admin, 專案路由模組 urls.py 中預設路由 path('admin/', admin.site.urls) 即用來存取此 App, 在瀏覽器輸入http://127.0.0.1:8000/admin 會顯示登入畫面 :




欲進入 admin 後台必須用 createsuperuser 指令設定登入帳密, 先按 Ctrl + C 停止測試伺服器, 然後輸入下列指令進行設定 :

python manage.py createsuperuser   

D:\django\test4\mysite>python manage.py createsuperuser
Username (leave blank to use 'user'): 
Email address: abc@gmail.com
Password:
Password (again):
Superuser created successfully.

用 python manage.py runserver 重新開啟測試伺服器, 然後用上面申請的帳密即可登入 : 




可見後台並未出現 Members 模型的資料表, 這是因為沒有向 admin 註冊此模型之故, 須編輯 App 目錄下的 admin.py 將此模型註冊到 admin 應用程式中才行. admin.py 預設只有一列匯入了 admin 應用程式的指令 :

from django.contrib import admin

# Register your models here.
 
在 admin.py 加入如下指令, 先匯入 myapp1 模型檔 models.py 中的模型類別 Members, 然後呼叫 admin.site.register() 函式註冊此 Members 模型 :  

# admin.py
from django.contrib import admin  
from myapp1.models import Members   # 匯入 Members 模型

admin.site.register(Members)    # 向 admin 註冊 Members 模型

存檔後重新整理網頁就會看到 Members 模型所對應的資料表了 :




可見實體資料庫中的資料表名稱是 Memberss, 與模型名稱 Members 有所不同, 不過因 ORM 作為中介, 開發者不會直接存取實體資料庫, 故實際資料表名稱為何無關緊要.  

按 +Add 可新增資料 : 




按 Save 儲存後顯示使用者列表 : 




此處顯示人名是因為 models.py 中的模型裡 __str__() 方法傳回的是 name 欄位之故 : 

    def __str__(self):
        return self.name

如果要顯示更多欄位, 必須在 admin.py 中定義一個 ModelAdmin 類別的子類別, 在其中定義一個名為 list_display 的顯示欄位 tuple, 而且必須向 admin 註冊, 例如 :

# admin.py 
from django.contrib import admin
from myapp1.models import Members   # 匯入 Members 模型

class MembersAdmin(admin.ModelAdmin):    # 顯示欄位類別
    list_display=('id', 'name', 'gender', 'birthday', 'email', 'phone', 'address')

# 向 admin 註冊 Members, MembersAdmin
admin.site.register(Members, MembersAdmin)     # 顯示欄位類別也要註冊

存檔後重新整理網頁就會取代 __str__() 的顯示單一欄位功能而顯示所指定的多欄位了 : 




可見資料預設是以 id 倒序 (降冪) 排列的, 如果要用升冪排序, 須於 MembersAdmin 類別中定義一個名為 ordering 的 tuple 變數, 其元素為預設排序欄位例如 'id', 如果要降冪排序, 則欄位名稱前面冠 "-" 即可, 例如 '-id' : 

from django.contrib import admin
from myapp1.models import Members   # 匯入 Members 模型

class MembersAdmin(admin.ModelAdmin):
    list_display=('id', 'name', 'gender', 'birthday', 'email', 'phone', 'address')
    ordering=('id', )     # 依照 id 升冪排序

# 向 admin 註冊 Members, MembersAdmin
admin.site.register(Members, MembersAdmin)

重新整理網頁就會按照 id 升冪排序了 : 




注意, 指定排序欄位 id 後面有個向上的小三角形, 表示是升冪排序欄位. 其實點按每個顯示欄位都會按照該欄位升冪或降冪排序, ordering 只是指定預設之排序欄位而已. 

除了 ordering 與 list_display 變數外, ModelAdmin 類別內還可以用 search_fields 變數指定搜尋欄位; 也可以用 list_filter 指定資料過濾欄位, 它們都是 tuple 變數, 例如 : 


from django.contrib import admin
from myapp1.models import Members   # 匯入 Members 模型

class MembersAdmin(admin.ModelAdmin):
    list_display=('id', 'name', 'gender', 'birthday', 'email', 'phone', 'address')
    ordering=('id', )
    search_fields=('name', 'email', 'phone', 'address',)
    list_filter=('name', 'gender')

# 向 admin 註冊 Members, MembersAdmin
admin.site.register(Members, MembersAdmin) 

注意, 並非所有欄位都可以可以放在 list_filter 過濾, 可以過濾的欄位類型只有下面幾種 : 
  • CharField
  • BooleanField
  • DateField
  • DateTimeField 
  • IntegerField
  • ForeignKey
重新整理網頁, 在上方會出現搜尋控件, 可搜尋 search_fields 變數所指定之欄位; 其次右方會出現 list_filter 變數指定之欄位值, 點擊就會顯示依據該值過濾之紀錄列表 : 




以上測試範例檔放在 GitHub :


沒有留言 :