2020年11月29日 星期日

Python 學習筆記 : Django 3 測試 (七) : 資料庫操作

資料庫操作是動態網頁的核心, 不論是會員登入, 部落格, 留言板, 或者討論區等功能都必須利用資料庫來儲存內容, Django 預設採用一個檔案式的簡易型資料庫管理系統 SQLite (與 MySQL 相容), 但只適用於測試開發或小型網站系統之用, 真正上架營運時會使用 MySQL 或 Postgre 等容量或效能較大之資料庫, 但 Django 使用 ORM 操作資料庫, 因此轉換資料庫非常容易, 只需調整設定, 不需要修改應用程式. 

本系列之前的筆記參考 : 


以下測試參考了如下書籍 : 
  1. Python 架站特訓班 : django 最強實戰 (碁峰, 文淵閣)
  2. Python 新手使用 django 架站技術實作 (何敏煌, 林亮昀, 博碩)
  3. Tango with Django 2 (Leif Azzopardi and David Maxwell, 2020, Learnpub)
以及線上教學網站 : 


互動網站的資料庫操作主要是 CRUD (Create 新增, Retrieve 查詢, Update 更新, Delete 刪除), 傳統關聯式資料庫使用 SQL 語言來實現 CRUD 操作, 範例如下 :


 CRUD 操作 SQL 指令 SQL 指令範例
 Create (新增) INSERT INSERT INTO users(name,age) VALUES('Jim',25)
 Retrieve (查詢) SELECT SELECT * FROM users WHERE age < 40
 Update (更新) UPDATE UPDATE users SET country='TWN'
 Delete (刪除) DELETE DELETE FROM users WHERE age > 30


參考 :

最常用的 SQL 指令

有別於其他後端技術直接使用 SQL 語法存取資料表, Django 資料庫操作並不需要直接面對 SQL, 而是使用抽象化的物件關聯對應模型 ORM (Object Relational Mapper) 將底層 SQL 操作包裝成物件方法, 具體來說就是提供了 django.db.models.Model 類別作為應用程式與資料庫之間的介面 (MTV 架構中的 M 指的就是這個 Model 類別), 只要使用由這個類別所建立之資料庫模型物件就能操作資料庫與資料表. 由於各家資料庫系統所使用之 SQL 語法存在差異, ORM 將操作 SQL 的細節包裝起來最大的好處就是幾乎可以無痛轉換資料庫, 從預設的 SQLite 轉換至 MySQL 或 Postgre 等資料庫時只要改換設定即可, 不需要修改程式或調整 SQL 語法. 

Django 的資料庫與 App 檔案結構綁在一起, 建立 App 時也會同時建立資料庫相關的檔案與目錄, 例如在前一篇測試中使用 python manage.py startapp helloworld 指令建立第一個應用程式 helloworld 時所建立與資料庫相關之檔案目錄如下圖所示 : 





以下摘要整理與資料庫操作相關之檔案和目錄 : 
  

1. 預設資料庫設定 :

在專案中建立第一個 App 時會在第一層專案目錄下產生預設的空白 SQLite 資料庫檔案 db.sqlite3, 此資料庫優點是備份方便 (非伺服器型之單一檔案) 且與 MySQL 相容, 適合測試開發或小型專案使用, 但在擴充性與效能上較不足, 實際佈署營運時通常改用 MySQL 或 Postgre 等資料庫 (Django 社群較偏愛 PostgreSQL, 例如免費主機 Heroku 支援 PostgreSQL), 但 Django 採用 ORM 架構, 因此只要修改資料庫設定, 不需修改應用程式. 

Django 的資料庫設定放在第二層專案目錄下的 settings.py 檔案裏, 其中的 DATABASE 變數即資料庫的設定, 預設為 SQLite, 在專案測試階段通常使用預設之 SQLite 資料庫, 不需要修改 : 

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

當系統要上線實際佈署到伺服器時就需要調整變數 DATABASES 裡的預設資料庫設定, 例如改用 MySQL 資料庫時所需的設定語法如下 :

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

