Python Django 概念與實作 - Django Model(二)

前言

目前小型以上的網站均會使用到資料庫,這節來介紹Django如何透過ORM的方式存取資料庫。

目前 Python Django 概念與實作 大致規劃為

  1. 佈置環境
  2. Django View(一)
  3. Django Model(二)
  4. Django Test(三)
  5. Django Forms(四)
  6. Django Admin(五)
  7. 登入功能(六)
  8. Blog功能(七)

Object Relational Mapping, ORM

一般來說,要存取資料庫通常會使用Structured Query Language, SQL來存取資料,類似SELECT * FROM order;這樣的語法。直接下SQL也不是不行,但大家仍會使用ORM作為操作資料庫的方式,雖然會消耗一些效能,但因有容易維護、跨不同資料庫、免於SQL Injection Attacks等優點,整體來說優點仍大於缺點。
ORM簡單來說是將資料庫不同的table定義成不同model,透過python操控已定義model來控制databse

SQLite

要儲存資料庫必須要有一個database server, DB server(資料庫伺服器)來儲存data,常見的RDBMS(關聯式資料管理系統)有MySQL, PostgreSQL等,亦有非關聯式,但不在我們今天討論的範圍當中。
Django內建輕量級別的database(資料庫): SQLite,讓我們在學習、開發時免於架設額外DB server。settings.py中可設定資料庫連線與儲存位置,這裡我們用default就好。

1
2
3
4
5
6
7
8
9
# settings.py
# 略...

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3', # 資料儲存位置
}
}

Model

首先我們先來定義我們的model(模型),以商品來說,最少會有以下三個欄位

  • 商品名稱
  • 商品敘述
  • 價錢
1
2
3
4
5
6
7
8
9
10
# orders/models.py
from django.db import models

class Product(models.Model):
name = models.CharField(max_length=20) # 長度上限為20
description = models.TextField(max_length=100) # 長度上限為100
price = models.DecimalField(max_digits=6, decimal_places=2, default=0)
# 總位數為6, 小數位為2, 預設為0
def __str__(self):
return self.name

每一個欄位也對應不同的屬性,可以依照實際狀況使用models.Model下的不同Field。
例如:

  • 商品是否顯示: BooleanField
  • 商品數量: IntegerField
  • 異動日期: DateField
  • 異動時間: TimeField

Tips
Decimal這種資料型態是用作固定精準度,若使用Float有可能導致0.1+0.2=0.3…1的狀況發生。
DecimalField中,max_digits(最大總位數)與decimal_places(小數位數)為必填的參數
e.g. price = 9999.99 則設定max_digits=6, decimal_places=2

Migration

模型完成後,要將DB對應的欄位設定轉成與模型一致,這一步驟我們稱作資料遷移
而我們不必逐一調整DB資料型態,django可先將模型轉為資料遷移腳本,接著執行腳本就可成功調整資料型態。

Tips
Migration雖然稱作資料庫遷移,但並不是將A資料移至B位置,而是讓DB server知道每一個欄位的性質與大小。e.g. name(名字)這個欄位是儲存字串且通常不會太大,而item_num(物品數量)則是正整數,設為Integer較恰當。

1
2
3
4
python manger.py makemigrations order # 生成遷移腳本
# Migrations for 'orders':
# orders/migrations/0001_initial.py
# + Create model Product

此時可以查看app下會產生遷移腳本

1
2
3
4
5
6
7
8
9
orders/
├── __init__.py
├── admin.py
├── apps.py
├── models.py
├── views.py
└── migrations
├── __init__.py
└── 0001_initial.py # 資料遷移腳本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 0001_initial.py
class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Product',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20)),
('description', models.TextField(max_length=100)),
('price', models.DecimalField(decimal_places=2, default=0, max_digits=6)),
],
),
]

執行migrate即可執行腳本

1
2
3
4
5
python manage.py migrate
# Operations to perform:
# Apply all migrations: account, admin, auth, contenttypes, news, orders, portfolio, sessions, sites, socialaccount
# Running migrations:
# Applying orders.0001_initial... OK

Notices
‼️ 只要資料型態有異動(無論是長度修改、新增/刪除欄位),均須重新產生新的腳本,並執行資料庫遷移。否則會造成資料庫存取失敗的問題產生

QuerySet

模型、資料庫都ok後,可以正式使用ORM操作DB🎉
使用django內建互動式shell,直接操作model來控制DB
執行以下程式開啟互動式shell(ctrl+D可中斷)

1
python manger.py shell

透過控制model來CRUD(新增、讀取、修改、刪除)資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Cmd click to launch VS Code Native REPL
# Python 3.10.0 (v3.10.0:b494f5935c, Oct 4 2021, 14:59:19) [Clang 12.0.5 (clang-1205.0.22.11)] on darwin
# Type "help", "copyright", "credits" or "license" for more information.
# (InteractiveConsole)

# 引入Product model
from order.models import Product

# 查詢Prdouct內的data(未建立data時是空的)
Product.objects.all() # <QuerySet []>

# 新增Product
p1 = Product(name="cake", description="This is a cake.", price = 50.0)

