上一篇我們讓 API 可以接收資料了:
@api.expect(task_model)
def post(self):
data = api.payload
return {"你傳進來的資料": data}, 201
透過 Request Body 把 JSON 傳進來,再用 api.payload 拿到資料。
不過那時候我們只是把收到的資料原封不動回傳回去,並沒有把它存起來。
所以新增完之後,再用 GET /tasks 去查,看到的還是一開始寫死的那一筆:
{
"name": "Flask",
"completed": false
}
今天就要讓「新增」和「查詢」真的接起來。
CRUD 是四個操作的縮寫,也是大部分後端會一直重複做的事情:
Create(新增)→ POST
Read(查詢)→ GET
Update(修改)→ PUT
Delete(刪除)→ DELETE
這四個其實就是 Day 5 認識的 HTTP Method,只是換成另一個講法。
今天先做前兩個:Create 和 Read。
Update 和 Delete 留到下一篇。
目前我們還沒有學到資料庫,所以先用 Python 的 List 和 Dict 把資料存在程式裡。
每一筆作業是一個 Dict:
{
"id": 1,
"name": "洗碗",
"completed": false
}
所有作業放在一個 List 裡:
tasks = [
{"id": 1, "name": "洗碗", "completed": False},
{"id": 2, "name": "寫 Day 14", "completed": False},
]
先說清楚一件事:
這種做法只是練習用的,因為資料是存在程式的記憶體裡,只要 Server 一關掉,資料就會全部不見。
這也是 Day 17 會講到「為什麼需要資料庫」的原因,今天先讓流程跑起來。
前面幾天我們寫的都是 /tasks,指的是「全部的作業」。
但如果之後要修改或刪除「某一筆」作業,就需要一個方式來指定是哪一筆。
所以每一筆資料通常都會有一個不會重複的編號,這個編號就是 id。
有了 id 之後,就可以寫成:
GET /tasks → 取得全部作業
POST /tasks → 新增一筆作業
GET /tasks/1 → 取得編號 1 的作業
這也是 Day 6 提到的 REST 設計方式:
/tasks → 作業這個 Resource 的集合
/tasks/1 → 集合裡面的其中一筆
app.py
from flask import Flask
from flask_restx import Api, Resource, fields
app = Flask(__name__)
api = Api(app)
task_model = api.model('Task', {
'name': fields.String(required=True, description='作業名稱', example='洗碗'),
'completed': fields.Boolean(required=True, description='是否完成', example=False),
})
# 先把資料存在程式裡(Server 關掉就會不見)
tasks = []
next_id = 1
@api.route('/tasks')
class TaskList(Resource):
def get(self):
return tasks
@api.expect(task_model)
def post(self):
global next_id
data = api.payload
task = {
"id": next_id,
"name": data.get("name"),
"completed": data.get("completed", False),
}
tasks.append(task)
next_id += 1
return task, 201
@api.route('/tasks/<int:task_id>')
class TaskDetail(Resource):
def get(self, task_id):
for task in tasks:
if task["id"] == task_id:
return task
api.abort(404, f"找不到 id 為{task_id} 的作業")
if __name__ == '__main__':
app.run(debug=True)
tasks = [] 和 next_id = 1
tasks 是存放所有作業的 List,一開始是空的。
next_id 是下一筆資料要使用的 id,每新增一筆就加 1,這樣 id 才不會重複。
class TaskList(Resource) 和 class TaskDetail(Resource)
這裡把原本的 Tasks 拆成了兩個類別。
Day 11 有提到,class 的名字跟 Path 沒有直接關係,它只是我們自己取的名字。
真正決定 Path 的是上面的 @api.route()。
/tasks 是「整份清單」,所以叫 TaskList
/tasks/ 是「其中一筆」,所以叫 TaskDetail
這樣之後看程式比較清楚。
return tasks
這裡直接回傳一個 Python 的 List,Flask-RESTX 會幫我們轉成 JSON 陣列。
所以如果現在還沒有新增任何資料,打開 /tasks 會看到:
[]
next_id 是寫在函式外面的變數,如果在函式裡面要「改變」它的值,Python 需要我們先寫 global 告訴它:我要改的是外面那個。
(這種寫法只適合現在這種練習,之後資料換成資料庫就不需要了。)
data 是 api.payload 拿到的字典。
data.get(“name”) → 取出 name
data.get(“completed”, False) → 取出 completed,如果沒有傳就當作 False
這裡用 .get() 而不是 data[“completed”],是因為如果使用者沒有傳這個欄位,用中括號會直接讓程式出錯(KeyError),最後變成 500。
(怎麼好好處理「使用者亂傳資料」,Day 24、Day 25 會再講。)
@api.route('/tasks/<int:task_id>')
這是今天另一個重點。
是 Flask 的路徑參數(URL 變數),意思是:
這段路徑是會變動的,把它取出來,轉成整數,然後叫做 task_id
所以:
/tasks/1 → task_id = 1
/tasks/2 → task_id = 2
取出來的值會傳給下面的函式,所以函式要寫成:
def get(self, task_id):
順帶一提,int:… 裡的 int 也有檢查的效果。
如果打開 /tasks/abc,因為 abc 不是整數,Flask 會直接回 404,不會進到我們的程式裡。
api.abort(404, ...)
如果整個 List 找過一遍都沒有這個 id,就代表這筆資料不存在。
這時候不應該回傳空白或 200,而是要告訴前端「找不到」。
api.abort(404, f"找不到 id 為 {task_id} 的作業")
api.abort() 會立刻中斷這次的處理,並回傳我們指定的狀態碼。
Day 24 會再完整介紹狀態碼和錯誤處理。
重新啟動:
python app.py
打開
http://127.0.0.1:5000/
Swagger 上現在有 GET /tasks、POST /tasks、GET /tasks/{task_id} 三個 API
步驟 1:先用 GET /tasks 看看
Try it out → Execute
因為還沒有新增任何資料,Response 會是:
[]
GET /tasks 回傳空陣列 []
步驟 2:用 POST /tasks 新增一筆
Try it out,把 Request Body 改成:
{
"name": "洗碗",
"completed": false
}
Execute 之後,Response 會是:
{
"id": 1,
"name": "洗碗",
"completed": false
}
可以看到 API 幫我們補上了 id,狀態碼是 201。
POST /tasks 新增成功,Response 是 201 和帶有 id 的資料
再新增一筆,例如:
{
"name": "寫 Day 14",
"completed": false
}
這次的 id 會是 2。
步驟 3:再用 GET /tasks 查一次
這次就會看到兩筆資料:
[
{
"id": 1,
"name": "洗碗",
"completed": false
},
{
"id": 2,
"name": "寫 Day 14",
"completed": false
}
]
GET /tasks 這次回傳兩筆資料
新增和查詢終於接起來了。
步驟 4:用 GET /tasks/{task_id} 查其中一筆
在 task_id 欄位填 1,Execute:
{
"id": 1,
"name": "洗碗",
"completed": false
}
GET /tasks/1 只回傳一筆資料
步驟 5:故意查一個不存在的
在 task_id 填 999,Execute:
Code 會變成 404,Response 大概是:
{
"message": "找不到 id 為 999 的作業"
}
GET /tasks/999 回傳 404
整個流程:
POST /tasks(帶著 JSON)
↓
執行 post(),api.payload 拿到資料
↓
補上 id,append 進 tasks
↓
回傳新增好的那一筆(201)
GET /tasks
↓
執行 get(),回傳整個 tasks
↓
Flask-RESTX 轉成 JSON 陣列
POST /tasks → 新增一筆,並自動給一個 id
GET /tasks → 取得全部
GET /tasks/{id} → 取得其中一筆,找不到就回 404
也順便認識了路徑參數 。
不過現在新增進去的資料,還是只能看、不能改。
如果今天作業寫完了,想把 completed 改成 true,或想把打錯的作業刪掉,要怎麼做?
下一篇就來完成 CRUD 的另外兩個:修改(PUT)和刪除(DELETE)。