iT邦幫忙

1

Django 學習紀錄 8.模型與資料庫

之前有在用Django寫一些小網站,現在暑假想說再來複習一下之前買的這本書
https://ithelp.ithome.com.tw/upload/images/20190724/20118889bj9fH1vhuR.jpg
於是我就把它寫成一系列的文章,也方便查語法
而且因為這本書大概是2014年出的,如今Django也已經出到2.多版
有些內容也變得不再支援或適用,而且語法或許也改變了
所以我會以最新版的Python和Django來修正這本書的內容跟程式碼

目錄:django系列文章-Django學習紀錄

8. 模型與資料庫

8.1 建置應用

執行指令

python manage.py startapp restaurants

8.1.1 安裝應用

mysite/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'restaurants', # <-加入app
]

8.1.2 移動模板與視圖

在restaurants底下新增目錄templates
並將menu.html移到這裡
我們不需要把新的模板目錄添加到settings.py裡
因為django預設應用目錄下的templates目錄為可用的模板路徑
接著把原本的views.py移到restaurants
restaurants/views.py

from django.shortcuts import render_to_response

def menu(request):
    food1 = {'name': '番茄炒蛋', 'price': 60, 'comment': '好吃', 'is_spicy': False}
    food2 = {'name': '蒜泥白肉', 'price': 100, 'comment': '人氣推薦', 'is_spicy': True}
    foods = [food1, food2]
    return render_to_response('menu.html', locals())

8.1.3 重新設定urls.py

mysite/urls.py

from django.contrib import admin
from django.urls import path
from restaurants.views import menu # <-修改這裡

urlpatterns = [
    path('admin/', admin.site.urls),
    path('menu/', menu),
]

8.2 Django模型

django預設使用SQLite資料庫

8.2.1 建立模型

Python的模型是一種ORM(object-relational mapping,物件關聯映射)的機制
我們只需要操作python的類別與物件就能建立資料表和存取資料
而不需要親自去寫SQL資料庫語言
建立模型檔
restaurants/models.py

from django.db import models

class Restaurant(models.Model):
    name = models.CharField(max_length=20)
    phone_number = models.CharField(max_length=15)
    address = models.CharField(max_length=50, blank=True)


class Food(models.Model):
    name = models.CharField(max_length=20)
    price=models.DecimalField(max_digits=3,decimal_places=0)
    comment = models.CharField(max_length=50, blank=True)
    is_spicy = models.BooleanField(default=False)
    restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)

所有的模型繼承自models.Model這個類別
操作模型時,會將一個模型類別對應到資料庫中的一張資料表
實體化自此類別的物件就是資料表中的一筆一筆資料
上面的name、phone_number、address變數會成為資料表中的欄位
而這三個變數都是models中的欄位物件(field object)
CharField是文字資料型態的欄位物件
max_length參數可以設定文字最大長度
blank參數可以允許欄位為空
DecimalField為浮點數的資料
max_digits參數設定最大位數
decimal_places參數設定小數點位數
BooleanField為布林真假值資料
而這一行

restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)

包含了重要的資料表關聯性的概念
一家餐廳有多種食物
或是一本書有許多編者或作者
這屬於一種Many-to-one的關係
這時候就要用ForeignKey(外鍵)來描述

8.2.2 檢查資料

在建置資料表前,先檢查看看之前設計的模型有沒有問題

python manage.py check

如果沒錯的話會顯示

System check identified no issues (0 silenced).

8.2.3 建立migration資料檔

在建立資料表前,必須先建立migration資料檔,以確保資料表的架構和版本有被記錄下來

python manage.py makemigrations restaurants

這會對指定的應用做migration的檢查,如果模型有異動,會產生新的migration檔
放置在app目錄底下的migration資料夾
如果makemigrations後面不指定app名稱
則django會對所有安裝好的app做migration的檢查
當模型有更動時,再次makemigrations,會生成新的migration檔
從檔名以及裡頭的dependencies和operations串列
可以知道之前做了哪些變動
而dependencies描述了migration檔案的相依性,也就是這次的migration是基於哪一個migration在更動的
使用下列指令可以知道模型是如何被翻譯成SQL語言的