若使用 Postgree 資料庫, 設定語法例如 :

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

主要的差別就是 ENGINE 與 PORT 不同而已. 參考 :



2. 資料模型定義 : 

最重要的是 App 目錄下用來定義資料表結構 (資料模型) 的 models.py 檔, 其預設內容為 :

from django.db import models     

# Create your models here.

預設已匯入 django.db.models 模組, 定義資料表結構 (資料模型) 只要加入下列兩個部份即可 :
  • 繼承 models.Model 類別定義一個資料模型子類別 (即資料表)
  • 此模型類別應定義一個 __str__() 方法傳回某個欄位名稱
注意,  __str__() 方法的作用是在資料庫後台管理程式 admin 中顯示資料表內容時會列出資料表內作為代表之某個欄位內容 (例如標題等), 如果模型的類別內沒有定義此方法, 則在資料庫後台葉面顯示此模型之資料表紀錄時將僅顯示此為物件 (xxx object) 而已, 無法得知這是哪一項紀錄, 可讀性不佳, 定義模型類別之語法如下 : 

class table_name(models.Model):
    field_name_1=models.xxxField(param=value)
    field_name_2=models.xxxField(param=value)
    ...
    def __str__(self):
        return self.field_name_x   #選擇可代表該資料物件之欄位名稱傳回

注意, 類別名稱慣用法為首字母大寫. Django 所有的資料模型都繼承自 django.db.models.Model 類別, 每一個繼承 Model 的子類別相當於是資料表 (table); 其實體 (物件) 對應到一筆紀錄 (row 或 record); 物件的屬性則對應到資料欄位 (fields), 而物件方法則對應到資料庫的 CRUD 操作, 如下表所示 :


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


Model 類別會將其實體物件之屬性與方法轉換成 SQL 語言, 所以程式設計者只要呼叫模型物件的方法 (create, get, save, delete 等) 即可存取資料庫, 不須使用 SQL 語法, Django 的 ORM 封裝了這些資料庫的介面細節. 

網站後端開發應先依據需求進行資料庫設計, 規劃需要哪些資料表, 每個資料表有哪些欄位, 以及資料表之間的關聯性等, 然後再進行程式設計. 若資料庫未規劃完全即進行程式設計, 很可能中途要回頭更改資料庫, 連帶影響已設計之程式需配合更改.

Django 的 django.db.models.Model 類別提供許多方法可定義資料表的欄位型態以及與其他資料表的關聯, 常用方法如下表所示 :


 欄位型態方法 說明
 BooleanField() 儲存布林值=True/False, 參數 : 無
 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


參考 :


最後的兩個方法 AutoField() 與 ForeignKey() 所定義的欄位比較特殊, 其中 AutoField() 用來設定自動增量主鍵 (primary_key) 欄位, 其實 Django 會自動為每一個資料表添加一個主鍵欄位 id, 但若想自行指定主鍵欄位可以使用 AutoField() 方法. 參考 :


其次, ForeignKey() 欄位則是用來關聯到其他資料表 (稱為外部鍵), 此欄位的第一參數所關聯之資料表類別名稱, 用來指向該資料表的主鍵, Django 預設會自動將其關聯到該資料表之 id 主鍵, 例如上面的 users 資料表中可以添加一個 nationality (國籍) 欄位指向另一個資料表 nations : 

nationality=models.ForeignKey(nations, on_delete=models.CASCADE) 




ForeignKey() 的第二參數用來設定當被參照的外部鍵被刪除時參照者應採取之動作, Django 的 models 套件定義了如下常用之屬性值 :
  • models.CASCADE : 同步執行刪除動作
  • models.PROTECT : 不刪除且拋出一個 ProtectedError 例外
  • models.SET_NULL : 將此外部鍵設為 null (須先以 null=True 參數定義此欄位)
  • models.SET_DEFAULT : 將此外部鍵設為預設值 (須先以 default 參數定義此欄位預設值)
  • models.DO_NOTHING : 不採取動作
