0%

前言

今天這篇我要來記錄如何在 Flask API 中串接並使用 Postgres
我在上一篇中提到在 Heroku 上用 SQLite 作為資料庫的缺點,以及 Heroku 推薦使用 Postgres
因此我去研究了如何做到這一點
過程遇到很多 error 真的很崩潰,也沒有把遇到的每個問題都記錄下來有點可惜
現在就憑印象,能寫多少就多少吧

這邊我會把 postgres 操作分成 local 跟 remote
先從 local 開始吧

安裝 Postgres

PostgreSQL 是一款蠻有名的關係資料庫
有時候縮寫成 Postgres
那我們到這裡下載不同作業系統對應的安裝軟體
那我用的是 Windows,所以這裡就用 windows 來舉例
安裝時注意一件事
我們要另外自己手動安裝 pgAdmin
所以記得把這個 pgAdmin4 的選項取消,不要選

接下來他會請你指定安裝路徑,super 使用者的密碼,預設 port,時區(選default就好)

再來要到這裡安裝 pgAdmin
安裝過程很單純,一直下一步到安裝就好,不會有什麼問題
完成後打開它

到這邊安裝就差不多了

在本地串接 API 和 Postgres

點擊此處建立資料庫,取名
(以後面的例子而言我是取 issuedata,不是圖裡的 NewDatabase)

接下來回到程式端
前幾篇文章的程式碼都是 for 教學用的
但我現在做了正式版,是將來要應用在 TAN 網站的版本
為了不造成混淆我先把連接資料庫前的正式版程式碼貼上來~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from flask import Flask, Request
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS

app = Flask(__name__)
CORS(app)

db = SQLAlchemy(app)

class Issue(db.Model):
__tablename__ = "issue_table"
id = db.Column(db.Integer, primary_key=True)
year = db.Column(db.Integer, nullable=False)
month = db.Column(db.Integer, nullable=False)
date = db.Column(db.Integer, nullable=False)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.String(2000))

def __repr__(self):
return f"{self.year}/{self.month}/{self.date} - {self.title}"


@app.route('/')
def index():
return 'Wellcome to TAN API!'


@app.route('/archive')
def get_archive():
issues = Issue.query.all()
output = []
for issue in issues:
issue_data = {
"id": issue.id,
"year": issue.year,
"month": issue.month,
"date": issue.date,
"title": issue.title,
"content": issue.content,
}
output.append(issue_data)

return {"drinks": output}


if __name__ == "__main__":
app.run()

要連接本地 postgres 資料庫,我需要加上:

1
2
app.config["SQLALCHEMY_DATABASE_URI"] = 'postgresql://postgres:et0997@localhost/issuedata'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

我們先來看第一行

app.config["SQLALCHEMY_DATABASE_URI"] = 'postgresql://postgres:et0997@localhost/issuedata'

這段 uri 的格式是 postgresql://<username>:<user_password>@<port>/<database_name>

  • username: PostgreSQL 的使用者。由於我沒有另外新建使用者,所以就用 super user 也就是postgres就可以了
  • user_password: user 密碼。你在安裝 postgres 的時候不是有設定過一次密碼嗎? 就是那個(user 選 postgres 的話啦)
  • port: 連接端口。由於我們是在本地測試,所以填localhost就好
  • database_name: 你剛剛用 pgAdmin 建立的那個資料庫名稱

總之這行是在告訴 Flask_SQLAlchemy 應該連接什麼資料庫,要怎麼找到它

最底部的 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
是為了避免跳出以下 warning

1
2
warnings.warn('SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead 
and will be disabled by default in the future. Set it to True to suppress this warning.')

就我的理解
這是 Flask_SQLAlchemy 造成的小問題
它的意思是
由於 Flask_SQLAlchemy 是 SQLAlchemy 的一個包裝(類似升級版)
本質上還是 SQLAlchemy 在運作
而 Flask_SQLAlchemy 有自己的一套 event notification system
(不知道怎麼翻比較好,直接用原文)
所以它必須隨時監聽 SQLAlchemy 的任何修改(如’SQLALCHEMY_TRACK_MODIFICATIONS’字面上的意思)
這可能會造成性能瓶頸
設為 False 避免這個問題

至此就設定好本地連接了

建立 local DB 的 table 和 資料

接著打開 python shell

1
2
3
4
5
6
from app import db, Issue # 從 app.py 引入 db 和 Issue 模組
db.create_all() # 在 database 中建立 Table,名稱為 Issue (如果沒用 __tablename__ 更改預設名稱的話)
# 宣告新的單筆資料 issue
issue = Issue(year=2077,month=1,date=27,title="Issue Title", content="This is something important.")
db.session.add(issue) # 把 issue 加進 session
db.session.commit() # 提交 session

完成後打開 pgAdmin
從以下路徑找到名為 issuedata 的 table

耶! 看到資料了

結語

剛剛的安裝過程都只是在自己的電腦上存取資料而已
但最終我們要把 API 放在網路上呀
到時要怎麼用 Postgres 呢?

我覺得 部落格好像不太適合寫太長
遠端的部分就留到下一篇吧!

前言+中言+後言

這會是個小短篇
在前兩篇文章中我介紹了怎麼用 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 這個平台吧
之後再整理上來囉~