python manage.py sqlmigrate restaurants(應用名稱) 0001(版本編號)

8.2.4 模型與資料庫之同步 - 利用migrate

接下來將模型同步到資料庫,真正產生資料表

python manage.py migrate restaurants 0001

migrate會根據指定的migration記錄(利用編號指定),將模型同步到資料庫,只要使用了migrate指令,就可以根據模型異動,將現行的資料庫調整到與記錄檔一樣
若不指定編號,則自動更新到最新版本
另外,與makemigrations相同,如果不指定應用名稱,預設將會對所有的app進行同步
使用migrate的好處是我們可以任意地更動資料庫結構並作成各種版本的記錄,且隨時可以指定任一版本進行同步

8.2.6 資料操作

進入django shell

python manage.py shell

建立一個模型物件

>>> from restaurants.models import Restaurant, Food
>>> r1 = Restaurant(name='派森家常小館', phone_number='02-12345678', address='天龍國天龍區
天龍路1號')
>>> r1
<Restaurant: Restaurant object (None)>

生成一筆資料了,不過還尚未被寫入資料庫中

將模型物件(資料)寫入資料庫

>>> r1.save()

若想要一次完成模型資料的建立與寫入資料庫

>>> r2 = Restaurant.objects.create(name='古意得餐廳', phone_number='02-7654321', address='天龍國天龍區天龍路100號')
>>> r2
<Restaurant: Restaurant object (2)>

r1在r2在shell中顯示出來的資訊都一樣
我們可以更改顯示出來的資訊,讓使用者好分辨
修改models.py

from django.db import models

class Restaurant(models.Model):
    name = models.CharField(max_length=20)
    phone_number = models.CharField(max_length=15)
    address = models.CharField(max_length=50, blank=True)

    def __str__(self):
        return self.name

class Food(models.Model):
    name = models.CharField(max_length=20)
    price=models.DecimalField(max_digits=3,decimal_places=0)
    comment = models.CharField(max_length=50, blank=True)
    is_spicy = models.BooleanField(default=False)
    restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)

    def __str__(self):
        return self.name

取出資料表中的資料

接著先離開shell(按Ctrl+Z)
這是為了讓我們剛剛修改過的模型代碼可以被載入直譯器
再進入shell一次

>>> from restaurants.models import Restaurant, Food
>>> restaurants = Restaurant.objects.all()
>>> restaurants
<QuerySet [<Restaurant: 派森家常小館>, <Restaurant: 古意得餐廳>]>

我們會發現因為之前__str__函式的緣故
顯示出來的資訊改變了

資料欄位的顯示

只需要利用python的方式思考就行

>>> r1 = restaurants[0]
>>> r1.phone_number
'02-12345678'
>>> r1.id
1

資料查詢與查詢集(QuerySet)

取出一筆資料:

>>> r = Restaurant.objects.get(name='古意得餐廳')
>>> r
<Restaurant: 古意得餐廳>

objects.get方法會回傳一個模型物件,也就是一筆資料,如果回傳的是多筆或是查詢結果失敗(空),都會引發例外,這可以利用try/expect來捕獲並處理

全查詢

>>> restaurants = Restaurant.objects.all()
>>> restaurants
<QuerySet [<Restaurant: 派森家常小館>, <Restaurant: 古意得餐廳>]>

objects稱之為管理器,每個模型都有一個管理器,他包含了關於查詢該模型資料的種種方法
而回傳出來的這個資料清單(嚴格來說不是我們所熟知的那個清單),被稱為QuerySet:查詢集,他是一個類似於清單的物件,把它想像成是儲存著藉由管理器查詢回來的資料集合
對於查詢集我們可以使用類似清單的方式來取值或做slicing(切片):