呼叫欄位型態函數時除了必要參數外, 還可以傳入選項參數, 常用選項參數如下表 :


 欄位選項參數 說明
 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 欄位之人類可讀名稱, 未指定以欄位名稱代替 (底線變空白)


參考 : 



3. 資料模型與資料庫之遷移與同步 : 

App 目錄下還有一個遷移子目錄 migrations, 其用途為記錄資料模型定義與版本變更, 預設只有一個檔案__init__.py 與目錄 __pycache__, 例如上一篇測試中的 helloworld 應用程式 :




這個 migrations 子目錄其實就是用來維持資料模型與資料庫同步的中介目錄, 每次修改資料模型檔 models.py 後, 必須依序執行下列兩個指令, 第一個指令 makemigrations 是將資料模型之變動情形先遷移或紀錄到這個目錄下 (makemigrations); 然後第二個指令 migrate 再根據 migrations 目錄中的內容將資料模型變化與真實資料庫同步 :
  • python manage.py makemigrations AppName   
    此指令會將應用程式 AppName 目錄下的 models.py 資料模型變更紀錄在該 App 的 migrations 子目錄內. 若省略 AppName 則會對此專案下的所有 App 進行資料模型變更紀錄作業.
  • python manage.py migrate AppName
    此指令會根據應用程式 AppName 目錄下的 migrations 子目錄所記錄的變更內容, 進行資料模型與資料庫同步作業. 若省略 AppName 則會對此專案下的所有 App 進行資料庫同步作業. 
因為在上一篇測試的 helloworld 應用程式中沒有使用資料庫, 因此即使沒有執行 migrations 也不妨礙 App 的執行. 


4. 通訊錄 App 資料表測試 : 

以下參考 "Python 架站特訓班 : django 最強實戰" 這本書中第四章的 student 資料表範例加以修改為通訊錄 App, 於本機 localhost 中測試 contact 資料表的 CRUD 操作. 


(1). 建立與註冊應用程式 :   

進入虛擬環境在第一層專案目錄下以下列指令建立 contact 應用程式 : 

python manage.py startapp contact

D:\django\venv>Scripts\activate     
(venv) D:\django\venv>cd mysite    
(venv) D:\django\venv\mysite>python manage.py startapp contact    

在專案目錄上一層用 tree 指令可以顯示此專案目錄結構下新增了 contact 應用程式子目錄 : 

(venv) D:\django\venv>tree mysite  
D:\DJANGO\VENV\MYSITE
├─contact
│  └─migrations
├─helloworld
│  ├─migrations
│  │  └─__pycache__
│  └─__pycache__
├─mysite
│  └─__pycache__
├─static
│  ├─css
│  ├─images
│  └─js
└─templates

建立 App 後須至專案設定檔 settings.py 中註冊此應用程式, 因為後續作資料模型遷移與同步等作業時, Django 管理程式 manage.py 都會檢查 App 是否存在, 開啟第二層專案目錄下的 settings.py 檔, 找到 INSTALLED_APPS 這個串列變數, 在最後面添加新增的應用程式 contact :

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'helloworld',
    'contact'


(2). 編輯資料模型檔 models.py :    

開啟 App 目錄 contact 下的 models.py, 定義一個繼承自 Model 的 contact 類別 (相當於資料表), 並呼叫 models 模組的欄位定義方法定義資料表內的欄位型態 : 

#models.py
from django.db import models

class contact(models.Model):
    cName=models.CharField(max_length=20, null=False)
    cSex=models.CharField(max_length=2, default='M', null=False)
    cBirthday=models.DateField(null=False)
    cEmail=models.EmailField(max_length=100, blank=True, default='')
    cPhone=models.CharField(max_length=50, blank=True, default='')
    cAddress=models.CharField(max_length=255, blank=True, default='')
    
    def __str__(self):
        return self.cName

此資料模型定義了六個欄位的資料表 contact, 並傳入選項參數約束欄位之性質, 例如設定預設值的 default 以及可容許空白的 blank 與可容許無值的 null 等. 


(3). 製作資料模型遷移紀錄與同步資料庫 :

