0%

前言+中言+後言

這會是個小短篇
在前兩篇文章中我介紹了怎麼用 python flask 架設 api

於是接下來我就在研究要如何
串接 sqlite 資料庫後放到 Heroku 上運作

研究途中想到個問題
就是 api 如果經過 POST 請求修改了資料庫
那在雲端更新的資料要怎麼抓下來到本地中?

我試了 git pull heroku main

但它卻告訴我 Already up to date.

Heroku Dev Center的一篇文章 有提到這件事
他說 sqlite 雖然方便又適合新手,但不建議用在 production

SQLite runs in memory, and backs up its data store in files on disk. While this strategy works well for development, Heroku’s Cedar stack has an ephemeral filesystem. You can write to it, and you can read from it, but the contents will be cleared periodically. If you were to use SQLite on Heroku, you would lose your entire database at least once every 24 hours.

意思是 SQLite 在記憶體運作,每隔一段時間 Heroku 就會把它清除

Even if Heroku’s disks were persistent running SQLite would still not be a good fit. Since SQLite does not run as a service, each dyno would run a separate running copy. Each of these copies need their own disk backed store. This would mean that each dyno powering your app would have a different set of data since the disks are not synchronized.

老實說這一段我不懂,但看到一些關鍵字說會發生每個 dyno (這啥)都有不同的 data set 的情形發生

所以他們推薦使用 Postgresql

那我就學這個吧我想應該就一些前置作業的不同吧
畢竟都是要用 ORM 來操作

前言

繼前一篇用 Flask 建立簡單的 API
我把大部分篇幅用來介紹我為什麼要做,為什麼選flask的心路歷程
以及前置作業上
這篇則會把來不及提到的資料庫環節記錄下來

先回顧一下前一篇的程式樣貌
僅定義了啟動 app 和根路徑的 GET 方法

1
2
3
4
5
6
7
8
9
10
from flask import Flask

app = Flask(__name__)

@app.route('/') # Decorator。 在這個路徑下接收到 request (預設為GET) 的話要做以下的事
def index():
return "Hello world from Flask"

if __name__ == "__main__": # 當這份程式被作為主程式執行時
app.run(debug=Ture) # 執行app,debug=True會讓app處於監聽狀態

建立 sqlite database

為了在 flask 中操作資料庫
我們需要使用 flask_sqlalchemy 模組
pip install flask_sqlalchemy
接著在 app.py 中
from flask_sqlalchemy import SQLAlchemy

並且我們會使用 ORM 的技術來操作資料庫 (這裡我們選擇輕量的資料庫:sqlite)

ORM 是 Object Relational Mapping(物件關聯對映)
一句話解釋就是

用操作物件的方式操作資料庫

這樣即使我們不用 SQL 語法,也可以使用 SQL 資料庫了
開始建立資料庫!

1
2
3
# 設定要連線的資料庫
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.db'
db = SQLAlchemy(app) # 宣告 db

'SQLALCHEMY_DATABASE_URI'這個參數是告訴app你要跟什麼資料庫連接
以我們的例子是用 sqlite,所以設為sqlite:///data.db
注意有三個斜線 它的意思是相對路經
後面的data.db是你待會要創建的 db 的檔案名稱

如同剛剛說的 ORM 是以操作物件的方式進行
我們需要新增一個 class 作為資料庫的 model

1
2
3
4
5
6
7
class Issue(db.model): # 宣告一個類別,這個類別繼承 db.Model
id = db.Column(db.Integer, primary_key=True) # 宣告一個id欄位,型別為db.Integer,
name = db.Column(db.String(80), unique=True, nullable=False) # 宣告一個name欄位
content = db.Column(db.String(20000)) # 宣告一個 content 欄位

def __repr__(self):
return f"{self.name} - {self.content}"

接下來我們回到 terminal 輸入 python
以打開 python 的 interactive prompt

假如說我想在 API 中存放 TAN 網站的 email 文字檔
那我就把每一份文字檔都叫作 issue 好了
逐一輸入以下代碼

1
2
3
4
5
from app import db  # 引入db模組
db.create_all() # 建立db,會生成一個 data.db 檔案
issue = Issue(name='issue 1', content="This is issue 1.") # 建立第一筆資料
db.session.add(issue) # 把資料加入資料庫
db.session.commit() # 確認,提交變更

就能在資料庫中建立第一份 issue
在下一章節,我們要把資料秀出來

新增 endpoint

