Python Web Flask 對JSON的存取 — 2從資料庫內容匯出(上)

Sean Yeh
Python Everywhere -from Beginner to Advanced
16 min readMar 10, 2021

--

Aqaba, Jordan, photo by Sean Yeh

上一篇我們介紹了如何將Form表單中填寫的資料,轉換為JSON檔案。如果你也照著文章裡面的方式測試結果的話,應該會發現這樣子的程式在實際的商業邏輯上,會有一些不太適用的地方。在這一篇我們會延續上一篇的範例,要對上一篇完成的程式碼進行改進與優化。

既有程式碼的缺點

在上一篇中,我們逐一開發的程式碼,並非無懈可擊的。在這裡我們可以列出一些缺點,例如:

錯誤不可逆

從Form表單輸入資料時,如果不小心輸入錯誤了,在目前既有的網站下,是無法得到救濟的。因此,公司中輸入資料的的A君,個性上必須非常細心,每次輸入欄位中的資料,必須正確無誤。一旦發生輸入錯誤的情況,就必須重頭開始重新輸入。

資料尋找不易

在目前網站的設計下,並沒有提供任何功能讓使用者可以較容易的確認某一項設定是否已經建立。當使用者只需要設定三、五筆個的時候,由於資料不多,透過肉眼應該可以確認。但是,如果比數超過十筆之後,要確認是否存在,難度就變得越來越高了。

基於以上的缺點,A君如果要順利的操作這樣的系統,必定藝高人膽大,才有辦法勝任這項工作。或者是由開發者自己來代替A君輸入,可是若事情演變成這樣,已經離這系統開發的本意越來越遠了。(原本就是想要透過某種方式可以讓身為工程師的自己更為輕鬆)

解決與優化方向

基於上面的說明,我想或許可以透過下面幾個可以方式,來優化既有的網站。

使用資料庫

針對錯誤不可逆的部分,我們可以先將Form表單輸入的資料儲存在資料庫中,再提供給後續進行處理。這樣子資料可以保存更久。只要資料庫沒有被刪除,資料就一直存在。

增加CRUD功能

此外,可以再輔以CRUD功能,來解決錯誤不可逆的問題。我們可以在系統中加上維護資料庫資料的功能,例如編輯資料,或者是刪除資料等等。簡單的說,就是透過新增、編輯、刪除與瀏覽等等的CRUD方式來管理資料庫中的資料。

增加搜尋功能

針對資料尋找不易的問題,我們可以增加網站搜尋的功能,針對輸入的設定值的名稱來搜尋看看,是否已經存在?這樣子就可以快速找到特定的設定值。

增加排序功能

另外,我們也可以在首頁增加排序的功能,除了在一般排序時,提供一個固定的順序例如從字母A開始排到字母Z,也可以製作一個切換選項,讓使用者A君依照需要切換排序的順序。

匯出JSON

最後,我們還是希望匯出一個JSON檔案。裡面要寫入所有的設定資料,以便後續提供給P系統讀取使用。在匯出JSON檔案,最好可以事先提供我們機會把希望寫入檔案的資料確認無誤之後,在匯出。匯出方式應該要很簡單,最好是按一下按鈕,就可以完成程序。

開始實作

以下,我們就依照前面的計畫,開始實際製作、撰寫程式碼。專案的檔案架構如下:

檔案架構

為了讓專案的資料整齊,未來好維護。在大部分的狀況都會設計一種架構,在這個架構裡面,把應該放的檔案,放在相應的位子。未來在整理的時候,就會比較容易。

檔案 app.py

透過這個方式的規劃,我們一般會把啟動整個專案app的部分寫在app.py的地方。未來,只要在終端機中下達flask run的指令,就可以啟動整個app。以下就是app.py裡面的程式碼,在這裡我們匯入(import)app並且啟動(run)app。

#匯入app實體from configmaker import app
from configmaker.views import *
# 啟動appif __name__ == '__main__': app.run(debug=True)

檔案 __init__.py

一般來說,在專案資料夾裡一定會有一個__init__.py檔案。在此也不例外,在configmaker資料夾裡面也有一個__init__.py檔案。我們沒有在這個檔案中保留空白,而是把匯入flask套件的相關部分放在這裡。如下面的程式碼,我們匯入了os、Flask 並且在這裡實體化了app(如粗體字),而且也設定了SECRET_KEY這類config網站的設定。因此,我們才可以在app.py匯入app(from configmaker import app)。