>>> restaurants[0]
<Restaurant: 派森家常小館>
>>> restaurants[0:2]
<QuerySet [<Restaurant: 派森家常小館>, <Restaurant: 古意得餐廳>]>
>>> restaurants[-1]
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "D:\vs code\python\demo\lib\site-packages\django\db\models\query.py", line 288, in
__getitem__
    "Negative indexing is not supported."
AssertionError: Negative indexing is not supported.

可惜並不支援負向索引

過濾查詢

利用objects管理器的filter方法,可以對資料進行過濾

>>> restaurants = Restaurant.objects.filter(name='古意得餐廳')
>>> restaurants
<QuerySet [<Restaurant: 古意得餐廳>]>

多重過濾

>>> restaurants = Restaurant.objects.filter(name='古意得餐廳', phone_number='02-7654321')
>>> restaurants
<QuerySet [<Restaurant: 古意得餐廳>]>

包含過濾

>>> restaurants = Restaurant.objects.filter(name__contains='餐廳')
>>> restaurants
<QuerySet [<Restaurant: 古意得餐廳>]>

利用屬性名+__contains=搜尋值可以搜尋該屬性包含值的資料

排序查詢

先產生幾筆資料

>>> r = Restaurant.objects.get(name='古意得餐廳')
>>> f1 = Food(name='宮保雞丁', price=120, comment='超級辣', is_spicy=True, restaurant = r)
>>> f1.save()
>>> f2 = Food(name='炒青菜', price=85, comment='每日不同', is_spicy=False, restaurant = r)
>>> f2.save()
>>> Food.objects.all()
<QuerySet [<Food: 宮保雞丁>, <Food: 炒青菜>]>

查詢出來的QuerySet預設是按照id進行排序的
利用order_by方法可以自行指定依照哪個資料進行排序

>>> Food.objects.all()
<QuerySet [<Food: 宮保雞丁>, <Food: 炒青菜>]>
>>> Food.objects.order_by('price')
<QuerySet [<Food: 炒青菜>, <Food: 宮保雞丁>]>

注意參數要放的是字串喔!

>>> Food.objects.order_by('price','name') # 先排price再排name
<QuerySet [<Food: 炒青菜>, <Food: 宮保雞丁>]>
>>> Food.objects.order_by('name','price') # 先排name再排price
<QuerySet [<Food: 宮保雞丁>, <Food: 炒青菜>]>
>>> Food.objects.order_by('-price') # 反向排序
<QuerySet [<Food: 宮保雞丁>, <Food: 炒青菜>]>

如果決定之後想讓資料都照某項特性排列,但卻不想每次這麼麻煩地打這些
在Food類別中加入Meta

class Food(models.Model):
    name = models.CharField(max_length=20)
    price=models.DecimalField(max_digits=3,decimal_places=0)
    comment = models.CharField(max_length=50, blank=True)
    is_spicy = models.BooleanField(default=False)
    restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)

    def __str__(self):
        return self.name
    
    class Meta:
        ordering=['price']

記得更動Meta時也需做migration

查詢集的操作與連鎖查詢

>>> foods = Food.objects.order_by('price')
>>> foods
<QuerySet [<Food: 炒青菜>, <Food: 宮保雞丁>]>
>>> foods = foods.filter(is_spicy=True)
>>> foods
<QuerySet [<Food: 宮保雞丁>]>
>>> food = foods.get(name__contains='宮保')
>>> foods
<QuerySet [<Food: 宮保雞丁>]>

上述操作可做一個串連:

>>> Food.objects.order_by('price').filter(is_spicy=True).get(name__contains='宮保')
<Food: 宮保雞丁>

外鍵與跨模型查詢

找出食物對應的餐廳

>>> food = Food.objects.get(name='宮保雞丁')
>>> food.restaurant
<Restaurant: 古意得餐廳>

查出一家餐廳中擁有的食物

>>> r = Restaurant.objects.get(name='古意得餐廳')
>>> r.food_set
<django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager.<locals>.RelatedManager object at 0x03E520D0>
>>> r.food_set.all()
<QuerySet [<Food: 炒青菜>, <Food: 宮保雞丁>]>

