Django -- 從平凡到超凡

第 7 章    資料模型與資料庫

∗ Django 的資料庫模組 (Database module):物件關聯對應 (Object-relational mapping, ORM)

▸ ORM 是一個程式技術,以物件導向程式語言在不相容的資料系統之間做資料轉換

▸ 不需要撰寫結構化查詢語言 (Structural Query Language, SQL)

▸ 可方便替換不同廠牌的資料庫:若在系統中直接撰寫連結 A 廠牌資料庫的 SQL 程式,換上 B 廠牌資料庫會不相容, 反之,以 ORM 當作中介軟體 (Middleware),就可自由替換資料庫,不必修改程式

orm

∗ Django 的資料模型 (Data model)

▸ Django model 是一個 Python class,用來描述資料模型

▸ 我們可以直接利用 Python 物件來操作資料,不需要透過 SQL

(1) 建立系統管理者

∗ 每個 Django project 都需要系統管理者 (也稱為超級使用者,Superuser)

▸ Django 提供功能強大的管理者頁面,用來管理資料

▸ Django 內建有 User model,用來儲存使用者資料,可在裡面建立 Superuser 帳號:

(blogVenv)$ cd <project>
(blogVenv)$ python manage.py createsuperuser
Username (leave blank to use '<username>'): admin
Email address: admin@gmail.com
Password: admin12345
Password (again): admin12345
Superuser created successfully.
✶ 註:亦可使用其他帳號與密碼,電子信箱真實性不重要

▸ 進入 admin 頁面:localhost:8000/admin/

admin page
✶ 練習:在 admin 頁面嘗試新增一個新使用者

(2) 建立模型

∗ 規劃 article app 的兩個 Model:Article 與 Comment

Article:

title
(Char)
content
(Text)
- -

Comment:

article
(ForeignKey)
content
(Char)
- -

▸ Django 使用 Python 的 class (類別) 來建立 model (Model名稱通常大寫),編輯 article/models.py,加入以下內容:

article/models.py
from django.db import models


class Article(models.Model):
    title = models.CharField(max_length=128, unique=True)
    content = models.TextField()

    def __str__(self):
        return self.title


class Comment(models.Model):
    article = models.ForeignKey(Article)
    content = models.CharField(max_length=128)

    def __str__(self):
        return self.article.title + '-' + str(self.id)
from ...:從 django.db 匯入 models
class Article ...:宣告一個 Article model (是一個 Python class),繼承 models.Model
# 有 2 個欄位:
- title:文章標題,字元欄位 (單行),最多 128 個字元,標題為唯一 (亦即:不能有兩篇相同標題的文章)
- content:文章內容,文字欄位 (大量、包含 Enter)
# def __str__(self)...:定義一個 __str__() 方法, 回覆 self.title 值,這是在範本中此物件預設顯示的值,在 admin 介面亦顯示此值
class Comment(...):宣告一個 Comment model, 繼承 models.Model
# 有 2 個欄位:
- article:外來鍵 (Foreign key),指向 Article model,指定此留言所屬的文章
∗ 一篇文章可以有許多留言,一個留言只能針對某一篇文章,因此是多對一的關係,利用外來鍵關聯
∗ 外來鍵的欄位名稱習慣上就設定為所指向的 model 名稱,但改為小寫
- content:留言內容,字元欄位 (單行),最多 128 個字元
# def __str__(self):定義一個 __str__() 方法, 回覆值為 self.article.title + '-' + str(self.id) (亦即將文章標題串上該物件的 id)
✶ Django ORM 欄位資料的存取是使用點號方式 (此為物件導向語言的標準用法),因此從留言 (Comment) 連到其所屬文章 (Article), 再取出該文章的標題 (title), 就可使用一連串的點號方式:coment.article.title
✶ 註:欄位名稱不可使用 Django model API 的名稱,例如 clean, save, delete
Model 常用的欄位資料型態

Django model 提供許多欄位資料型態,常用的如下:

CharField:字元(單行)
DateField:日期
DateTimeField:日期時間
EmailField:電郵
FileField:檔案
FloatField:浮點數
ImageField:影像
IntegerField:整數
TextField:文字(多行、大量)
ForeignKey:多對一關聯 (外來鍵)
OneToOneField:一對一關聯
ManyToManyField:多對多關聯

