上一篇我們讓 POST 和 GET 接上了資料庫,資料也確實保存下來了。
今天要把剩下的兩個補完:
PUT /tasks/{id} → 修改
DELETE /tasks/{id} → 刪除
做完之後,整個 API 就是完整的資料庫版本了。
Day 15 我們寫過一個 find_task():
def find_task(task_id):
for task in tasks:
if task["id"] == task_id:
return task
api.abort(404, f"找不到 id 為{task_id} 的作業")
那時候是自己用 for 迴圈一筆一筆比對。
現在資料在資料庫裡,就不用自己找了:
def find_task(task_id):
task = db.session.get(Task, task_id)
if task is None:
api.abort(404, f"找不到 id 為{task_id} 的作業")
return task
db.session.get(Task, task_id)
意思是「用主鍵去 Task 這張表拿一筆資料」,相當於 Day 18 的:
SELECT * FROM task WHERE id = 1;
找不到的時候它會回傳 None,所以我們自己判斷之後回 404。
Flask-SQLAlchemy 其實也有更短的寫法:
task = db.get_or_404(Task, task_id)
它會在找不到的時候自己丟出 404。
不過我們自己寫一個 find_task(),之後想要自訂錯誤訊息比較方便,所以先維持自己判斷的版本。
@api.expect(task_input)
@api.marshal_with(task_response)
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)
db.session.commit()
return task
跟 Day 15 的版本比一下,差別只有兩個:
原本是改字典:task[“name”] = …
現在是改物件的屬性:task.name = …
改完之後多了一行 db.session.commit()
這裡有一個 ORM 很方便、但一開始會覺得有點神奇的地方:
我們並沒有寫任何 UPDATE 語法。
因為 task 是從 session 裡拿出來的物件,SQLAlchemy 會追蹤它有沒有被改過。
我們改了 task.name 之後,commit 的時候它就會自動產生對應的 UPDATE 送進資料庫。
所以修改不用 add(),只要改屬性再 commit 就可以了。
@api.response(204, '刪除成功')
def delete(self, task_id):
"""刪除指定編號的作業"""
task = find_task(task_id)
db.session.delete(task)
db.session.commit()
return '', 204
一樣先找到那筆資料,找不到就 404。
找到之後用 db.session.delete(task) 標記要刪除,再 commit 真的送出去。
跟 Day 15 的 tasks.remove(task) 是同樣的意思,只是這次是對資料庫做。
app.py
from flask import Flask
from flask_restx import Api, Resource, fields
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tasks.db'
db = SQLAlchemy(app)
api = Api(app, title='Task API', version='1.0', description='30 天鐵人賽練習用的作業管理 API')
class Task(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
completed = db.Column(db.Boolean, nullable=False, default=False)
task_input = api.model('TaskInput', {
'name': fields.String(required=True, description='作業名稱', example='洗碗'),
'completed': fields.Boolean(required=True, description='是否完成', example=False),
})
task_response = api.model('TaskResponse', {
'id': fields.Integer(readonly=True, description='作業編號'),
'name': fields.String(description='作業名稱'),
'completed': fields.Boolean(description='是否完成'),
})
def find_task(task_id):
task = db.session.get(Task, task_id)
if task is None:
api.abort(404, f"找不到 id 為{task_id} 的作業")
return task
@api.route('/tasks')
class TaskList(Resource):
@api.marshal_list_with(task_response)
def get(self):
"""取得所有作業"""
return db.session.scalars(db.select(Task)).all()
@api.expect(task_input)
@api.marshal_with(task_response, code=201)
def post(self):
"""新增一筆作業"""
data = api.payload
task = Task(
name=data.get("name"),
completed=data.get("completed", False),
)
db.session.add(task)
db.session.commit()
return task, 201
@api.route('/tasks/<int:task_id>')
@api.param('task_id', '作業編號')
@api.response(404, '找不到作業')
class TaskDetail(Resource):
@api.marshal_with(task_response)
def get(self, task_id):
"""取得指定編號的作業"""
return find_task(task_id)
@api.expect(task_input)
@api.marshal_with(task_response)
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)
db.session.commit()
return task
@api.response(204, '刪除成功')
def delete(self, task_id):
"""刪除指定編號的作業"""
task = find_task(task_id)
db.session.delete(task)
db.session.commit()
return '', 204
with app.app_context():
db.create_all()
if __name__ == '__main__':
app.run(debug=True)
可以跟 Day 16 的版本放在一起看,會發現 API 的架構完全沒變。
變的只有中間那一段:從操作 List 變成操作資料庫。
python app.py
打開
http://127.0.0.1:5000/
Swagger 首頁,五個 API 都在
PUT /tasks/1 之後,completed 變成 true

DELETE /tasks/2 回傳 204

刪除後只剩一筆

python check_db.py

Day 18 有學到 SQL 的 WHERE:
SELECT * FROM task WHERE completed = 0;
用 ORM 也可以做到。
例如讓 GET /tasks 支援「只看還沒完成的作業」:
from flask import request
@api.route('/tasks')
class TaskList(Resource):
@api.param('completed', '篩選是否完成,填 true 或 false')
@api.marshal_list_with(task_response)
def get(self):
"""取得所有作業,可以用 completed 篩選"""
stmt = db.select(Task)
completed = request.args.get('completed')
if completed is not None:
stmt = stmt.filter_by(completed=(completed.lower() == 'true'))
return db.session.scalars(stmt).all()
這裡用到的是 Day 13 提過的另一種傳資料方式:Query String。
也就是把條件放在網址上:
/tasks?completed=false
加上 @api.param 之後,Swagger 上的 GET /tasks 也會多一個 completed 輸入框可以填。
GET /tasks 多出 completed 參數,填 false 之後只查到未完成的作業
(這段是延伸,覺得太多可以先跳過,不影響後面的內容。)
把 find_task() 改成用 db.session.get() 去資料庫找
PUT:改物件屬性 + commit,SQLAlchemy 自動產生 UPDATE
DELETE:db.session.delete() + commit
整個 CRUD 都接上資料庫了
順便看了怎麼用條件查詢
到這裡,API 加資料庫的基本流程算是完整了。
不過還記得 Day 20 最後提到的限制嗎?
db.create_all() 只會建立還不存在的表。
如果之後想在 Task 多加一個欄位,例如「截止日期」,資料庫並不會自動跟著改,而且直接砍掉資料庫重建又會把資料全部弄丟。
下一篇就來認識 Flask-Migrate,看看資料表改了要怎麼辦。