p1 # <Product: cake>
p1.name # 'cake'
p1.description # 'This is a cake.'
p1.price # 50.0

# 未儲存前仍是空的
Product.objects.all() # <QuerySet []>

# 儲存後查詢
p1.save()
Product.objects.all() # <QuerySet [<Product: cake>]>

# 修改後仍須儲存
p1.price = 75.50
p1.save()

# 刪除時無需儲存
p1.delete() # (1, {'orders.Product': 1})
Product.objects.all() # <QuerySet []>

還有各種查詢方式

  • .all: 列出全部
  • .filter, .exclude: 包含篩選, 除外篩選
    • 字串類
      • exact: 精準比較
      • iexact: 鬆散比較(忽略大小寫)
      • contains: 包含字元
      • icontains: 包含字元(忽略大小寫)
      • startswith: 起始字元
      • istartswith: 起始字元(忽略大小寫)
      • endswith: 結尾字元
      • iendswith: 結尾字元(忽略大小寫)
    • 邏輯類
      • gt: 大於
      • gte: 大於等於
      • lt: 小於
      • lte: 小於等於
      • in: 在List中
      • range: 在範圍內
      • isnull: 為null
  • .order_by: 排序
  • .distinct: 排除重複值
  • .exists: 是否存在, 返回True/False
  • .get: 取QuerySet內的物件, 返回object/DoesNotExcist
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    p1 = Product(name="cake", description="This is a cake.", price = 50.0)
    p1.save()

    p2 = Product(name="apple", description="This is an apple.", price = 5.0)
    p2.save()

    p3 = Product(name="apple i17", description="This is a phone.", price = 999.99)
    p3.save()

    Product.objects.all() # <QuerySet [<Product: cake>, <Product: apple>, <Product: apple i17>]>

    ### filter(包含) & exclude(排除) ###
    # 精準比較
    Product.objects.filter(name='cake') # <QuerySet [<Product: cake>]>
    Product.objects.filter(name__exact='cake') # <QuerySet [<Product: cake>]>
    # 包含字元
    Product.objects.filter(name__contains='apple') # <QuerySet [<Product: apple>], <Product: apple i17>]>
    # 起始字元
    Product.objects.filter(name__startswith='a') # <QuerySet [<Product: apple>], <Product: apple i17>]>
    # 多重篩選
    Product.objects.filter(name__contains='apple', price__gte=3) # <QuerySet [<Product: apple>], <Product: apple i17>]>
    # 排除
    Product.objects.exclude(name='cake') # <QuerySet [<Product: apple>], <Product: apple i17>]>
    # 排序(降序)
    Product.objects.order_by('-price') # <QuerySet [<Product: apple i17>, <Product: cake>, <Product: apple>]>
    # OR
    from django.db.models import Q
    Product.objects.filter(Q(name='cake')|Q(name='apple')) # <QuerySet [<Product: cake>, <Product: apple>]>


    # 限制篩選範圍
    Product.objects.all()[0:2] # <QuerySet [<Product: cake>, <Product: apple>]>
    # 取QuerySet內物件
    Product.objects.all()[0] # <Product: cake>
    Product.objects.all()[3:].get() # 取值失敗時, 拋出"DoesNotExist" Error
    # raise self.model.DoesNotExist(
    # orders.models.DoesNotExist: Product matching query does not exist.

MVT

前一小節有介紹MVT,這裡我們實作/orders/products顯示當前所有商品資訊

  1. 修改views.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # orders/views.py
    from django.shortcuts import render
    from .models import Product

    def product(request):
    products = Product.objects.all()
    # 透過context傳至templates
    context = {
    'products': products
    }
    return render(request, 'orders/product.html', context=context)
  2. 新增templates/product.html,並使用Jinja 2語法使用傳入的變數
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <!-- orders/templates/product.html-->
    <!-- 略... -->
    <div class="row row-cols-1 row-cols-md-3">
    {% for product in products %}
    <div class="col mb-4">
    <div class="card h-100">
    <svg class="bd-placeholder-img card-img-top" width="100%" height="180" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Image cap" preserveAspectRatio="xMidYMid slice" focusable="false"><rect width="100%" height="100%" fill="#6c757d"></rect><text x="40%" y="50%" fill="#dee2e6">Image cap</text></svg>
    <div class="card-body">
    <h5 class="card-title">{{ product.name }}</h5>
    <span>${{ product.price }}</span>
    <p class="card-text">{{ product.description }}</p>
    </div>
    </div>
    </div>
    {% endfor %}
    </div>

    Tips
    關於Jinja 2語法的使用,可參考之前Flask的文章

  3. 修改urls.py
    1
    2
    3
    4
    5
    6
    7
    8
    # orders/urls.py
    from django.urls import path
    from . import views

    app_name = 'orders'
    urlpatterns = [
    path("product", views.product, name="product"),
    ]

結論

  • 定義資料庫model與使用models.Model下不同的Field
  • 資料庫遷移
  • QuertSet的CRUD與不同篩選條件
  • 在Django中實作ORM