(3) 資料庫遷移

∗ 執行 makemigrations

▸ 有任何 Model 的新增或修改,都需要立即執行資料庫遷移,如下:

--> Right click project --> Django --> Custom Command --> Command: makemigrations --> OK
Migrations for 'article':
  article/migrations/0001_initial.py:
    - Create model Article
    - Create model Comment
Finished "/home/<username>/webapps/git/blog/blog/manage.py makemigrations" execution. 

makemigrationsarticle/migrations/ 目錄下建立 0001_initial.py 程式檔 (稱為 Migration file,遷移檔),用來產生資料表,內容如下:

article/migrations/0001_initial.py
# -*- coding: utf-8 -*-
# Generated by Django x.x.x on xxxx-xx-xx xx:xx
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Article',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(max_length=128, unique=True)),
                ('content', models.TextField()),
            ],
        ),
        migrations.CreateModel(
            name='Comment',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('content', models.CharField(max_length=128)),
                ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='article.Article')),
            ],
        ),
    ]
✶ 如果使用者並未定義 id 欄位,Django 會自動增加一個 id 欄位 (正整數)
--> 最好不要自行定義 id 欄位,交由 Django 來設定及操作較好
✶ 可利用 $ python manage.py sqlmigrate <app> <migrateNumber> 指令顯示 0001_initial.py 內的 SQL 指令 ,例如:
(blogVenv)$ python manage.py sqlmigrate article 0001
BEGIN;
--
-- Create model Article
...
-- Create model Comment
...

COMMIT;

∗ 執行 Migrate

▸ Right click project --> Django --> Migrate

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, article, sessions
Running migrations:
  Applying article.0001_initial... OK
Finished "/home/<username>/webapps/git/blog/blog/manage.py migrate" execution.

∗ 在管理者頁面檢視資料

▸ 在 article/admin.py 登記 ArticleComment,即可在 admin 頁面檢視資料

article/admin.py
from django.contrib import admin
from article.models import Article, Comment


admin.site.register(Article)
admin.site.register(Comment)

▸ 在 admin 頁面 (localhost:8000/admin/) 即可看到資料 Model 資料:

管理者登入
✶ Django 會在各個 Model 的名稱後面加上 s 成為複數

(4) Django ORM

∗ ORM (Object-relational mapping,物件關聯對應)

▸ ORM 是一個工具,讓使用者能以物件導向的方式操作資料庫,不再使用 SQL 語言

▸ 常用的資料庫操作:新增、讀取、修改、刪除、及查詢 (「增讀改刪查」:Create, read, update, delete, search, CRUDS),以 Article 為例

✶ 新增 (Create)
直接新增並儲存物件:
Article.objects.create(...)
Article.objects.get_or_create(...)
先產生物件,設定各欄位值,再儲存:
article = Article()
article.title = ...
article.content = ...
article.save()
✶ 讀取 (Read)
Article.objects.get(...)      # 取出一筆符合條件的資料
✶ 修改 (Update)
取出物件,修改欄位值,再儲存:
article = Article.objects.get(...)
article.title = ...
article.content = ...
article.save()
✶ 刪除 (Delete)
取出物件,刪除:
article = Article.objects.get(...)
article.delete()
✶ 查詢 (Search)
Article.objects.all()      # 取出所有資料
Article.objects.get(...)      # 取出一筆符合條件的資料
Article.objects.filter(...)      # 取出多筆符合條件的資料
Article.objects.exclude(...)      # 取出多筆不符合條件的資料
Article.objects.order_by(...)      # 取出所有資料並排序
Article.objects.filter(...).order_by(...)      # 取出多筆符合條件的資料並排序

(5) 建立資料填充程式

∗ 資料填充程式 (Data population script)

▸ 程式開發期間或系統第一次部署時,將資料一筆一筆輸入資料庫太耗時,通常會撰寫自動填資料的程式, 此種程式稱為資料填充程式 (Data population script)

▸ 所需填的資料:

✶ 測試資料:程式開發或測試階段所需要的測試資料
✶ 基本資料:程式第一次部署時必須要填入的資料 (例如:admin 帳號、產品基本資料、或系統設定資料)

▸ 規劃:將所有填充程式集中放在專案目錄下的 populate 目錄裡

∗ 建立資料填充程式