import os
from flask import Flask
#實體化 app
app = Flask(__name__)
# config
app.config['SECRET_KEY']='mysecret'

檔案 models.py

這個檔案將會設定我們的資料庫欄位。

檔案 forms.py

這個檔案是放置Form表單的位置。我們需要的表單都會放在這裡。目前會有一個輸入表單AddItemForm。這個表單是透過FlaskForm與WTF Form等套件來完成的。

from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField,SelectField
from wtforms.validators import DataRequired
from wtforms import ValidationError
class AddItemForm(FlaskForm):
item = StringField('輸入名稱', validators=[DataRequired()])
submit = SubmitField('加入項目')

關於WTF Form表單的詳細製作方式,可以參考下面這一篇。

檔案 views.py

這個檔案非常的精彩,幾乎網站大部分的邏輯都寫在這裡。如果網站規模更大的話,建議還需要透過blueprint來進一步的拆分檔案比較好。

import os,jsonfrom flask import url_for, render_template, url_for, flash, redirect, request, jsonfrom configmaker import appfrom configmaker.forms import AddItemForm
@app.route('/')
def home():
...略...

目前views.py匯入的套件如上面所列,關於這個檔案的其他的細節,我們會在稍後介紹。

templates資料夾

templates放的是Jinja2樣板引擎需要的各種樣板。我們在這裡一樣會把主要版型放在base.html,其他的版型會透過extends的方式匯入主要版型。

{% extends "base.html" %}

config資料夾

config則放的是我們匯出的JSON檔案。預計的檔案名稱為config_data.json。

以上是整個架構的說明。接下來就可以開始增添新功能的行動。

資料庫

首先,我們要建立一個資料庫,來儲存設定檔案。在這裡我要使用SQLAlchemy來建立資料庫。

在__init__.py 檔案中匯入Flask SQLAlchemy與Flask Migrate套件。

from flask_sqlalchemy import SQLAlchemyfrom flask_migrate import Migrate

設定資料庫

因為我們需要指定資料庫的位置,所以還需要匯入os套件。

import os

匯入之後,可以先設定基礎路徑basedir:

basedir = os.path.abspath(os.path.dirname(__file__))

這個路徑會指向專案的絕對路徑。

接著要設定config。其中SQLALCHEMY_DATABASE_URI決定資料庫sqlite的檔名與位置。

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+os.path.join(basedir, 'data.sqlite')app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

實體化db,並且Migrate資料庫。以上是__init__.py需要調整的地方。

db = SQLAlchemy(app)Migrate(app,db)

其他使用SQLAlchemy的細節,可以參考下面這一篇的說明:

使用Flask Migrate的細節,則可以參考下面這一篇:

建立Models

現在,我們需要建立資料庫的資料表以及欄位。首先要從__init__.py匯入db。然後建立一個繼承自db.Model的class。

from configmaker import dbclass Configuration(db.Model):__tablename__ = 'configurations'id = db.Column(db.Integer, primary_key = True)item = db.Column(db.String(120), unique=True, index=True)def __init__(self, item):self.item = itemdef __repr__(self):return self.item

這個class的名稱為Configuration,裡面有兩個欄位,一個是id另一個則是item。id欄位的屬性為數字(integer)並且為主鍵。換句話說,每當使用者新增一筆紀錄時,就會自動產生一個數字id存在id欄位中。另一個item欄位,是主要的欄位。這個欄位就是我們想要儲存資料的地方,將會透過使用者操作表單來輸入資料。此外,我們透過__tablename__命名這個資料表為configurations( __tablename__ = ‘configurations’ )

從def __init__這裏可以知道,使用者只需要輸入一個item參數。

def __init__(self, item):
self.item = item

透過def __repr__就會返回item值。

def __repr__(self):
return self.item

以下是資料庫Model與__init__之間的相連關係的視覺化圖示:

CRUD

建立資料庫的程式寫完之後,我們要來寫管理資料庫資料的CRUD功能。

輸入資料庫的Create Views

首先製作建立資料的View。我們命名為add_items_db。路由也是/add_items_db,這樣子比較不會忘記。使用form = AddItemForm()作為表單。AddItemForm的class則寫在forms.py裡面。

接著,我們若收到表單資料的條件符合,就進行程式處理,反之則回覆add_items_form.html樣板。邏輯如下:

form = AddItemForm()    # 若條件符合    if form.validate_on_submit():    #執行程式...    #反之return render_template('add_items_form.html',form=form)

那當條件符合時,我們要執行哪些程式?