只要資料模型定義檔 models.py 有修改就要執行 migrations 動作使模型與資料庫能同步, 首先用 makemigrations 指令製作 contact 模型之遷移紀錄, 此處為了單純起見指定僅遷移 contact 應用程式之模型 (若不指定則是遷移所有 App 的資料模型) : 

(venv) D:\django\venv\mysite>python manage.py makemigrations contact   
Migrations for 'contact':
  contact\migrations\0001_initial.py
    - Create model contact

可見完成後在 App 的 migrations 子目錄下產生了第一個遷移紀錄檔 0001_initial.py : 




其內容如下 :

# Generated by Django 3.1.3 on 2020-11-27 08:40
from django.db import migrations, models

class Migration(migrations.Migration):
    initial = True
    dependencies = [
    ]
    operations = [
        migrations.CreateModel(
            name='contact',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('cName', models.CharField(max_length=20)),
                ('cSex', models.CharField(default='M', max_length=2)),
                ('cBirthday', models.DateField()),
                ('cEmail', models.EmailField(blank=True, default='', max_length=100)),
                ('cPhone', models.CharField(blank=True, default='', max_length=50)),
                ('cAddress', models.CharField(blank=True, default='', max_length=255)),
            ],
        ),
    ]

此遷移紀錄就是模型與資料庫同步所需要的中介檔, 它其實是從 models.py 轉換而來的. 注意, Django 自動為此資料表添加了一個名為 id 的自動增量主鍵欄位, 這也是使用 ForeignKey() 外部鍵欄位關聯其他資料表時預設之綁定欄位. 

建立遷移紀錄後即可用 migrate 指令進行模型與資料庫同步動作, 同樣也是指定 contact 應用程式之模型 (若不指定則同步所有 App 之模型), 也就是執行上面的中介檔程式  0001_initial.py  :

(venv) D:\django\venv\mysite>python manage.py migrate contact   
Operations to perform:
  Apply all migrations: contact
Running migrations:
  Applying contact.0001_initial... OK

此 migrate 指令執行 contact.0001_initial.py 中介檔後會在專案預設資料庫 db.sqlite3 中建立資料表, 使用 DB Browser for SQLite 軟體瀏覽 db.sqlite3 可知已根據資料模型建立了三個資料表 : 




可見同步時 ORM 真正建立的資料表名稱是 contact_contact 而非 contact, 點選此資料表切換到 Browse Data 頁籤可看到 Django 自動添加的 id 自動增量主鍵欄位 :




利用此軟體可以執行 SQLite 資料庫的 CRUD 操作, 關於 DB Browser for SQLite 用法參考 :



(4). 用內建應用程式 admin 管理資料庫 : 

雖然可以使用外部軟體例如 DB Browser for SQLite 來管理 SQLite 資料庫, 但其實不假外求,  Django 本身就內建了一個類似 PHP 的 phpMyAdmin 那樣的資料庫後台管理應用程式 admin, 但資料表必須經過註冊程序, 並且設定管理者帳戶後我們才能使用此後台 App 來管理資料庫. 

在建立 App 時會在 App 目錄下產生一個名為 admin.py 的資料模型註冊管理程式 :




開啟 admin.py 可知預設只是匯入了 django.contrib.admin 這個 App 模組而已 :

from django.contrib import admin

# Register your models here.

欲在 admin 後台應用程式中管理此 App 之資料表, 需在此 admin.py 程式中註冊此 App 所用到的的資料表 (亦即在 models.py 中所定義之資料模型名稱), 因為 contact 應用程式只有一個資料表, 因此只要從 contact.models 模組匯入 contact 類別, 並呼叫 admin.site.register() 方法註冊 contact 模型類別即可 :

from django.contrib import admin
from contact.models import contact     #匯入資料模型類別 (即資料表)

admin.site.register(contact)                   #註冊資料表

藍色部分為新增程式碼, 編輯後存檔, 這樣這個資料表即納入 admin 應用程式管理了, 但必須先建立後台管理員帳戶才能登入系統, 先按 CTRL+C 關閉測試伺服器, 執行 createsuperuser 指令設定超級使用者時卻出現錯誤 : 