▸ 在專案目錄下新增 populate目錄: populate/__init__.py

▸ 新增 populate/__init__.py 空白檔案:將 populate 目錄設定為 Python package

▸ 新增 populate/base.py:此為各個填充程式所需之設定檔,由於填充程式是直接執行, 而非透過伺服器,故需先設定 Django 環境

base.py
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog.settings')
import django
django.setup()

▸ 新增部落格文章填充程式

populate/article.py
from populate import base
from article.models import Article, Comment


titles = ['如何像電腦科學家一樣思考', '10分鐘內學好Python', '簡單學習Django']
comments = ['文章真棒', '並不認同您的觀點', '借分享', '學好一個程式語言或框架並不容易']


def populate():
    print('Populating Article and Comment ... ', end='')
    Article.objects.all().delete()
    Comment.objects.all().delete()
    for title in titles:
        article = Article()
        article.title = title
        for j in range(20):
            article.content += title + '\n'
        article.save()
        for comment in comments:
            Comment.objects.create(article=article, content=comment)
    print('done')


if __name__ == '__main__':
    populate()
✶ 首先匯入所需資源,注意,from populate import base 一定要放在第一行, 需先設定 Django 後,其餘程式才能正常運作
titles = ... , comments = ... 預設文章標題及留言內容
def populate():新增資料的函式
# Article.objects.all().delete(), Comment.objects.all().delete():刪除所有文章及留言資料
# for title in titles:使用 titles 串列裡的資料當作標題
# article = Article():新增一筆 Article 實體 (Instance)
# article.title = title:設定文章標題
# for j in range(20):
article.content = ...
--> 設定留言內容為 20 行文字 (亦使用標題當內容)
# article.save():存入資料庫
# Comment.objects.create(article=article, content=comment): 新增並儲存此文章所屬留言,每篇文章儲存 4 筆留言
if __name__ == '__main__':
populate()
-->如果此模組是直接執行而不是被匯入,就呼叫 populate() 函式來新增資料
# Python 模組的執行與匯入:
- 模組被直接執行時,Python 會將內建變數 __name__ 內容設定為 __main__
- 模組被匯入時,Python 會將__name__ 設定為該模組名稱,例如:
  import math
  print(math.__name__)
  --> math
- 因此判斷 __name__ 變數,就可知道該模組是直接執行或是被匯入, 這是 Python 程式常用的模式

▸ 執行填充程式:啟動虛擬環境,移到專案目錄,然後執行模組

(blogVenv)$ cd <project>
$ python -m populate.article
Populating Article and Comment ... done

▸ 在管理者頁面檢視 ArticleComment 資料表:

管理者頁面檢視文章     管理者頁面檢視留言
✶ 以上兩個頁面所顯示的標題就是我們在這兩個 Model 中 __str__(self) 函式所回覆的值

∗ 也可以利用填充程式建立 admin 帳號

populate/admin.py

from populate import base
from django.contrib.auth.models import User


def populate(): 
    print('Creating admin account ... ', end='')
    User.objects.all().delete()
    User.objects.create_superuser(username='admin', password='admin', email=None)
    print('done')


if __name__ == '__main__':
    populate()

▸ 首先匯入相關模組:User 是 Django 的內建模型,用來儲存使用者資料

def populate():新增 admin 帳號的函式

User.objects.all().delete():清除所有 User model裡的資料
User.objects.create_superuser:利用 create_superuser() 函式新增 admin 帳號

▸ 執行填充程式:

$ python -m populate.admin
Creating admin account ... done

∗ 整合所有填充程式:一次執行完所有填充程式

▸ 新增整合填充程式

populate/test.py
from populate import admin, article


admin.populate()
article.populate()
✶ 匯入兩個資料填充模組
✶ 執行 adminarticle 模組內的 populate() 函式
✶ 由於 test.py 模組一定是直接執行,不會被匯入,因此不需要判斷是被執行或匯入 (if __name__ == '__main__')

▸ 執行整合填充程式:一次將所有填充程式執行完畢

(blogVenv)$ python -m populate.test
Creating admin account...done
Populating Article and Comment...done

▸ 再到管理者頁面檢視 ArticleComment 模型,確認內容正確

(6) 客製化管理者頁面

∗ 管理者頁面裡資料的顯示模式可以做許多客製化,例如:

▸ 客製化 Comment model:在 article/admin.py 加入以下內容來設定顯示的欄位 (list_display)

article/admin.py
from django.contrib import admin
from article.models import Article, Comment


class CommentModelAdmin(admin.ModelAdmin):
    list_display = ['article', 'content']
 
    class Meta:
        model = Comment


admin.site.register(Article)
admin.site.register(Comment, CommentModelAdmin)
✶ 增加一個 CommentModeAdmin 類別 (繼承 admin.ModelAdmin)
✶ 客製化頁面顯示清單 (list_diaplay):顯示 articlecontent 欄位
class Meta 是一個類別容器 (Class container),內含有關該類別的詮釋資料 (稱為 Metadata),例如:排序、權限、所使用的 Model 等
✶ 在 admin.site.register(Comment) 新增參數 CommentModeAdmin
--> 測試:清單顯示 2 個欄位 (ARTICLE, CONTENT)
2欄Comment

▸ 設定資料連結欄位 (list_diaplay_links):透過 article 來連結 (此項為預設)

class CommentModelAdmin(admin.ModelAdmin):
    list_display = ['article', 'content']
    list_display_links = ['article']

▸ 設定過濾器 (list_filter):設定右方過濾欄位為 articlecontent,點選即可濾出該項目的相關資料

class CommentModelAdmin(admin.ModelAdmin):
    ...
    list_display_links = ['article']
    list_filter = ['article', 'content']
--> 測試:點選文章標題就會列出所屬留言
過濾器

▸ 設定搜尋欄位 (search_fields):設定搜尋欄位為 content, 輸入資料即可搜尋該欄位的內容

class CommentModelAdmin(admin.ModelAdmin):
    ...    
    list_filter = ['article', 'content']
    search_fields = ['content']
--> 測試:出現搜尋欄位
搜尋器

▸ 設定編輯欄位(list_editable):設定 content 欄位可編輯

...

class CommentModelAdmin(admin.ModelAdmin):
    ...
    search_fields = ['content']
    list_editable = ['content']
--> 測試:可直接編輯 content 欄位內容
編輯器

▸ 其他客製化請參考這裡

(7) 增加 Model 欄位

∗ 增加 Article model 欄位

▸ 管理者發表文章時,應記錄發表時間,因此應增加 pubDateTime 欄位,此外, 將設定文章依照發表時間順序反向顯示:

class Article(models.Model):
    title = models.CharField(max_length=128, unique=True)
    content = models.TextField()
    pubDateTime = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

    class Meta:
        ordering = ['-pubDateTime']
✶ 新增 pubDateTime 欄位 (發表日期時間,Publication date time)
# DateTimeField:日期時間格式
# auto_now_add=True:在物件新增時自動設定為當時時間,設定之後即無法修改
class Meta:在詮釋資料中設定模型的特性
# ordering = ['-pubDateTime']:物件存入資料庫時依照時間反向排序 (負值表示反向,亦即近期發表的文章放在最上面),此處使用 Python list 資料結構來指定欄位, 並可設定多個欄位排序
運算的地點

以上我們將資料排序的運算放在 Model 中,也就是在存入資料庫時,就依照時間順序排列, 這樣做會增加資料儲存的時間,但以後在 views 程式中取出資料時,就不需再排序。如果不如此做, 就必須在 views 中執行排序,例如:

articles = Article.objects.order_by('-pubDateTime')

但到底在 Model 中排序,還是在 views 中排序較好?請參考一個重要的 Web 系統設計理念:

Fat models, thin views, and stupid templates.

∗ 增加 Comment model 欄位

▸ 使用者留言時,也應記錄留言時間,因此也加上 pubDateTime 欄位, 按照時間排序

class Comment(models.Model):
    article = models.ForeignKey(Article)
    content = models.CharField(max_length=128)
    pubDateTime = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.article.title + '-' + str(self.id)

    class Meta:
        ordering = ['pubDateTime']

∗ 執行資料庫遷移:

▸ 執行 Makemigrations:在 article/migrations/ 目錄下產生 0002*.py 檔案

You are trying to add the field 'pubDateTime' with 'auto_now_add=True' to article without a default; the database needs something to populate existing rows.
 1) Provide a one-off default now (will be set on all existing rows)
 2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