一個 API 通常不會只有根目錄有作用
於是 endpoint 就派上用場了
endpoint 指的是 API 的對外接口
API的每個有效路徑都是一個 endpoint
那我就可以建立一個 endpoint 叫做 ‘/issues’
只要使用者對這個 endpoint 送出請求
API 就會回傳相對應的資料
具體來說怎麼做呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app.route('/issues') # 新增一個端點
def get_issues():
issues = Issue.query.all() # 回傳包含所有Issue物件的列表

# 由於issues是一個列表
# 但 api 的回傳值必須是 json serializable 所以不能是列表
# 必須是字串或是 dictionary
# 即使是 dictionary,裡面的所有 value 也都必須是 json serializale
# 簡單來說整個回傳值必須要長得跟 json 結構一模一樣,否則就必須是 string
output = []
for issue in issues:
issue_data = {"name": issue.name, "content": issue.content}
output.append(issue_data)
return {"issues": output}

補充說明:

Issue.query.all() 查了很久都不知道這個 query 是誰的語法
後來才知道他是db.session.query(Issue)的縮寫
一些相關的用法如下:

取得所有 todo 資料
Todo.query.all()

限制 1 筆資料
Todo.query.limit(1).all()

正向/逆向排序
Todo.query.order_by(Todo.content).all()
Todo.query.order_by(Todo.content.desc()).all()

取得第一筆資料
Todo.query.first()

取得 primary key=1 一筆資料
Todo.query.get(1)

如此一來
連接到 http://127.0.0.1:5000/issues 時就會看到 {“issues”:[{“name”:”issue 1”, “content”:”This is issue 1.”}]}

GET+parameter

如果我們需要帶參數要怎麼寫呢?
那就要在路徑中用 < > 夾住參數
這樣就能在function中傳入參數了
如下:

1
2
3
4
5
@app.route('/issues/<id>') # 把接在 /issues/ 後面的字令作 id
def get_issue(id): # 傳入 id
# get_or_404() 跟 get() 類似,差別在於如果 id 不存在不會回傳 None 而是 404 error
issue = Issue.query.get_or_404(id)
return {"name":issue.name,"content":issue.content}

這樣就可以獲取第一筆資料

補充說明:
Issue.query.get_or_404(id) 這邊之所以可以用 id 來獲取檔案
是因為前面在定義 id 的 Column 的時候,有特別設定 primary_key=True,意思是設為主鍵
SQLite 主鍵是用於定義唯一行記錄的一個值。一個表只能有一個主鍵。
其實它的功用就像 id 一樣,確保每筆資料都是唯一的不重複。

撰寫 POST 方法

想要新增 POST 方法,作法跟 GET 很像
只不過我們要指定 app.route 的 methods 參數

1
2
3
4
5
6
7
@app.route('/issues', methods=["POST"]) # 設方法為 POST
def add_issue():
# 根據 request 的 body 的內容建立新的 Issue 物件
issue = Issue(name=request.json["name"], content=request.json["content"])
db.session.add(issue) # 加入 db
db.session.commit() # 確認提交
return {"id": drink.id} # 回傳 id

POST 有沒有成功比較難單用瀏覽器測試
所以我們可以用 postman 如下 (記得要選擇 POST 再按 send)

撰寫 DELETE 方法

最後是刪除
做法跟POST類似,都需要給予參數

1
2
3
4
5
6
@app.route('/issues/<id>')
def delete_issue(id):
drink = Drink.query.get_or_404(id)
db.session.delete(drink)
db.session.commit()
return {"message":f"Id {id} deleted!"}

感想

終於講完啦!!! 寫了好久天哪
寫這兩篇之前我還以為我會的差不多了
但一解釋起來才發現細節其實完全不會
一邊惡補一邊寫哈哈
這就是我想寫部落格的目的 讚

不過現在我只做了非常非常單純也不實用的 api
還需要很多加強,實際應用在網頁上也不知道會發生什麼事情
就看我未來幾天的造化囉

下一步就是要練習把 api 部屬到網路上了
我應該會用 Heroku 這個平台吧
之後再整理上來囉~

前言

雖然我是覺得在學好一件事情之前不要亂學其他東西另闢戰場
但現實就是永遠有學不完的東西
也永遠沒有學好一件事情的時候
誰知道我會突然又需要學起 python 呢

事情是這樣
最近稍微熟悉一下 RWD 和 React 之後
我就開始動工 react 版本的 TAN 了
做著做著我便開始思考要怎麼簡化網頁的更新流程

目前的做法是每次收到老師的郵件,我就要手動進到伺服器去改檔案
雖然熟了操作起來還蠻快的,但還是有些麻煩而且有可能會東漏西漏
原本更慘,在我剛接手這個網站的時候
除了要打開每一份 html 檔更改標籤
還要手動做篩選,必須自己決定每個 issue 是被歸類在什麼類別
是後來我改用 javascript 選擇性 render html tag
才讓更新網站的速度變得比較能夠接受
但終究我還是需要 ssh 進到伺服器然後把檔案傳上去

