Yuanlin Lin

Blog

菜雞也看得懂的 Django + MongoDB 接口設計實作教學(上)

Yuanlin Lin 林沅霖

2020-09-11

這篇文章是《菜雞也看得懂的全端專案開發實作教學》系列中的其中一篇文章。跟隨本系列,就算是毫無開發經驗的菜雞也能獨立開發全端專案!

本系列已停更:這篇文章是 2020 年 9 月 11 日發表於 Medium 的文章,距離這個部落格架設的日子已經經過快 2 年啦!目前已經不打算繼續更新這個系列,因為我現在主要使用 Go 語言開發後端而不是 Python。

在上一篇文章《菜雞也看得懂的 Django REST Framework 串接 MongoDB 教學》我們已經成功的在 Django REST Framework 專案中透過 MongoEngine 串接上了 MongoDB 資料庫,如果還沒的同學可以先看上一篇:

菜雞也看得懂的 Django Rest Framework 串接 MongoDB 教學

在本文,我們將說明如何在這個連接上資料庫的後端專案中,按照 REST 的接口設計原則來寫出幾個有具體功能的接口。如果你還不知道什麼是 REST 原則,可以參考本系列基礎教學的第二篇文章《菜雞也看得懂的 RESTful API 概念教學》。

快速複習 REST 的概念

為了避免有些讀者看完 REST 的概念,但來到這裡已經忘記了,所以我們再快速的複習一次 REST 的原則:

每個 URL 代表一個「資源」,使用不同的 Http Method 代表對資源進行不同的操作。「資源」指的可以是一份資料、一個檔案或是任何由後端管理的東西。

什麼意思?舉個例子,今天我們設定一個接口是 /user/<user_id> ,這個時候, /user/ken20001207 這個 URL 就說指向 ID 為 ken20001207 的這個 user 資源,同樣地, /user/xyza55688 這個 URL 就說指向 ID 為 xyza55688 的這個 user 資源。

因此,我只要對 /user/ken20001207 發送一個 GET Request,就相當於是前端向後端請求「取得」 ID 為 ken20001207 的這個 user 的資料。

我只要對 /user/ken20001207 發送一個 PUT Request,就相當於是前端向後端請求「更新」 ID 為 ken20001207 的這個 user 的資料。

我只要對 /user/ken20001207 發送一個 DELETE Request,就相當於是前端向後端請求「刪除」 ID 為 ken20001207 的這個 user 的資料。

一個 URL 代表一個資源,不同的 Method 代表不同的操作。

為 Uber Eats 設計一個 Order 物件

在我們開始開發前,我們應該針對當前的專案制定一套 Schema,也就是說我的整套系統可能會用到哪些類型的「資源」,這些「資源」在程式的運行中扮演什麼角色。

舉個例子,今天我們想要寫一個 Uber Eats 線上點餐系統,那麼我們就必須設計一個資源叫做「訂單」,透過對「訂單」這個資源的寫入和變更,後端就可以清楚的瞭解現在整個系統內的訂單情形。

首先,「訂單」這個資源應該有哪些屬性?也就是說至少需要多少訊息才能完整的表達一個訂單?照理來講,訂單應該會有訂單編號、訂單建立時間、訂單內容、訂單配送地址和訂單狀態對吧:

Order { id: string, create_time: Date, content: string, address: string, status: string }

因此我們現在定義出了一個 Order (訂單)物件的外貌,他有五個屬性,分別是 string 類型的 id (訂單編號)、Date 類型的 create_time (訂單建立時間)、 string 類型的 cotent (訂單內容)、string 類型的 address(地址)以及 string 類型的 status (訂單狀態)。

例如下方這個物件就是一個我們規定的 Order 物件:

Order { id: "148526341", create_time: "2020-09-11T10:25:36+08:00", content: "大麥克配中薯可樂", address: "桃園市中山路 889 號", status: "製作中" }

注意到這邊的 create_time 使用 ISO8601 標準的方式來表示一個時間,因為如果要在 Request 和 Response 傳遞訊息,我們必須透過純文字的方式,因此在傳遞一個 DateTime 物件的時候最好依照現有的時間表達標準,可參考:

ISO 8601

UNIX Time

舉個反例,下方這兩個物件就不是我們的 Order 物件:

Order { id: "148526341", content: "大麥克配中薯可樂" }

這個物件的屬性和我們設定的 Order 不匹配,因此他沒辦法和我們制定的 Order 物件對上。

Order { id: "148526341", create_time: "2020-09-11T10:25:36+08:00", content: 1234, address: "桃園市中山路889號", status: "配送中" }

這個物件的其中一個屬性使用了錯誤的類型(content 應該是 string,現在是 number),因此他也不是一個我們設計的 Order。