You can accept the default 'timezone.now' by pressing 'Enter' or you can provide another value.
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
[default: timezone.now] >>> <Enter>
<Enter> 表示接受輸入建議:timezone.now (此程序會出現 2 次,因為有 2 個欄位需給初始值)
Migrations for 'article':
  article/migrations/0002_auto_....py:
    - Change Meta options on article
    - Change Meta options on comment
    - Add field pubDateTime to article
    - Add field pubDateTime to comment
Finished "/home/<username>/webapps/git/blog/blog/manage.py makemigrations" execution.

▸ Django 在 article/migrations 目錄裡產生 0002_auto_....py 檔案,內容如下:

# -*- coding: utf-8 -*-
# Generated by Django x.x.x on ...
from __future__ import unicode_literals

from django.db import migrations, models
import django.utils.timezone

class Migration(migrations.Migration):

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

    operations = [
        migrations.AlterModelOptions(
            name='article',
            options={'ordering': ('-pubDateTime',)},
        ),
        migrations.AlterModelOptions(
            name='comment',
            options={'ordering': ['pubDateTime']},
        ),
        migrations.AddField(
            model_name='article',
            name='pubDateTime',
            field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='comment',
            name='pubDateTime',
            field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
            preserve_default=False,
        ),
    ]
dependencies:此程式依賴 ('article', '0001_initial') 版本
migrations.AlterModelOptions:更改選項
migrations.AddField:增加欄位

▸ 執行 Migrate:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, article, sessions
Running migrations:
  Applying article.0002_auto_... OK
Finished "/home/<username>/webapps/git/blog/blog/manage.py migrate" execution.

▸ 重新執行文章填充程式 ((blogVenv)$ python -m populate.article),並至 admin 頁面確認資料正確

(8) 重建資料庫

▸ 系統開發期間,常因 Model 欄位頻繁的修改,造成資料庫錯亂而無法完成資料庫遷移程序,此時需要完全刪除資料庫, 然後重建,步驟如下:

1. 停止伺服器:按下 Eclipse 下方的 Eclipse stop server 按鈕
2. 刪除再重建資料庫
$ sudo -i -u postgres
[sudo] password for <username>:
postgres@<username>:~$ dropdb blogDB
postgres@<username>:~$ createdb blogDB
postgres@<username>:~$ psql
postgres=# grant all privileges on database "blogDB" to "blog";
GRANT
postgres=# \q
postgres=# exit
3. 刪除所有 <app>/migrations/00*.py 檔案 (也可以不執行此步驟,直接使用原先的 Migration files)
$ find . -type f -name 00*.py -exec rm {} \;
# 也可利用 Nautilus 的檔案搜尋功能:先移到專案根目錄,然後在搜尋欄裡輸入 000, Nautilus 就會列出所有以 000 開頭的檔案,最後再全選刪除
執行blog
4. 資料庫遷移 1:makemigrations (若未執行步驟 3,則不需執行此指令)
5. 資料庫遷移 2:migrate
6. 填充資料:(blogVenv)$ python -m populate.test
7. 重啟伺服器:工具列執行按鈕 --> 1 blog blog
執行blog

(9) 小結

∗ 遷移檔案不分享

▸ 回顧第 3 章之「專案的組成要件」:在多個開發者之間唯一分享的資料是專案,資料庫則是開發者個別在自己的本機端建立, 彼此並不分享,因此,資料庫遷移檔也不需要分享

▸ 在 .gitignore 裡加入以下內容,以排除 00*.py 遷移檔:

*~
__pycache__
*.pyc
00*.py

▸ 但 EGit 在 Commit 時,有可能還是會挑到 .gitignore 裡列舉的檔名型態,需人工勾選排除

∗ 上推專案到 Github

▸ Right click project --> Team --> Commit --> Commit message: Chapter 7 finished --> Commit and Push

∗ 練習

1. 刪除 blogDB 資料庫,重建資料庫,並重新填入資料

2. 在 bookstore 專案中

✶ 新增 book app
✶ 在 book app 中新增 Book model, 包含以下欄位:書名、作者姓名、出版商、出版日期、印刷版次、及售價
✶ 建立 populate 套件目錄並撰寫填充程式 populate/book.py, 建立至少 10 本書之資料

上一章       下一章