1. はじめに(概要)
Python / Flask と PostgreSQL 16 を使用し、CRUD 機能を備えた書籍管理 Web アプリ(book-log-app)を開発しました。
初期状態の app.py 内に HTML を直接記述していた構成から、「保守性の向上」 と 「テスト駆動による品質保証」 を目指してリファクタリングを実施したプロセスの記録です。
2. システム構成と技術スタック
-
バックエンド: Python 3.12 / Flask /
psycopg2-binary/ Poetry -
データベース: PostgreSQL 16 (
RealDictCursorによる辞書形式データ取得) -
フロントエンド: Jinja2 テンプレート / 外部 CSS (
static/css/style.css) -
テスト / CI/CD:
pytest/ GitHub Actions (.github/workflows/test.yml)
3. リファクタリングのポイントとトラブルシューティング
① HTML/CSS の分離(Jinja2 & 静的ファイル化)
課題
app.py 内の Flask ルート関数内に直接 HTML 文字列を記述していたため、デザインの変更やロジックの可読性が著しく低下していました。
解決策
- プロジェクト直下に
templates/フォルダを作成し、index.htmlおよびedit.htmlにレンダリングを分離。 -
static/css/style.cssを作成し、Jinja2 のurl_for関数を用いて外部 CSS を読み込み。
<!-- templates/index.html 内での外部CSS読み込み -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
つまずいたポイントと学び
-
TemplateNotFoundエラー: フォルダ名をtemplate(単数形)にしてしまっていたため発生。Flask の標準規約通りtemplates(複数形) へ修正して解消。 -
HTML コメントの構文エラー: HTML コメントで
< !--のように<と!の間にスペースが入ってしまい、ブラウザがコメントと認識せず画面上にそのまま文字列が露出。正しい記法<!-- ... -->に修正。 -
ブラウザキャッシュの挙動: CSS を更新した際、Flask / ブラウザ側で強烈に静的ファイルがキャッシュされるため、検証時は強力再読み込み(
Cmd + Shift + R)を徹底する必要性を実感。
② UI 表示の工夫(数値データを星マーク ★ に変換)
設計方針
データベース側は集計や平均評価の計算を考慮して INTEGER(1〜5)で保持し、画面表示時のみ Jinja2 の条件分岐で ★ マークへ動的変換します。
<!-- templates/index.html 評価の星マーク表示 -->
<td>
{% if book.rating == 5 %}<span class="star-filled">★★★★★</span>
{% elif book.rating == 4 %}<span class="star-filled">★★★★</span><span class="star-empty">☆</span>
{% elif book.rating == 3 %}<span class="star-filled">★★★</span><span class="star-empty">☆☆</span>
{% elif book.rating == 2 %}<span class="star-filled">★★</span><span class="star-empty">☆☆☆</span>
{% elif book.rating == 1 %}<span class="star-filled">★</span><span class="star-empty">☆☆☆☆</span>
{% else %}<span class="star-none">-</span>
{% endif %}
</td>
/* static/css/style.css */
.star-filled {
color: #f39c12; /* オレンジ色 */
}
.star-empty {
color: #ccc; /* グレー */
}
.star-none {
color: #999;
}
③ あいまい検索・ステータス絞り込みの実装
タイトルや著者名に対する大文字・小文字を区別しない曖昧検索を実現するため、PostgreSQL の ILIKE 演算子を採用しました。WHERE 1=1 をベースに検索パラメータに応じて動的にクエリを構築しています。
# app.py
@app.route("/")
def index():
keyword = request.args.get("keyword", "").strip()
status = request.args.get("status", "").strip()
conn = get_db_connection()
cur = conn.cursor()
sql = "SELECT * FROM books WHERE 1=1"
params = []
# キーワード検索(タイトルまたは著者名にマッチ)
if keyword:
sql += " AND (title ILIKE %s OR author ILIKE %s)"
params.extend([f"%{keyword}%", f"%{keyword}%"])
# ステータス絞り込み
if status:
sql += " AND status = %s"
params.append(status)
sql += " ORDER BY id DESC;"
cur.execute(sql, params)
books = cur.fetchall()
cur.close()
conn.close()
return render_template("index.html", books=books, keyword=keyword, status=status)
4. 自動テスト(pytest)の追従と修復
ルーティングの変更や POST 先の変更に伴い、既存のテストコード(test_app.py)でアサーションエラーや 405 Method Not Allowed が発生しました。テストコードを最新のアプリケーション構造に同期させました。
発生したテストエラーと対処
test_index_page失敗
- 原因: レスポンスに含まれる期待文字列が旧バージョンのタイトルのままだった。
-
修正: アサーション対象を新 UI の表示文字
書籍管理アプリに更新。
- **
test_add_bookで405 METHOD NOT ALLOWED**
-
原因:
app.py側で新規作成エンドポイントを/から/add(POST)に変更したため、client.post("/")が拒否された。 -
修正: リクエスト先を
client.post("/add", ...)に変更。
検索機能のテストケース追加
# test_app.py
def test_search_books(client):
# テスト用データを3件作成
client.post("/add", data={"title": "Python入門", "author": "山田太郎", "status": "読書中"}, follow_redirects=True)
client.post("/add", data={"title": "Flask Web開発", "author": "佐藤花子", "status": "積読"}, follow_redirects=True)
client.post("/add", data={"title": "SQL実践ガイド", "author": "山田太郎", "status": "読了"}, follow_redirects=True)
# 1. キーワード「Python」で検索 -> 「Python入門」のみヒットすること
response = client.get("/?keyword=Python")
content = response.get_data(as_text=True)
assert "Python入門" in content
assert "Flask Web開発" not in content
# 2. ステータス「積読」で絞り込み -> 「Flask Web開発」のみヒットすること
response = client.get("/?status=積読")
content = response.get_data(as_text=True)
assert "Flask Web開発" in content
assert "Python入門" not in content
最終的に 5 passed(全件合格)を達成し、CI/CD パイプライン(GitHub Actions)上でも問題なくグリーンパスすることを確認しました。
5. まとめ・今後の展望
今回のリファクタリングを通じて、以下の重要性を再確認しました。
- 関心事の分離 (SoC): データ保持(PostgreSQL)、ビジネスロジック(Flask)、画面表示(Jinja2 / CSS)の役割を明確に分けることで、コードの可読性と保守性が大幅に向上する。
-
リファクタリングとテスト: アプリの構造を変えた際は即座に
pytestを実行し、変更に対する影響範囲を素早く検知・修正する開発サイクルが極めて有効である。
今後は、存在しない ID アクセス時の 404 エラーハンドリングの強化や、サーバーサイド・バリデーションのさらなる堅牢化に取り組む予定です。