我們可以比對 使用者輸入的資料,與現有資料庫中儲存的資料,是否有重複?如果重複了,就不需要在收錄到資料庫中,反之若是從來都沒有輸入過的資料,就可以收錄儲存在資料庫裡面。

input_item = form.item.dataitem_in_db = Configuration.query.filter_by(item = input_item).first()if item_in_db:    flash('您填寫資料的資料已經存在!!!')else:    configuration = Configuration(item=input_item)    db.session.add(configuration)    db.session.commit()    flash('感謝填寫資料!!!!')    return redirect(url_for('home'))

上面的程式碼就是依照這樣一個故事線來設計的。變數 input_item為使用者透過表單輸入的值,而

item_in_db則是資料庫中已收錄的值。因此,我們透過if判斷式來確認只有當資料庫尚未收錄時,才會執行寫入資料庫的程序。db.session.add與db.session.commit就是資料庫寫入的方式,configuration則是我們寫入的內容,要將input_item寫入資料庫的item欄位中。

寫完之後,整個網頁就會(redirect)轉到home頁面,也就是首頁。下面是add_items_db的完整程式碼:

@app.route('/add_items_db',methods=['GET','POST'])def add_items_db():
"""加入 DB"""
form = AddItemForm()
if form.validate_on_submit():
#item
input_item = form.item.data
item_in_db = Configuration.query.filter_by(item = input_item).first()
if item_in_db:
flash('您填寫資料的資料已經存在!!!')
else: configuration = Configuration(item=input_item)
db.session.add(configuration)
db.session.commit()
flash('感謝填寫資料!!!!')
return redirect(url_for('home'))
return render_template('add_items_form.html',form=form)

forms.py與views.py的關係整理如下:

新增項目的頁面完成後,即顯示如下的畫面:

顯示資料的Read View

顯示資料列表是每個網站幾乎會存在的功能。我們在首頁先製作一個基礎的顯示功能,後續再把篩選與查詢的功能加上。

製作顯示資料列表的 View。我們命名為home,路由在首頁( / ),這樣子一進入網站就可以看到目前存在資料庫中的內容。在這個View裡面,我們會透過query.filter的方式把資料庫中的資料找出來,並且將找出來的資料(存在config_items變數中)透過home.html樣板顯示出來。

@app.route('/')def home():
"""首頁"""
item_order = Configuration.id.desc()
config_items = Configuration.query.filter(item_order)
return render_template('home.html',config_items=config_items)

在樣板home.html中,我們透過for迴圈的使用把資料逐一的顯示出來。

<ul class="list-group list-group-flush">  {% for config in config_items %}    <li class="list-group-item">#{{config.id}}-{{config.item}}</li>  {% endfor %}</ul>

這部分相對單純,接著我們要設計編輯資料以及刪除資料的功能,編輯與刪除兩個功能有些類似。在列表頁中,我們希望透過提供按鈕或連結讓使用者點選之後,可以啟動「編輯」或者是「刪除」的功能。因此,我們需要調整一下上面for迴圈中的<li> HTML標籤,在標籤裡面增加兩個按鈕,整個計畫如下:

因此,home.html樣板的程式碼修改為:

<ul class="list-group list-group-flush">{% for config in config_items %}<li class="list-group-item">#{{config.id}} - {{config.item}}
<div class="btn-toolbar justify-content-between float-md-right" role="toolbar" aria-label="Toolbar with button groups">
<div class="btn-group" role="group">
<a class="btn btn-outline-success btn-sm" href="{{url_for('update_items_db',id=config.id)}}">編輯</a>
<button type="button" class="btn btn-outline-danger btn-sm"data-toggle="modal" data-target="#del_model-{{config.id}}">刪除</button>
</div>
</li>
<!-- delete box -->
<div class="modal" tabindex="-1" id="del_model-{{config.id}}">
...省略...
</div>
<!--// delete box -->
{% endfor %}
</ul>

由於我們套用了Bootstrap的框架,利用框架中的Button Groups與modal元件製作,程式碼看起來比較複雜。不過,粗體字的部分為前面提到的按鈕部分,這個是這裡的重點。

頁面完成後,顯示如下的畫面:

由於篇幅長,上篇部分在此停筆,後續部分將於下篇說明。

--

--

Sean Yeh
Python Everywhere -from Beginner to Advanced

# Taipei, Internet Digital Advertising,透過寫作讓我們回想過去、理解現在並思考未來。並樂於分享,這才是最大贏家。