Pythonで学ぶ 家計簿アプリ開発(第4回)~SQLiteでデータを保存する~

GitHubでつくって学ぶ

はじめに

前回は、フォームから支出データを追加・削除できるようにした。ただし、データの保存先はPythonのリストのままだったため、アプリを再起動すると内容が消えてしまうという課題が残っていた。

今回は、この課題をSQLiteというデータベースを使って解決する。データベースにデータを保存することで、アプリを再起動しても、追加・削除した内容がそのまま残るようになる。

今回やること

  • SQLiteとは何かを知る
  • テーブルを作成し、これまでの固定データを初期データとして登録する
  • 支出の一覧表示・追加・削除を、すべてデータベースへの操作に置き換える
  • sqlite3コマンドで、データベースの中身を直接確認する

SQLiteとは

SQLiteは、1つのファイルの中にデータベース全体が収められる、軽量なデータベースである。MySQLやPostgreSQLのように、別途データベース専用のサーバーを立てる必要が無く、Pythonの標準ライブラリsqlite3だけで扱える。

そのため、今回のような小規模な学習用アプリや、個人利用のツールには特に向いている。データはすべてdatabase.dbという1つのファイルに保存される。

コード解説

app.py

import sqlite3

from flask import Flask, render_template, request, redirect, url_for

app = Flask(__name__)
DATABASE = "database.db"


def get_db():
    db = sqlite3.connect(DATABASE)
    db.row_factory = sqlite3.Row
    return db


def init_db():
    db = get_db()
    db.execute("""
        CREATE TABLE IF NOT EXISTS expenses (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            date TEXT NOT NULL,
            category TEXT NOT NULL,
            item TEXT NOT NULL,
            amount INTEGER NOT NULL
        )
    """)
    count = db.execute("SELECT COUNT(*) FROM expenses").fetchone()[0]
    if count == 0:
        db.executemany(
            "INSERT INTO expenses (date, category, item, amount) VALUES (?, ?, ?, ?)",
            [
                ("2026-08-01", "食費", "スーパー", 3200),
                ("2026-08-03", "交通費", "電車", 480),
                ("2026-08-05", "娯楽費", "映画", 1800),
            ],
        )
        db.commit()
    db.close()


@app.route("/")
def index():
    db = get_db()
    expenses = db.execute("SELECT * FROM expenses ORDER BY date").fetchall()
    db.close()
    return render_template("index.html", expenses=expenses)


@app.route("/add", methods=["POST"])
def add():
    db = get_db()
    db.execute(
        "INSERT INTO expenses (date, category, item, amount) VALUES (?, ?, ?, ?)",
        (
            request.form["date"],
            request.form["category"],
            request.form["item"],
            int(request.form["amount"]),
        ),
    )
    db.commit()
    db.close()
    return redirect(url_for("index"))


@app.route("/delete/<int:expense_id>", methods=["POST"])
def delete(expense_id):
    db = get_db()
    db.execute("DELETE FROM expenses WHERE id = ?", (expense_id,))
    db.commit()
    db.close()
    return redirect(url_for("index"))


init_db()

前回までと違うポイントを説明する。

  • get_db():データベースファイル(database.db)への接続を作る関数。row_factory = sqlite3.Rowを指定しておくと、取得したデータをexpense["date"]のように、辞書に近い形で扱えるようになる
  • init_db()expensesテーブルが無ければ作成し、データが1件も無い場合だけ、初期データを3件登録する。この関数はアプリ起動時に一度だけ実行される
  • id INTEGER PRIMARY KEY AUTOINCREMENT:1件ごとに自動で割り振られる、重複しない番号。前回は「何番目のデータか」で削除対象を指定していたが、今回からはこのidを使って指定する
  • db.execute("INSERT INTO ...", (値1, 値2, ...)):SQL文の中に直接値を埋め込まず、?のプレースホルダに値を渡す形にしている。こうすることで、SQLインジェクションという攻撃を防げる
  • db.commit():データの追加・削除といった変更を、実際にデータベースへ反映させる。これを呼ばないと、変更が保存されない

templates/index.html(抜粋)

<form action="{{ url_for('delete', expense_id=expense.id) }}" method="post">
  <button type="submit">削除</button>
</form>

削除ボタンのURLが、前回のloop.index0(何番目か)から、expense.id(データベース上のID)に変わっている。sqlite3.Rowのおかげで、expense.idexpense.dateのように、辞書のような書き方でアクセスできる。

動作確認

flask --app app run --debugで起動すると、初回のみdatabase.dbが新しく作られ、初期データ3件が登録される。ブラウザで一覧を確認し、前回と同じように支出の追加・削除ができることを確かめる。

ここからが今回の本題である。一度アプリを再起動(ターミナルでCtrl+Cのあと、再度flask --app app run --debug)してみてほしい。追加・削除した内容が、今回は消えずにそのまま残っているはずである。

データベースの中身を直接見る

ブラウザ経由でなく、データベースの中身を直接のぞくこともできる。ターミナルで以下を実行する。

sqlite3 database.db

sqlite3というプロンプトに切り替わるので、以下のコマンドを打ってみる。

.tables
SELECT * FROM expenses;

.tablesでテーブルの一覧、SELECT * FROM expenses;で登録されている全データが表示される。
終了するときは.quitとタイプする。

ここに注意しよう!

sqlite3コマンドは、指定したファイルが無ければ空の新しいファイルを作ってしまうため、間違った場所で実行すると「空っぽのデータベース」を開いてしまう。

実行する前に以下を確認しよう。

  1. pwdで現在の場所を確認する。/workspaces/pc-labo-python-learning/04_sqliteになっているか
  2. ls database.dbで、そのファイルが存在し、サイズが0バイトでないか確認する(ls -la database.dbだとサイズも見える)

04_sqliteフォルダの外(リポジトリのルートなど)で実行してしまうと、そこに新しい空のdatabase.dbができてしまうので、必ずcd 04_sqliteをしてから実行するのがポイントです。

今回のまとめ

  • SQLiteは、1ファイルで完結する軽量なデータベースで、Pythonの標準ライブラリsqlite3だけで扱える
  • ?のプレースホルダを使ってSQL文に値を渡すことで、SQLインジェクションを防げる
  • idのような一意な番号を使うことで、「何番目か」ではなく「どのデータか」を正確に指定できる
  • sqlite3コマンドを使うと、アプリを介さずデータベースの中身を直接確認できる

次回予告

次回は、これまで蓄積してきた支出データを、月別・カテゴリ別に集計し、グラフとして可視化する。家計簿アプリらしい機能に、いよいよ着手する。

ソースコード

GitHub:pc-labo-python-learning/04_sqlite

コメント

タイトルとURLをコピーしました