太好了,我們成功的設計完一個物件應該要有什麼格式了,現在我們就把他寫進我們的 Django 專案吧。首先我們在和專案同名的那個資料夾(放 settings.py 的地方)裡面新增一個 models.py:

from datetime import datetime from mongoengine import ( Document, StringField, DateTimeField) class Order(Document): create_time = DateTimeField(default=datetime.now()) content = StringField(required=True) address = StringField(required=True) status = StringField(default="收到訂單")

可以看到我們在 models.py 寫了一個新的 class 叫做 Order。注意,因為他是繼承了 MongoEngine 裡面的 Document 這個 class,所以他其實就代表了我們連上的資料庫裡面的 order 這個 collection。

什麼意思?舉個例子,我們可以直接透過簡單的一行程式去對資料庫裡面的 Order 進行查詢:

the_order = Order.objects.get(id="123456789")

這個時候,the_order 就是資料庫裡面 id 為 123456789 的訂單物件,實在是太方便了,偉哉 MongoEngine !

你是否有注意到,我在 StringField(字串欄位)和 DateTimeField (日期時間欄位)還加了一些選擇性的參數,例如 required 是否必要、 default 預設值 … 透過這些參數,你可以把你的物件在規範的更完善,這樣你的資料管理起來也更方便哦!還有哪些參數可以用呢,請參考:

https://docs.mongoengine.org/guide/defining-documents.html#fields

為我們的專案設計接口

制定好了一個 Order 物件以後,我們可以規劃一個路徑來指向他。最直觀的方法就是當 URL 是 /order/148526341 的時候,這個路徑就指向了 id 為 148526341 的訂單。

因此,如果我們要點餐,我們就可以對 /order/發出一個 POST Requset,記得在 Request Body 帶上我們的訂單資訊:

// POST /order/Request Body: { content: "孫東寶主廚牛排五分熟", address: "桃園市中山路 889 號" }

等等,為什麼只有 content 和 address ? 剛剛不是規定好了 Order 物件應該要有 5 個屬性嗎?

如果你和我一樣提出了這個疑問,想想看,你在麥當勞點餐的時候,會這樣和店員說嗎:

你好我要一份大麥克,訂單編號是 1486432,狀態是製作中,謝謝。

店員應該會覺得你是來亂的吧,誰點餐的時候會和店家指定訂單編號和訂單狀態啊!因此,創建資源的時候通常 Request Body 只會帶有你需要指定的內容,當你成功創建資源之後,後端再把完整的資料透過 Response 回傳回來:

// POST /order/Request Body: { content: "孫東寶主廚牛排五分熟", address: "桃園市中山路 889 號" }
// Response Body: { id: "148526341", create_time: "2020-09-11T10:25:36+08:00", content: "孫東寶主廚牛排五分熟", address: "桃園市中山路 889 號", status: "收到訂單" }

是不是有點像你點晚餐以後,店員開了一張發票或是購物明細給你的感覺?但我們今天這個購物明細更厲害了,因為他是會動的!

比如說今天餐廳開始準備你的餐點了,他們就會對 /order/148526341 發出一個 PUT Request:

// PUT /order/148526341Request Body: { id: "148526341", create_time: "2020-09-11T10:25:36+08:00", content: "孫東寶主廚牛排五分熟", address: "桃園市中山路 889 號", status: "準備中" }
Response Body: { id: "148526341", create_time: "2020-09-11T10:25:36+08:00", content: "孫東寶主廚牛排五分熟", address: "桃園市中山路 889 號", status: "準備中" }

注意到了嗎,餐廳把你的訂單的狀態改成了「準備中」!

所以當你過了10分鐘肚子已經餓了,打開 Uber Eats 看一下的時候,你的 Uber Eats App (前端)就會發送一個 GET Request 到 /order/148526341 查詢你當前的訂單資料:

// GET /order/148526341Response Body: { id: "148526341", create_time: "2020-09-11T10:25:36+08:00", content: "孫東寶主廚牛排五分熟", address: "桃園市中山路 889 號", status: "準備中" }

然後你的 App 就會顯示出大大的 「準備中」三個字讓你知道餐廳已經在幸苦的趕工你的餐點啦!

說了那麼多,其實上面都還在「設計」階段,還沒有真的開工。但是請記得,「設計」階段往往是一個專案最關鍵的步驟,一個好的設計可以讓專案的開發效率飛起來;而一個草率的設計就會讓整個團隊面臨 Debug 地獄以及 API 串接地獄 …

現在我們就要來把剛剛的設計實作出來了,還記得我們在本系列後端教學的第一篇文章已經寫出了兩個沒啥功能的 API 嗎?如果忘記了記得回去看一下哦:

對不起這篇文章還沒寫,如果你正在等待這篇文章,不妨來 Chief Noob 的 Discord 和我說一聲,我會儘快為你趕工出來了!

現在我們仿照一樣的做法在專案目錄中新增一個 order_api 資料夾,並且在裡面新增必要的 __init__.pyurls.pyviews.py

