上一篇完成了 CRUD 的前兩個:
POST /tasks → 新增
GET /tasks → 查全部
GET /tasks/{id} → 查其中一筆
今天要把剩下的兩個做完:
PUT /tasks/{id} → 修改
DELETE /tasks/{id} → 刪除
修改和刪除都一定要先知道「要動的是哪一筆」。
如果只寫 /tasks,後端不會知道我們想改的是第 1 筆還是第 2 筆。
所以:
PUT /tasks/1 → 修改編號 1 的作業
DELETE /tasks/1 → 刪除編號 1 的作業
這也呼應 Day 6 說的:Path 指定 Resource,Method 表示要做什麼。
上一篇的 get() 裡面有一段找資料的程式:
for task in tasks:
if task["id"] == task_id:
return task
api.abort(404, ...)
今天 PUT 和 DELETE 也都需要先找到那一筆資料,如果每個地方都複製一次會很雜亂。
所以先把它抽成一個函式:
def find_task(task_id):
for task in tasks:
if task["id"] == task_id:
return task
api.abort(404, f"找不到 id 為{task_id} 的作業")
因為 api.abort() 會直接中斷這次的請求,所以只要順利執行到下一行,就代表資料一定有找到。
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),
})
tasks = []
next_id = 1
def find_task(task_id):
for task in tasks:
if task["id"] == task_id:
return task
api.abort(404, f"找不到 id 為{task_id} 的作業")
@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):
return find_task(task_id)
@api.expect(task_model)
def put(self, task_id):
task = find_task(task_id)
data = api.payload
task["name"] = data.get("name", task["name"])
task["completed"] = data.get("completed", task["completed"])
return task
def delete(self, task_id):
task = find_task(task_id)
tasks.remove(task)
return '', 204
if __name__ == '__main__':
app.run(debug=True)
@api.expect(task_model)
def put(self, task_id):
task = find_task(task_id)
data = api.payload
task["name"] = data.get("name", task["name"])
task["completed"] = data.get("completed", task["completed"])
return task
流程其實很單純:
先用 task_id 找到那一筆(找不到就 404)
用 api.payload 拿到這次要改成什麼
把值蓋上去
回傳改好的資料
修改和新增一樣需要帶資料進來,所以 put() 上面也要加 @api.expect(task_model),Swagger 才會有輸入框。
這裡有一個小細節:
task[“name”] = data.get(“name”, task[“name”])
意思是「如果這次有傳 name 就用新的,沒有傳就保留原本的」。
其實嚴格來說,PUT 的語意是「整筆取代」:
使用者要把完整的資料傳進來,沒傳到的欄位就會被清掉。
如果只想改其中一個欄位(例如只把 completed 改成 true),比較正確的 Method 是 PATCH(部分更新)。
我們這次為了單純一點,只使用 PUT,而且寫成「沒傳的欄位就保留原本的值」。
現在先知道有 PATCH 這個東西,以及 PUT 的原本語意是什麼就可以了。
def delete(self, task_id):
task = find_task(task_id)
tasks.remove(task)
return '', 204
一樣先找到那一筆,找不到就 404。
找到之後用 tasks.remove(task) 把它從 List 裡拿掉。
比較特別的是回傳:
return '', 204
204 是 No Content,表示「這次處理成功了,但沒有東西要回傳給你」。
刪除之後資料已經不存在了,也沒什麼好回傳的,所以很常用 204。
如果想讓前端收到一句訊息,也可以寫成:
return {“message”: “刪除成功”}, 200
兩種都有人用,重點是整個 API 要一致。
重新啟動:
python app.py
打開
http://127.0.0.1:5000/
Swagger 上現在有 GET / POST /tasks,以及 GET / PUT / DELETE /tasks/{task_id}
因為 Server 一重開資料就會不見(資料存在記憶體裡),所以要先用 POST /tasks 新增兩筆來測試。
步驟 1:新增資料
{
"name": "洗碗",
"completed": false
}
{
"name": "寫 Day 15",
"completed": false
}
步驟 2:用 PUT 把第 1 筆改成完成
點開 PUT /tasks/{task_id}
task_id 填 1
Request Body 改成:
{
"name": "洗碗",
"completed": true
}
Execute,Response:
{
"id": 1,
"name": "洗碗",
"completed": true
}
PUT /tasks/1 之後,completed 變成 true
再用 GET /tasks 確認一次,第 1 筆的 completed 真的變成 true 了。
修改後用 GET /tasks 確認資料真的被改掉
步驟 3:用 DELETE 刪掉第 2 筆
點開 DELETE /tasks/{task_id}
task_id 填 2
Execute
這次的 Code 會是 204,而且 Response body 是空的。
DELETE /tasks/2 回傳 204,沒有內容
再用 GET /tasks 看一次,就只剩下第 1 筆了。
刪除後 GET /tasks 只剩一筆資料
步驟 4:再刪一次同一筆
剛剛已經把 id 2 刪掉了,如果再 DELETE 一次 /tasks/2:
Code 會是 404,訊息是「找不到 id 為 2 的作業」。
重複刪除同一筆,回傳 404
這樣就知道 find_task() 有確實發揮作用。
GET /tasks → 取得全部(200)
POST /tasks → 新增一筆(201)
GET /tasks/{id} → 取得一筆(200 / 404)
PUT /tasks/{id} → 修改一筆(200 / 404)
DELETE /tasks/{id} → 刪除一筆(204 / 404)
到這裡,CRUD 四個操作其實都已經寫完了。
不過現在的程式還有一些可以整理的地方,例如回傳的資料格式沒有統一,Swagger 文件上也看不出每個 API 會回傳什麼。
下一篇就把這些整理起來,完成第一個比較完整的 API。