利用小寫模型名稱_set
就可得到一個關係管理器,類似於objects,我們可以對他使用各種查詢的方法
接下來介紹另外一種關係

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=30)
    
    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)

一本書有多個作者,一個作者也有多本著作
這時候是多對多關係

>>> book = Book.objects.get(id=1)
>>> book.authors.all()
<QuerySet [<Author: 金庸>, <Author: 古龍>]>
>>> author = Author.objects.get(id=1)
>>> a.book_set.all()
[<Book: 便秘的魔法石>, <Book: 考盃的考驗>]

更新與刪除資料庫數據

如果有一筆資料需要修改

>>> food = Food.objects.get(name='宮保雞丁')
>>> food.price = 200
>>> food.save()

不過save()方法會將一筆資料的所有欄位重新輸入
為了避免一些錯誤發生
使用查詢集的方法update

>>> Food.objects.filter(name='宮保雞丁').update(price=200)
1

注意update是QuerySet的方法,無法作用在單獨物件上,所以我們要偷偷用點技巧,使用filter來過濾出只含有宮保雞丁的QuerySet
另外,此方法會回傳一個整數代表有幾筆資料被更新了
因為它是查詢集的方法,所以update是可以更動所有在查詢集中的物件的,比如說今天古意得餐廳改變營業模式成為熱炒100了,那麼我們可以這麼做

>>> Restaurant.objects.get(name='古意得餐廳').food_set.update(price=100)
2

刪除資料的方法

>>> f = Food.objects.get(name='宮保雞丁')
>>> f.delete()
(1, {'restaurants.Food': 1})
>>> Food.objects.all()
<QuerySet [<Food: 炒青菜>]>
>>> Food.objects.all().delete()
(1, {'restaurants.Food': 1})
>>> Food.objects.all()
<QuerySet []>

不論是查詢集還是單獨的模型物件都是適用的

8.3 完成更強大的餐單顯示app

前面的資料操作試完後
記得把資料改回去
接著
views.py

from django.shortcuts import render_to_response
from restaurants.models import Restaurant, Food

def menu(request):
    restaurants = Restaurant.objects.all()
    return render_to_response('menu.html', locals())

改完後會發現出現這個錯誤
https://ithelp.ithome.com.tw/upload/images/20190717/20118889fnf3dfdaYf.png
原因是因為如果只安裝pylint,vs code會不知道那是你定義的ORM
所以要安裝 pylint_django 修正他

pip install pylint_django

裝好後,在settings.json中加入

"python.linting.pylintArgs": [
        "--load-plugins=pylint_django",
    ]

錯誤訊息就會消失了
menu.html

<!doctype html>
<html>
    <head>
        <title> Menu </title>
        <meta charset='UTF-8'>
        <style>
            table, th, td{
                border: 1px solid black;
            }
        </style>
    </head>
    <body>
        {% for r in restaurants %}
            <h2>{{ r.name }}</h2>
            {% if r.food_set.all %}
                <p>本餐廳共有{{ r.food_set.all|length }}道菜</p>
                <table>
                    <tr>
                        <th>菜名</th>
                        <th>價格</th>
                        <th>註解</th>
                        <th>辣不辣</th>
                    </tr>
                {% for food in r.food_set.all %}
                    <tr>
                        <td> {{ food.name }} </td>
                        <td> {{ food.price }} </td>
                        <td> {{ food.comment }} </td>
                        <td> {% if food.is_spicy %} 辣 {% else %} 不辣 {% endif %} </td>
                    </tr>
                {% endfor %}
                </table>
            {% else %}
                <p>本餐廳啥都沒賣</p>
            {% endif %}
        {% endfor %}
    </body>
</html

結果:
https://ithelp.ithome.com.tw/upload/images/20190717/20118889eCh3R8hUB1.png

上一篇:Django學習紀錄 7.模板的變量與標籤[附常用的過濾器整理]

下一篇:Django學習紀錄 9.後台管理系統Admin


尚未有邦友留言

立即登入留言