所以現在,我打算再更簡化一些
開發一個 api 架在雲端
只要進行 POST request 就能直接更新檔案
然後 react 端就只要 GET 接著渲染出來就好

這代表我要學做 api 啦!!!

該選什麼框架?

一開始我就遇到了二選一
記得之前hahow的老師建議我可以用 python 寫 api
所以我查了一下有一個適合的模組 Flask
但後來我又發現有一個 Express.js
可以讓我用 javascript 寫 api
這讓我有點猶豫 (畢竟我現在 JS 比 python 熟很多)

但最後我選了 flask
正是因為 python 已經漸漸被我淡忘了
如果再不找機會練習的話,真的就要變回一張白紙了
那麼以下就是我建立第一個 flask API 的心路歷程

我會把步驟拆成:

  1. 前置作業
  2. 撰寫 API
  3. 連接 database

前置作業

建立專案資料夾並進入

1
mkdir my-first-flask-api

這次決定自建API,也終於讓我知道虛擬環境的概念
基本上它就像是 node 專案裡的 node_module 資料夾
存放一些專案的 dependency
只不過跟node不同的是
node每個專案都有獨立的環境,所以不同專案間的依賴是互不影響的
但 python 就比較複雜,如果直接 pip 安裝任何模組
他們會被裝在 ...python/lib/site-packages... 之類的地方
有點像 global install 的感覺
但其實沒那麼簡單,因為 python 又可以同時存在好幾個版本
結果就是各個專案間互相牽制,造成一堆問題

所以我們先來建置虛擬環境!
在眾多模組當中我選了 virtualenv 這個模組

首先 pip 安裝

pip install virtualenv

對於什麼時候要用 pip 安裝,什麼時候要用 pip3,之後我再寫一篇短文紀錄好了
大部分時候先用 pip 是 ok 的
有問題時再試 pip3

安裝好後執行以下指令,以在專案資料夾中建立一個虛擬環境資料夾

virtualenv .venv

他會根據你的作業系統
建立不同結構的.venv資料夾

啟用虛擬環境

Windows

如果用 cmd
.\.venv\Scripts\activate.bat

如果用 powershell (VScode是用這個)
.\.venv\Scripts\activate.ps1

. .\.venv\Scripts\activate

MacOS

source ./.venv/bin/activate

. ./.venv/bin/activate

執行完如果在命令列的最開頭看到 (.venv)
就代表成功了

接下來安裝本次最重要的模組 Flask

pip install flask

完成後可以輸入 pip show flask
檢查模組是否如期被安裝在 .venv 虛擬環境中
如果在這之前沒有先啟用虛擬環境的話
flask 就會被安裝在 site-package 那裏

安裝完所需的模組後執行:
pip freeze > requirements.txt

pip freeze 有點像為目前的開發環境拍張 snapshot
紀錄所使用的模組有哪些
就好像凍結這一瞬間一樣(莫名有點浪漫?)
後面的 > 代表要把左側的輸出值寫入右側的檔案
也就是 requirements.txt

這是為了讓其他人 clone 這份專案後能夠以
pip install -r requirements.txt (-r: –requirement)
安裝專案所需的模組

這邊要注意,最好是使用複數的 requirements
雖然打什麼檔名都可以 pip install -r
但有些時候會嚴格檢查(如Heroku)
所以還是要照規矩來比較好

撰寫API

我們建立一個 python file app.py

1
2
3
4
5
6
from flask import Flask

app = Flask(__name__)

if __name__ == "__main__": # 當這份程式被作為主程式執行時
app.run(debug=Ture) # 執行app,debug=True會讓app處於監聽狀態

接著我們建立根路徑

1
2
3
@app.route('/') # Decorator。 在這個路徑下接收到 request (預設為GET) 的話要做以下的事
def index():
return "Hello world from Flask"

目前整個程式非常單純
就是引入flask後啟動它
並設定當根路徑獲得GET請求時要回傳什麼資訊

接下來我們回到 terminal
執行 python app.py
便能開啟API並等待請求

1
2
3
4
5
FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
warnings.warn(FSADeprecationWarning(
* Debugger is active!
* Debugger PIN: 124-333-105
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

打開瀏覽器並進到 http://127.0.0.1:5000/
就會看到輸出的字串了!

待續

文章先寫到這吧不想讓這篇變太長
串接資料庫的部分就留到下一篇囉
到時也會把 API 的 POST, DELETE 方法給做出來