(venv) D:\django\venv\mysite>python manage.py createsuperuser     

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
......
......
django.db.utils.OperationalError: no such table: auth_user    

原因是在上面使用 migrate 指令同步資料庫時指定了 contact 應用程式, 所以只同步了 contact 這個 App 的模型, 但 admin 這個內建的後台 App 使用了一些內建資料模型, 但卻沒有同步到資料庫中, 因此找不到 auth_user 等資料表, 必須重新同步所有 App 的資料模型才行, 先關閉 DB Browser for SQLite 程式 (否則會 lock 住 db.sqlite3 資料庫檔) : 

(venv) D:\django\venv\mysite>python manage.py migrate     
Operations to perform:
  Apply all migrations: admin, auth, contact, contenttypes, 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 sessions.0001_initial... OK


可見內建應用程式也許多資料模型, 可用 DB Browser for SQLite 程式開啟 db.sqlite3 瀏覽 : 




這時再次使用 createsuperuser 就可以順利設定後台管理員帳密了 :

(venv) D:\django\venv\mysite>python manage.py createsuperuser    
使用者名稱 (leave blank to use 'user'): tony1966
電子信箱: tony1966@brabra.com.tw
Password:
Password (again):
Superuser created successfully.

然後用 runserver 重新開啟測試伺服器, 在瀏覽器輸入網址 127.0.0.1:8000/admin :




登入成功後即進入資料庫後台管理網頁, 在 "網站管理" 項下會列出各 App 的資料表, 此處 contact 應用程式底下僅有一個資料表 contact :




按右方的 "+新增" 鈕會出現輸入表單可新增紀錄 :




連續新增兩筆後結果下如下 :




注意, 這裡顯示聯絡人之姓名的原因是在 models.py 裡面定義 contact 模型類別時有定義了 __str__() 方法並傳回此類別之 cName 屬性之故 : 


    def __str__(self):
        return self.cName   

將 models.py 中的 __str__() 方法去除, 則預設將傳回物件本身, 因而只顯示 object : 



這樣就無法得知該紀錄到底是哪一筆紀錄了. 

在 __str__() 方法中傳回欄位名稱只能在紀錄列表顯示一個欄位值, 如果要顯示多個欄位資料必須在 admin.py 程式中定義一個繼承 admin.ModelAdmin 類別之子類別, 然後將欲顯示之欄位名稱放在元組中指派給 list_display 變數, 最後在註冊資料模型時將此子類別做為第二參數傳給 admin.site.register() 方法即可, 例如 :

# admin.py
class contactAdmin(admin.ModelAdmin):
    list_display=('id', 'cName', 'cSex', 'cBirthday', 'cEmail', 'cPhone', 'cAddr')
admin.site.register(contanct, contactAdmin)

結果如下 :




此 ModelAdmin 子類別除了提供顯示多欄位功能外, 也可以指定過濾欄位, 或進行搜尋與排序, 這需要在 admin.py 程式中添加 list_filter (過濾欄位元組), search_fields(搜尋欄位元組), 以及 ordering (排序欄位元組) 這三個變數, 修改後的 admin.py 如下所示 : 

#admin.py
from django.contrib import admin
from contact.models import contact

class contactAdmin(admin.ModelAdmin):
    list_display=('id', 'cName', 'cSex', 'cBirthday', 'cEmail', 'cPhone', 'cAddress')
    list_filter=('cName', 'cSex')  
    search_fields=('cName',)  
    ordering=('id',)  

admin.site.register(contact, contactAdmin)

注意, 這四個變數值必須為串列或元組, Python 的串列與元組若只有一個元素後面必須有一個逗號, 否則會出現錯誤. 其次 ordering 預設為遞增排序, 如果要做遞減排序, 則要在欄位前加 "-" 字元, 例如 ordering=("-id",). 重新整理網頁結果如下 :




可見在記錄列表以 id 排序 (遞增), 上方出現了搜尋框, 可搜尋資料表內之紀錄內容; 右方則出現了過濾選項. 

沒有留言:

張貼留言