example_project
__init__.py
models.py
settings.py
urls.py
order_api
__init__.py
settings.py
urls.py
views.py
manage.py

然後就進到 order_apiviews.py 開始實作接口吧,首先我們從創立訂單 ( /order/) 下手:

# order_api/views.py from rest_framework.decorators import api_view @api_view(['GET', 'POST', 'DELETE']) def all_order(request): # API 的程式碼

等等,你這個時候應該發現了一個奇怪的事情,我們居然在 @api_view() 裡面一次寫了三個 Method 在同一個 function 上?沒錯!當同一個 URL 可以用不同的 Method 來請求的時候,我們就可以讓一個 function 能夠處理多種請求方式。

但是如果我們用一個 function 來處理三種請求,那麼我們就必須手動從裡面辨識現在是哪個類型的請求:

# order_api/views.py from rest_framework.decorators import api_view @api_view(['GET', 'POST', 'DELETE']) def all_order(request): if request.method == 'GET': # GET 請求要做的事情 if request.method == 'POST': # POST 請求要做的事情 if request.method == 'DELETE': # DELETE 請求要做的事情

接下來我們就針對這三個請求逐一完成,本文先以 GET 請求 (查詢所有 Order)為例。

# 取得所有訂單 if request.method == 'GET': all_order = Order.objects.all() all_order_serializer = OrderSerializer(all_order, many=True) return JsonResponse(all_order_serializer.data, status=status.HTTP_200_OK, safe=False)

all_order 是從我們剛剛的 Order 這個 class 透過 Order.objects.all() 這個方式來得到整個資料庫中所有的 Order。接下來我們將 all_order 放入 OrderSerializer 裡面,因為 all_order 有不只一個 order,所以別忘了加上 many=True。放進去以後我們通過 all_order_serializer.data就可以得到一個能夠放入 JsonResponse 的資料啦。

這邊用到了一個新的東西 OrderSerializer,這個東西我們把他寫在 order_api/serializers.py 裡面:

from rest_framework_mongoengine.serializers import DocumentSerializer from example_project.models import Order class OrderSerializer(DocumentSerializer): class Meta: model = Order fields = '__all__'

Serializer 的功能,如果用簡單的白話文說,就是把一個我們創立的 Document Class 轉換為 Python 的 Dictionary,讓我們可以做進一步的操作或輸出;當然,Serializer 的功能遠不止如此,未來會有一篇文章專門介紹他的 Best Practice。

最後記得我們要從網站的根路徑把所有 /order/開頭的請求都接入 order_api 來處理:

# example_project/urls.py from django.urls import include, path urlpatterns = [ path('order/', include('order_api.urls')), ]

接入到 order_api 以後,再從 order_api 的 urls 接入對應的 function:

# example_project/urls.py from django.urls import path from order_api import views urlpatterns = [ path('', views.all_orders), ]

試著用 Postman 或是 Visual Studio Code 的 Rest Clinet 對你剛完成的 API 發送請求看看吧!

結語

在這篇文章,我們學會了如何為自己正在做進行的專案設定出 Schema,並且用 MongoEngine 的 Document class來宣告在 Django 專案中。接著我們就可以輕鬆的透過這個 class 來對資料庫的資料做查詢或操作了。

本篇只示範性的給出 GET /order/ 這個接口的實作範例,剩餘的實作會在下篇給出,建議讀者在看完本文後當作回家作業先自己嘗試看看。

《菜雞也看得懂的全端專案開發實作教學》是我把最近一年左右接觸到的開發技術用非常白話的方式寫成一個很詳細的系列,其中內容包含了前後端分離開發的基本概念、Django Rest Framwork + MongoDB 的後端開發、Docker 和 CircleCI 的自動化部署、React 以及 React Native 的前端開發等等,希望可以讓高中生、大學生甚至是已經出社會的人士,對開發有興趣卻不知道從哪裡下手的人們有一個入門的地方。

由於我也是從網路上反覆翻找大量資料,經過自己的解讀、理解以及實作後才打出這一系列的文章,難免可能會有觀念錯誤的部分,還請各位讀者可以向我指出,讓我有一個成長學習的機會 👍

如果你也是對專案開發有興趣的同學,或是正在跟著我們的系列教學,歡迎你加入我們 Chief Noob 的 Discord 菜雞開發社群,一起討論和互相學習:

Chief Noob - Discord

如果你喜歡我的文章,希望你可以給我 Clap 以及留言,這是對我持續創作的一個重要動力!

分享你的看法

暫無留言,你可以成為第一個留言的人!

author-avatar

關於作者

Yuanlin Lin 林沅霖

台灣桃園人,目前就讀浙江大學,主修計算機科學與技術,同時兼職外包全端開發工程師,熱愛產品設計與軟體開發。