PythonのSQLAlchemyとSQLiteで、支出を登録して月ごとの一覧とカテゴリ別の合計を確認できる家計簿アプリを作りました。Flaskの画面から支出を入れると、月次一覧と集計がその場で更新されます。ORMでSQLを直接書かずに登録と集計を組み立てる流れを、初心者が追体験できるように一つずつ解説します。
SQLAlchemyの基本概念、要件定義、実装、動作確認までを順番に学べる構成です。動画は目次から確認したい場面へ移動でき、本文だけでも手順と考え方が完結します。
動画の内容をテキストで確認する
オープニング。SQLAlchemyとSQLiteを使って支出集計家計簿アプリを作るカリキュラムを始めます。概要紹介。
SQLAlchemyとSQLiteの役割と使い方を学ぶ 支出集計家計簿アプリの要件を整理する 完成コードと実行結果を確認する 最後に完成した画面を実際に操作して確かめる 具体的にやること。
支出の日付とカテゴリと金額とメモを入力するフォームを表示する フォーム送信を受け取りSQLiteへ支出を保存する 対象月を選んで月次の支出一覧を表示する 支出一覧を日付の新しい順に並べる カテゴリ別の合計金額を集計して表示する 実装環境・必須アプリ。
OS:Windows 11 Pro Python:3.13.3シェル:PowerShell 5.1必須アプリ:コードエディター、Webブラウザ、ターミナル パッケージ:pip、Flask、SQLAlchemy、pysqlite3-binary PythonのORMライブラリ『SQLAlchemy』とは。
SQLでなくPythonのコードでデータを扱うORM:SQLAlchemyは、PythonのコードからデータベースをSQLなしで操作できるORMライブラリ SQLAlchemyとFlaskで作る家計簿アプリの要点。
トップページで対象月の一覧と集計を返す 登録用ルートでフォーム送信を受け取るrequestで日付やカテゴリなどの入力値を読む 対象月の支出一覧を新しい順で返す カテゴリごとの合計金額を降順で返す 対象月の合計金額を1つの数値で返す 支出集計家計簿アプリの要件定義。
支出を登録すると一覧に新しい行が増える 登録した支出のカテゴリが集計表へ加算される 対象月を切り替えると該当月の一覧だけが表示される 月合計が一覧の金額の総和と一致する 金額欄を空で送っても0円として登録される カテゴリ欄を空で送ると未分類として登録される INTRO: Monaco Editorで支出集計家計簿アプリを実装。
コードを1行ずつ入力し、補完と自動インデントを使いながら実行結果を確認します。LINE 001: モジュール概要の開始。このファイル全体がSQLAlchemyとSQLiteを使った家計簿アプリであることを示す説明文の始まりです。
ドキュメント文字列としてプログラムの目的を記録しています。LINE 003: 提供機能の説明。支出登録・カテゴリ集計・月次表示という3つの主要機能をFlaskとSQLAlchemyとSQLiteで実装することを説明しています。
読み手がこの後のコードの全体像をつかめるようにしています。LINE 004: モジュール説明の終了。ファイル冒頭のドキュメント文字列を閉じる記号です。
ここまでがアプリ全体の概要説明になります。LINE 006: 日付操作モジュールの読み込み。日付や日数計算を扱うdatetimeモジュールをdtという名前で読み込んでいます。
今日の日付取得や日数差の計算に使われます。LINE 008: Flask関連機能の読み込み。Webアプリを作るFlaskクラスや、リダイレクト、テンプレート描画、リクエスト取得、URL生成に必要な機能をまとめて読み込んでいます。
これらは画面表示やフォーム処理に使われます。LINE 009: SQLAlchemy読み込みの開始。データベース操作に必要な部品をsqlalchemyパッケージからまとめて読み込む処理の始まりです。
複数行にわたって必要な要素を列挙しています。LINE 010: カラム定義用クラスの読み込み。テーブルの列を定義するためのColumnクラスを読み込んでいます。
この後のExpenseクラスで各項目の定義に使われます。LINE 011: 日付型の読み込み。日付データを扱うためのDate型を読み込んでいます。
支出日を保存する列の型として使われます。LINE 012: 整数型の読み込み。整数データを扱うためのInteger型を読み込んでいます。
金額やID列の型として使われます。LINE 013: 文字列型の読み込み。文字列データを扱うためのString型を読み込んでいます。
カテゴリ名やメモの列の型として使われます。LINE 014: データベース接続関数の読み込み。データベースへの接続設定を作るcreate_engine関数を読み込んでいます。
SQLiteファイルへの接続に使われます。LINE 015: 日付要素抽出関数の読み込み。日付から年や月といった要素を取り出すextract関数を読み込んでいます。
月次表示の絞り込み条件を作る際に使われます。LINE 016: 集計関数の読み込み。合計値などを計算するfunc関数を読み込んでいます。
カテゴリごとの合計金額や月合計の計算に使われます。LINE 017: SQLAlchemy読み込みの終了。複数行にわたるsqlalchemyからの読み込み処理を閉じる記号です。
ここまでで必要な部品がすべて揃います。LINE 018: ORM基盤機能の読み込み。モデルの土台になるdeclarative_baseと、データベースとのやり取りを行うセッションを作るsessionmakerを読み込んでいます。
これらはテーブル定義とデータ操作の両方で使われます。LINE 022: モデルの土台作成。ORMモデルの基底クラスを作成し、Baseという名前で保持しています。
このあと定義するExpenseクラスはこのBaseを継承します。LINE 023: データベース接続設定の開始。SQLiteデータベースへの接続を作るcreate_engineの呼び出しを開始しています。
接続先や設定を複数行にわたって指定します。LINE 024: 接続先ファイルの指定。kakeibo.dbという名前のSQLiteファイルを接続先として指定しています。
このファイルに支出データが保存されます。LINE 025: スレッド制限の解除設定。同一スレッド以外からの接続を許可する設定を指定しています。
Flaskがリクエストごとに異なるスレッドで動く場合に必要な設定です。LINE 026: 接続設定の終了。create_engineの呼び出しを閉じる記号です。
ここまでの設定でengineという接続オブジェクトが作られます。LINE 027: セッション生成器の作成。engineに紐づいたデータベースセッションを作るためのSessionLocalを用意しています。
この後の各処理でデータベースとやり取りする際に使われます。RUN 1/7: データベース接続を確認する。create_engineで作ったエンジンが、SQLiteのkakeibo.dbへ接続する設定になっているかを確認します。
CHECK 1/7: 途中実行に成功。接続先データベース: sqlite:///kakeibo.db 方言: sqlite RETURN 01: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 031: 支出テーブルモデルの定義開始。支出データを表すExpenseクラスを定義し始めています。Baseを継承することでこのクラスがデータベースのテーブルに対応します。
LINE 032: テーブル名の指定。このクラスに対応するデータベース上のテーブル名をexpensesと指定しています。実際のSQLiteファイル内にこの名前でテーブルが作られます。
LINE 034: 主キー列の定義。各支出データを一意に識別するID列を整数型の主キーとして定義しています。新しいデータを登録するたびに自動で番号が割り当てられます。
LINE 035: 支出日列の定義。支出が発生した日付を保存する列を定義しています。nullable=Falseにより必ず値を入れる必要があります。
LINE 036: カテゴリ列の定義。食費や交通費などの分類名を保存する列を最大40文字の文字列として定義しています。必須項目として設定されています。
LINE 037: 金額列の定義。支出の金額を保存する整数型の列を定義しています。必ず値が入るよう必須項目にしています。
LINE 038: メモ列の定義。支出に関する補足メモを保存する列を最大200文字の文字列として定義しています。値がない場合は空文字が初期値として使われます。
RUN 2/7: 支出テーブルの列を確認する。Expenseクラスとして定義した支出テーブルの名前と、各列の名前がそろっているかを確認します。CHECK 2/7: 途中実行に成功。
テーブル名: expenses カラム名: ['id', 'spent_on', 'category', 'amount', 'memo'] RETURN 02: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 042: 初期化関数の定義開始。
テーブル作成とサンプルデータ投入を行うinit_db関数を定義し始めています。アプリ起動時に呼び出されて準備を整えます。LINE 043: テーブルの作成。
定義したモデルに基づいて、まだ存在しないテーブルをデータベース内に作成しています。すでに存在する場合は何もしません。LINE 044: セッションの開始。
データベースとやり取りするためのセッションを新しく作成しています。この後のクエリや登録処理はこのセッションを通じて行われます。LINE 045: 例外処理の開始。
データベース処理中にエラーが起きても後始末を確実に行うため、try文を開始しています。LINE 046: データ件数の確認。Expenseテーブルに登録済みのデータが0件かどうかを確認しています。
初回起動時だけサンプルデータを追加するための条件分岐です。LINE 047: 今日の日付の取得。現在の日付を取得してtoday変数に保存しています。
サンプルデータの日付計算の基準として使われます。LINE 048: サンプルデータ追加の開始。複数の支出データをまとめてセッションに追加するadd_allの呼び出しを開始しています。
リストの形で複数件のExpenseを指定します。LINE 049: サンプルデータリストの開始。add_allに渡す支出データのリストを開始しています。
以降の行で個々のExpenseオブジェクトを列挙します。LINE 050: 食費サンプルの作成。今日の日付で食費1200円、メモ「ランチ」の支出データを1件作成しています。
初回起動時の表示確認用サンプルです。LINE 051: 交通費サンプルの作成。今日の日付で交通費400円、メモ「電車代」の支出データを1件作成しています。
カテゴリ集計の動作確認にも使われます。LINE 052: 日用品サンプルの作成開始。昨日の日付を使う日用品のサンプルデータ作成を複数行にわたって開始しています。
LINE 053: 日用品の日付指定。今日から1日前の日付を計算し、日用品サンプルの支出日として指定しています。LINE 054: 日用品のカテゴリ指定。
このサンプルデータのカテゴリを日用品と指定しています。LINE 055: 日用品の金額指定。このサンプルデータの金額を880円と指定しています。
LINE 056: 日用品のメモ指定。このサンプルデータのメモを「洗剤」と指定し、日用品サンプルの作成を締めくくっています。LINE 057: 日用品サンプルの作成終了。
日用品カテゴリのExpenseオブジェクト作成を閉じる記号です。LINE 058: 食費サンプル2件目の作成開始。一昨日の日付を使う食費のサンプルデータ作成を複数行にわたって開始しています。
LINE 059: 食費2件目の日付指定。今日から2日前の日付を計算し、このサンプルデータの支出日として指定しています。LINE 060: 食費2件目のカテゴリ指定。
このサンプルデータのカテゴリを食費と指定しています。LINE 061: 食費2件目の金額指定。このサンプルデータの金額を650円と指定しています。
LINE 062: 食費2件目のメモ指定。このサンプルデータのメモを「コーヒー豆」と指定しています。LINE 063: 食費サンプル2件目の作成終了。
2件目の食費カテゴリのExpenseオブジェクト作成を閉じる記号です。LINE 064: 家賃サンプルの作成開始。33日前の日付を使う家賃のサンプルデータ作成を複数行にわたって開始しています。
月をまたぐ月次表示の確認に使われます。LINE 065: 家賃の日付指定。今日から33日前の日付を計算し、家賃サンプルの支出日として指定しています。
先月分の支出として月次集計のテストに使われます。LINE 066: 家賃のカテゴリ指定。このサンプルデータのカテゴリを家賃と指定しています。
LINE 067: 家賃の金額指定。このサンプルデータの金額を60000円と指定しています。LINE 068: 家賃のメモ指定。
このサンプルデータのメモを「先月分」と指定しています。LINE 069: 家賃サンプルの作成終了。家賃カテゴリのExpenseオブジェクト作成を閉じる記号です。
LINE 070: サンプルデータリストの終了。add_allに渡す支出データのリストをここで閉じています。合計5件のサンプルデータが用意されたことになります。
LINE 071: データ追加処理の終了。session.add_allの呼び出しを閉じる記号です。5件のサンプルデータがセッションに追加された状態になります。
LINE 072: データベースへの反映。セッションに追加したサンプルデータをデータベースに書き込み、確定させています。この処理を行わないとデータは実際には保存されません。
LINE 073: 後始末処理の開始。エラーの有無にかかわらず必ず実行される後始末処理を開始しています。ここでセッションを閉じる処理を行います。
LINE 074: セッションのクローズ。使い終えたデータベースセッションを閉じ、接続資源を解放しています。処理の最後に必ず実行される安全な後始末です。
RUN 3/7: 初期サンプルの投入を確認する。init_dbを実行して、テーブルの作成と初期サンプルの支出5件が登録される様子を確認します。CHECK 3/7: 途中実行に成功。
初期サンプルの件数: 5 RETURN 03: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 078: 月次データ取得関数の定義開始。
指定した年月の支出一覧・カテゴリ集計・月合計をまとめて取得するfetch_month関数を定義し始めています。セッションと年月を引数として受け取ります。LINE 079: 絞り込み条件の作成開始。
指定した年月に一致する支出だけを取り出すための条件をタプルとしてまとめる処理を開始しています。この条件は後続のクエリで共通して使われます。LINE 080: 年の一致条件の指定。
支出日から年を取り出し、指定された年と一致するかどうかを判定する条件を作成しています。月次表示の絞り込みの一つとして使われます。LINE 081: 月条件の指定。
extract関数で支出日から月を取り出し、指定された月と一致する条件をタプルの2つ目の要素として作成しています。年の条件と組み合わせて、後続のクエリで使う絞り込み条件periodを完成させます。LINE 082: 条件タプルの終了。
periodというタプルの定義を閉じています。ここで作られた年と月の2つの条件が、以降のfilterで一括して使われます。LINE 083: 支出一覧クエリの開始。
expensesという変数に、支出データの一覧を取得するクエリの結果を代入する処理の始まりです。括弧内でメソッドを連ねて条件や並び順を指定していきます。LINE 084: Expenseテーブルの問い合わせ開始。
session.queryでExpenseテーブルに対する問い合わせを開始しています。ここから続くメソッドチェーンで絞り込みや並び替えを行います。LINE 085: 年月による絞り込み。
先ほど作成したperiodのアンパックした条件を使って、指定した年と月に該当する支出だけに絞り込んでいます。LINE 086: 日付とIDによる並び替え。支出日の新しい順、同じ日付の場合はIDの大きい順に並び替えています。
これにより最新の支出が一覧の上に表示されます。LINE 087: クエリ結果の取得。all()を呼び出して、条件と並び順に合致する支出データをすべてリストとして取得しています。
LINE 088: 支出一覧クエリの終了。expensesへの代入式を閉じる括弧です。ここまでの処理で完成した支出一覧がexpensesに格納されます。
LINE 089: カテゴリ集計クエリの開始。summaryという変数に、カテゴリごとの合計金額を集計するクエリの結果を代入する処理の始まりです。LINE 090: カテゴリと合計金額の取得。
Expense.categoryとfunc.sumで金額の合計を計算し、totalという名前を付けて取得対象に指定しています。LINE 091: 年月による絞り込み。支出一覧と同じperiod条件を使って、対象月のデータだけに絞り込んでいます。
LINE 092: カテゴリでグループ化。Expense.categoryを基準にデータをグループ化し、同じカテゴリの支出をまとめて集計できるようにしています。LINE 093: 合計金額の降順で並び替え。
カテゴリごとの合計金額が大きい順に並ぶように指定しています。支出の多いカテゴリが上に表示されます。LINE 094: 集計結果の取得。
all()を呼び出して、グループ化と並び替えを適用したカテゴリ別集計の結果をリストとして取得しています。LINE 095: カテゴリ集計クエリの終了。summaryへの代入式を閉じる括弧です。
ここまでの処理で完成したカテゴリ別集計結果がsummaryに格納されます。LINE 096: 月合計金額の計算。対象月の支出金額をすべて合計し、データが存在しない場合は0になるようにorで補っています。
この値がtotalに格納されます。LINE 097: 結果のまとめて返却。支出一覧、カテゴリ集計、月合計の3つの値をまとめて呼び出し元に返しています。
この関数の処理結果がここで確定します。RUN 4/7: 今月分の取得結果を確認する。fetch_monthを呼び出し、今月の支出一覧の件数と合計金額が取り出せるかを確認します。
CHECK 4/7: 途中実行に成功。今月の一覧件数: 4 今月の合計: 3130 RETURN 04: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 101: HTMLテンプレートの開始。画面全体のHTMLを文字列として定義するTEMPLATE変数の宣言です。doctype宣言によりHTML5であることを示しています。
LINE 102: html要素の開始。ページ全体を囲むhtml要素を開始し、lang属性で日本語のページであることを指定しています。LINE 103: head要素の開始。
ページのメタ情報やタイトル、スタイルを記述するhead要素を開始しています。LINE 104: 文字コードの指定。meta要素で文字コードをutf-8に指定し、日本語などの文字が正しく表示されるようにしています。
LINE 105: ページタイトルの設定。ブラウザのタブなどに表示されるページタイトルを「家計簿アプリ」に設定しています。LINE 106: スタイル定義の開始。
ページの見た目を整えるCSSを記述するstyle要素を開始しています。LINE 107: 全体のスタイル設定。body要素にフォントや最大幅、余白、文字色などの基本的なスタイルを設定し、読みやすいページの見た目を整えています。
LINE 108: 見出し1のスタイル設定。h1要素の文字サイズを1.4remに指定し、大きめの見出しとして目立たせています。LINE 109: 見出し2のスタイル設定。
h2要素の文字サイズを1.1remに指定し、h1よりやや小さいセクション見出しとして表示させています。LINE 110: セクションのスタイル設定。section要素に余白や枠線、角丸を設定し、支出登録や月次表示などの区切りを分かりやすくしています。
LINE 111: ラベルのスタイル設定。label要素をinline-blockにして余白を設定し、フォーム内の各項目が見やすく並ぶようにしています。LINE 112: 入力欄のスタイル設定。
input要素に内側の余白を設定し、フォームの入力欄を押しやすく見やすい大きさに調整しています。LINE 113: ボタンのスタイル設定。button要素に背景色や文字色、角丸、カーソル形状を設定し、クリックできるボタンらしい見た目にしています。
LINE 114: テーブルのスタイル設定。table要素の幅を100%にし、罫線を隣接するセルと共有させるcollapse指定と上部の余白を設定しています。LINE 115: セルのスタイル設定。
th要素とtd要素に下線と内側の余白、左揃えの文字配置を設定し、表の各セルを見やすくしています。LINE 116: 金額列のスタイル設定。amountクラスを指定した要素の文字を右揃えにし、金額の数字が読みやすく並ぶようにしています。
LINE 117: 合計表示のスタイル設定。totalクラスを指定した要素の文字を太字にし、月合計の金額が目立つようにしています。LINE 118: スタイル定義の終了。
style要素の閉じタグで、ここまでのCSS定義を終了しています。LINE 119: head要素の終了。head要素の閉じタグで、メタ情報やタイトル、スタイルの記述を終了しています。
LINE 120: body要素の開始。画面に実際に表示される内容を記述するbody要素を開始しています。LINE 121: ページ見出しの表示。
h1要素でアプリのタイトル「SQLAlchemy × SQLite家計簿アプリ」を画面上部に表示しています。LINE 123: 支出登録セクションの開始。支出登録フォームをまとめるsection要素を開始しています。
LINE 124: 支出登録の見出し。h2要素で「支出登録」という見出しを表示し、このセクションの役割を分かりやすく示しています。LINE 125: 支出登録フォームの開始。
form要素でPOSTメソッドを指定し、送信先を/addに設定した支出登録フォームを開始しています。LINE 126: 日付入力欄の表示。日付を選択するinput要素を表示し、初期値としてtoday変数に入った今日の日付を設定しています。
LINE 127: カテゴリ入力欄の表示。カテゴリを入力するテキスト入力欄を表示し、placeholderで「食費」などの入力例を示しています。LINE 128: 金額入力欄の表示。
金額を入力する数値入力欄を表示し、placeholderで「1200」という入力例を示しています。LINE 129: メモ入力欄の表示。メモを入力するテキスト入力欄を表示し、placeholderで「ランチ」などの入力例を示しています。
LINE 130: 登録ボタンの表示。submit型のボタンを表示し、クリックするとフォームの内容が/addへ送信されるようにしています。LINE 131: 支出登録フォームの終了。
form要素の閉じタグで、支出登録フォームの記述を終了しています。LINE 132: 支出登録セクションの終了。section要素の閉じタグで、支出登録に関する部分の記述を終了しています。
LINE 134: 月次表示セクションの開始。月次の支出一覧やカテゴリ集計を表示するsection要素を開始しています。LINE 135: 月次表示の見出し。
h2要素で「月次表示」という見出しを表示し、このセクションの役割を分かりやすく示しています。LINE 136: 対象月選択フォームの開始。form要素でGETメソッドを指定し、送信先を/に設定した対象月選択用のフォームを開始しています。
LINE 137: 対象月入力欄の表示。月を選択するinput要素を表示し、現在表示中の月をmonth変数から初期値として設定しています。LINE 138: 表示ボタンの表示。
submit型のボタンを表示し、クリックすると選択した月のデータを表示するよう画面が再読み込みされます。LINE 139: 対象月選択フォームの終了。form要素の閉じタグで、対象月選択用のフォームの記述を終了しています。
LINE 140: 月合計金額の表示。p要素で対象月の名称と合計金額をtotal変数から表示し、その月にいくら使ったかがひと目でわかるようにしています。LINE 141: 支出一覧テーブルの開始。
支出データを一覧表示するtable要素を開始しています。LINE 142: テーブル見出し行の表示。thead要素の中で「日付」「カテゴリ」「金額」「メモ」という各列の見出しを表示しています。
LINE 143: テーブル本体の開始。実際の支出データを行として表示するtbody要素を開始しています。LINE 144: 支出データの繰り返し開始。
Jinja2のforループでexpensesに含まれる各支出データを1件ずつ取り出し、以降の行を繰り返し出力しています。LINE 145: 支出行の開始。1件の支出データを表す表の行tr要素を開始しています。
LINE 146: 支出日の表示。各支出のspent_onをisoformat()で文字列に変換して、日付をtd要素に表示しています。LINE 147: カテゴリの表示。
各支出のcategoryをtd要素に表示し、その支出がどのカテゴリに属するかを示しています。LINE 148: 金額の表示。各支出のamountをtd要素に表示し、amountクラスによって金額が右揃えで見やすくなるようにしています。
LINE 149: メモの表示。各支出のmemoをtd要素に表示し、支出内容の補足情報を確認できるようにしています。LINE 150: 支出行の終了タグ。
1件分の支出データを表示する行の終了タグです。日付・カテゴリ・金額・メモを並べた1行分の表示がここで完結します。LINE 151: 支出ループの終了。
expensesリストをループして支出を表示する処理の終了地点です。全件の表示が終わるとこのタグでループを閉じます。LINE 152: 支出一覧テーブル本体の終了。
支出一覧テーブルのtbody要素を閉じるタグです。ここまでに表示した各支出行がテーブルの本体としてまとまります。LINE 153: 支出一覧テーブルの終了。
月次の支出一覧を表示するテーブル全体を閉じるタグです。テーブルの表示範囲がここで終わります。LINE 154: 月次表示セクションの終了。
月次表示に関する入力フォームや一覧表を囲んでいたセクションの終了タグです。ここまでで月次表示部分の画面構成が完了します。LINE 156: カテゴリ集計セクションの開始。
カテゴリごとの支出合計を表示するための新しいセクションを開始するタグです。ここから集計表示のためのHTMLが始まります。LINE 157: カテゴリ集計の見出し。
画面に「カテゴリ集計」という見出しを表示する行です。ユーザーがこの下に何が表示されるかひと目で分かるようにしています。LINE 158: カテゴリ集計テーブルの開始。
カテゴリ別の合計金額を表示するためのテーブルを開始するタグです。idにcategory-summaryを設定し、後で参照しやすくしています。LINE 159: 集計テーブルの見出し行。
カテゴリ集計テーブルの見出し部分で、「カテゴリ」と「合計金額」という2つの列名を表示しています。LINE 160: 集計テーブル本体の開始。カテゴリ集計の各行を格納するtbody要素の開始タグです。
ここから実際の集計データが行として並びます。LINE 161: カテゴリ集計のループ開始。summaryに含まれるカテゴリと合計金額のペアを1件ずつ取り出して繰り返し処理するループの開始です。
LINE 162: 集計行の開始タグ。1つのカテゴリに対する集計結果を表示する行の開始タグです。この中にカテゴリ名と合計金額を並べます。
LINE 163: カテゴリ名の表示。ループ変数catの値を使って、そのカテゴリの名前をセルに表示しています。LINE 164: カテゴリ合計金額の表示。
ループ変数subtotalの値を使って、そのカテゴリの合計金額を「円」付きで表示しています。LINE 165: 集計行の終了タグ。1つのカテゴリ分の集計行の終了タグです。
カテゴリ名と合計金額の表示がここで完結します。LINE 166: カテゴリ集計ループの終了。summaryをループして各カテゴリの集計を表示する処理の終了地点です。
全カテゴリの表示が終わるとここでループを閉じます。LINE 167: 集計テーブル本体の終了。カテゴリ集計テーブルのtbody要素を閉じるタグです。
ここまでに表示した各カテゴリの行がテーブル本体としてまとまります。LINE 168: 集計テーブルの終了。カテゴリ集計を表示するテーブル全体を閉じるタグです。
テーブルの表示範囲がここで終わります。LINE 169: カテゴリ集計セクションの終了。カテゴリ集計に関する見出しや表を囲んでいたセクションの終了タグです。
ここまでで集計部分の画面構成が完了します。LINE 170: body要素の終了。画面全体の本文部分を囲んでいたbody要素を閉じるタグです。
表示すべき内容がすべてここまでに含まれています。LINE 171: HTML文字列の終了。html要素を閉じ、TEMPLATE変数に格納するHTML文字列全体を終了させています。
ここまでがFlaskで描画するテンプレートの内容です。RUN 5/7: 画面テンプレートを確認する。HTMLテンプレートの中に、支出登録フォームとカテゴリ集計表の目印が含まれているかを確認します。
CHECK 5/7: 途中実行に成功。テンプレートの文字数: 2491 登録フォームを含む: True カテゴリ集計表を含む: True RETURN 05: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。
LINE 175: Flaskアプリの作成。Flaskクラスのインスタンスを作成し、appという変数に代入しています。このappを使ってルーティングやサーバー起動を行います。
LINE 176: ルート一覧の定義。アプリが持つルートのパスをリストとして定義しています。ここではトップページのパスだけを保持しています。
LINE 180: トップページのルート登録。appのrouteデコレータを使い、トップページ「/」へのアクセス時に次の関数を呼び出すよう設定しています。LINE 181: 月次表示関数の定義。
トップページにアクセスしたときに実行されるindex関数を定義しています。この関数の中で月次データの取得と画面表示を行います。LINE 182: 表示対象月の取得。
URLのクエリパラメータからmonthを取得し、指定がなければ今日の年月を初期値として使用しています。LINE 183: 月分割処理の開始。month文字列を年と月に分割する処理を試みるtryブロックの開始です。
エラーが起きた場合に備えています。LINE 184: 年月の分割と変換。month文字列をハイフンで分割し、それぞれ整数に変換してyearとmonに代入しています。
LINE 185: 分割失敗時の例外処理。month文字列の形式が不正で分割に失敗した場合に、この例外処理へ移ります。LINE 186: 現在日付の取得。
月の指定が不正だった場合のフォールバックとして、現在の日付を取得しています。LINE 187: フォールバック値の設定。不正な月指定があった場合に、今日の年・月・表示用文字列で値を上書きし、正しい状態に戻しています。
LINE 188: DBセッションの作成。データベースへ接続するためのセッションを新しく作成しています。以降の問い合わせはこのセッションを通じて行います。
LINE 189: データ取得処理の開始。データベースからデータを取得する処理をtryブロックで囲み、後で確実にセッションを閉じられるようにしています。LINE 190: 月次データの取得。
fetch_month関数を呼び出し、指定した年月の支出一覧・カテゴリ集計・合計金額をまとめて取得しています。LINE 191: テンプレート描画の開始。取得したデータを使ってTEMPLATEをレンダリングする処理を開始しています。
この結果がブラウザへ返される画面になります。LINE 192: 描画対象テンプレートの指定。render_template_stringに渡す最初の引数として、画面全体のHTMLテンプレートを指定しています。
LINE 193: 表示月のテンプレート変数化。現在表示している対象月をテンプレート変数monthとして渡し、画面上部の月選択欄などに反映させています。LINE 194: 今日の日付のテンプレート変数化。
支出登録フォームの日付欄に初期値として使うため、今日の日付をisoformatでテンプレートへ渡しています。LINE 195: 支出一覧のテンプレート変数化。取得した支出データのリストをテンプレート変数expensesとして渡し、一覧表の各行の描画に使います。
LINE 196: カテゴリ集計のテンプレート変数化。取得したカテゴリ別集計データをテンプレート変数summaryとして渡し、集計表の各行の描画に使います。LINE 197: 合計金額のテンプレート変数化。
取得した月合計の金額をテンプレート変数totalとして渡し、画面上部の合計表示に使います。LINE 198: render_template_string呼び出しの終了。render_template_string関数への引数指定を締めくくり、実際にHTMLを生成してレスポンスとして返す準備が整います。
LINE 199: 後片付け処理の開始。tryブロックで例外が発生してもしなくても必ず実行されるfinallyブロックの開始です。セッションの後始末を保証します。
LINE 200: DBセッションのクローズ。使い終わったデータベースセッションを閉じ、接続資源を解放しています。RUN 6/7: 月次表示ページを開く。
テスト用クライアントでトップページを開き、月次表示の画面が正しく返るかを確認します。CHECK 6/7: 途中実行に成功。トップページの状態: 200 見出しを含む: True RETURN 06: エディターへ戻りました。
入力済みのコードを維持したまま、次の実装へ進みます。LINE 204: 支出登録ルートの登録。appのrouteデコレータを使い、「/add」へのPOSTリクエストを受け取ったときにこの後の関数を呼び出すよう設定しています。
LINE 205: 支出登録関数の定義。フォームから送信された支出データを受け取り、データベースへ保存するadd関数を定義しています。LINE 206: 入力日付の取得。
フォームから送信された日付文字列を取得し、未入力の場合は今日の日付を使うようにしています。LINE 207: 日付変換処理の開始。取得した日付文字列をdate型に変換する処理をtryブロックで囲み、変換失敗に備えています。
LINE 208: 日付文字列のdate型変換。ISO形式の日付文字列をdatetime.dateオブジェクトへ変換し、spent_onに格納しています。LINE 209: 日付変換失敗時の例外処理。
日付文字列の形式が不正で変換に失敗した場合に、この例外処理へ移ります。LINE 210: 日付のフォールバック。日付の変換に失敗した場合、今日の日付をspent_onとして使用することで処理を続行できるようにしています。
LINE 211: カテゴリの取得と既定値設定。フォームからカテゴリ名を取得し、前後の空白を取り除いた上で、未入力の場合は「未分類」を設定しています。LINE 212: 金額変換処理の開始。
フォームから受け取った金額文字列を整数に変換する処理をtryブロックで囲み、変換失敗に備えています。LINE 213: 金額の整数変換。フォームから受け取った金額文字列を整数に変換し、未入力の場合は0として扱っています。
LINE 214: 金額変換失敗時の例外処理。金額の文字列が数値として変換できなかった場合に、この例外処理へ移ります。LINE 215: 金額のフォールバック。
金額の変換に失敗した場合、amountを0とすることで登録処理が止まらないようにしています。LINE 216: メモの取得。フォームからメモ欄の入力内容を取得し、前後の空白を取り除いてmemoに格納しています。
LINE 217: DBセッションの作成。支出データを保存するために、新しいデータベースセッションを作成しています。LINE 218: 保存処理の開始。
データベースへの登録処理をtryブロックで囲み、後で確実にセッションを閉じられるようにしています。LINE 219: 支出オブジェクトの追加開始。作成したExpenseオブジェクトをセッションに追加する処理の開始行です。
この時点ではまだデータベースに反映されていません。LINE 220: Expenseオブジェクトの生成。フォームから取得した日付・カテゴリ・金額・メモを使って、新しいExpenseオブジェクトを生成しています。
LINE 221: 追加処理の引数終了。session.addに渡す引数の指定を締めくくり、Expenseオブジェクトをセッションへ追加する処理が完了します。LINE 222: データベースへのコミット。
セッションに追加した支出データを実際にデータベースへ保存するため、コミット処理を実行しています。LINE 223: 保存処理の後片付け開始。tryブロックで例外が発生してもしなくても必ず実行されるfinallyブロックの開始で、セッションの後始末を保証します。
LINE 224: データベースセッションの終了処理。支出登録処理が終わったタイミングでセッションを閉じ、データベースとの接続を解放しています。try-finally構文により、途中でエラーが発生してもこの処理が必ず実行されるようになっています。
LINE 225: 登録後に月次画面へリダイレクト。支出データの保存が完了した後、登録した日付の年月を指定して一覧表示画面へ移動させています。redirectとurl_forを使うことで、登録した支出が反映された最新の月次画面をユーザーに見せる仕組みです。
RUN 7/7: 支出登録の送信を確認する。テスト用クライアントから支出を送信し、登録後に月次表示へ戻るリダイレクトが返るかを確認します。CHECK 7/7: 途中実行に成功。
登録後の状態: 302 戻り先: /?month=2026-07 RETURN 07: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 229: アプリ起動時のデータベース初期化。
モジュールが読み込まれたタイミングでinit_db関数を呼び出し、テーブル作成とサンプルデータの投入を行っています。この処理により、アプリを初めて起動したときでもすぐに動作確認ができる状態になります。LINE 232: スクリプト直接実行時の判定。
このファイルが他のモジュールからインポートされたのではなく、直接実行されたときだけ以降の処理を行うための条件分岐です。Pythonの慣用的な書き方で、モジュールとしての再利用時に不要な処理が実行されるのを防いでいます。LINE 233: Flask開発サーバーの起動。
app.runを呼び出し、Flaskの組み込みサーバーを起動してWebアプリを動かしています。debug=Trueにより、コード変更時の自動再読み込みやエラー画面の詳細表示が有効になります。PREVIEW: 完成した支出集計家計簿アプリを操作。
入力・送信・結果表示を実際のブラウザ画面で確認します。UPLOAD: 実際の入力データでアプリを操作。日付を既定のまま食費の支出を登録すると、月次一覧に新しい行が追加され、カテゴリ集計にも食費の合計が反映されることを確認できました。
確認1/4: 今月の支出一覧を表示する。テスト用クライアントで今月の月次表示を開き、初期サンプルの食費や交通費が一覧に並ぶことを確認します。確認2/4: 支出を登録して一覧に反映する。
登録フォームへ書籍代の支出を送り、その月の一覧と金額に登録内容が反映されることを確認します。確認3/4: カテゴリ別の合計を確認する。今月の月次表示でカテゴリ集計の欄を開き、カテゴリごとの合計金額がまとまって表示されることを確認します。
確認4/4: 対象月を切り替えて先月分を表示する。対象月を先月へ切り替えて月次表示を開き、先月に登録された家賃の支出が表示されることを確認します。学習内容のまとめ。
支出テーブルをExpenseクラスとして定義する トップページで対象月の一覧と集計を返す 対象月の支出一覧を新しい順で返す 支出をkakeibo.dbというSQLiteファイルへ永続化する 小さく実行確認しながら完成状態まで段階的に組み立てる エンディング。Python研修はCodeCampでご確認ください。
PythonのORMライブラリ『SQLAlchemy』とは
SQLAlchemyは、PythonのコードからデータベースをSQLなしで操作できるORMライブラリです。この家計簿アプリでは画面をFlaskが受け持ち、保存先には追加インストールの要らないSQLiteを使い、その読み書きをSQLAlchemyが橋渡しする仕組みです。
テーブルをExpenseクラスとして定義すれば、支出の登録も月ごとの集計もPythonのメソッド呼び出しで表現できます。SQLの構文を覚える前でも、慣れたクラスや関数の感覚でデータ操作を学べる点が入門に向いています。
この家計簿アプリでSQLAlchemyが引き受けるデータ操作の内訳です
- 支出テーブルをExpenseクラスとして定義する
- create_engineでSQLiteのkakeibo.dbへ接続する
- sessionを通じて支出の登録と検索を行う
- func.sumやgroup_byでカテゴリ別合計を求める
- extractで年と月による絞り込みを行う
テーブルをExpenseクラスで表すORMの書き方
ORMは、データベースの表をPythonのクラスに、1行のデータをインスタンスへ対応させる仕組みです。この教材ではExpenseクラスがexpensesテーブルを表し、支出1件が1つのインスタンスになります。列はColumnで宣言し、Date型やInteger型といった型に加えてnullableやdefaultといった制約もまとめて書けます。
SQLのCREATE TABLEを直接書かなくても、クラス定義だけで表の構造を決められる点が特徴です。
Expenseクラスが持つ列とその型の一覧です
- 自動採番される主キーのid
- 支出日を表すDate型のspent_on
- 分類名を入れるString型のcategory
- 金額を表すInteger型のamount
- 補足を残すString型のmemo
エンジンとセッションでSQLiteへつなぐ流れ
create_engineは接続先を表すエンジンを作り、ここではsqlite:///kakeibo.dbという同じフォルダのファイルを指定します。SQLiteはサーバーを立てずにファイル1つで動くため、追加のインストールや設定なしで試せるのが利点です。
sessionmakerはエンジンに紐づくセッションの工場で、SessionLocalを呼ぶたびに新しいセッションが得られます。セッションはデータの読み書きをまとめる単位で、使い終えたら閉じて接続を返します。
接続まわりでこの教材が指定している値です
- 接続文字列のsqlite:///kakeibo.db
- スレッド共有を許すcheck_same_thread=False
- エンジンを束ねるsessionmakerのbind
- 都度セッションを開くSessionLocal
commitとcloseで保存と後片付けをする理由
セッションへaddした支出は、commitを呼んだ時点でデータベースへ書き込まれます。登録予定を積んでからまとめて確定する流れなので、途中で失敗しても不完全なデータが残りにくいのが利点です。この教材ではtryとfinallyを組み合わせ、成功しても失敗してもfinallyでセッションを閉じます。
閉じ忘れによる接続の滞留を避けられるため、Webアプリでも安定して動かせます。
支出を1件保存するときの手順です
- Expenseをsession.addで登録予定にする
- session.commitで変更を確定する
- finallyのsession.closeで必ず閉じる
Python・SQLAlchemyで開発する場合の環境構築
この記事のセットアップ手順と掲載コードは、Windows 11 Pro、PowerShell 5.1、Python 3.13.3で動作確認しています。仮想環境を有効化せず、その中のPythonを直接指定するため、以下のコマンドはPowerShellとコマンドプロンプト(cmd)の両方で使えます。
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install Flask SQLAlchemy
.\.venv\Scripts\python.exe -m pip install pysqlite3-binary
macOS・Linuxでは仮想環境内のPythonパスが異なります。今回の動作確認環境とは異なるため、以下は環境差分を補う参考手順です。
python3 -m venv .venv
./.venv/bin/python -m pip install --upgrade pip
./.venv/bin/python -m pip install Flask SQLAlchemy
./.venv/bin/python -m pip install pysqlite3-binary
- SQLiteはPython標準ライブラリのsqlite3としても同梱されていますが、本手順ではpysqlite3-binaryを追加インストールし、SQLAlchemyがsqlite:///のURLでSQLiteへ確実に接続できるようにしています。
- 初回起動時にカレントディレクトリへkakeibo.dbが作られ、支出データはこのファイルに永続化されます。作り直したいときはこのファイルを削除してください。
支出集計家計簿アプリの要件定義
目的は、SQLAlchemyとSQLiteで支出を登録し月次一覧とカテゴリ別合計を画面で確認できる家計簿アプリを作れるようになることです。
対象者として、Pythonの基本文法を学び終えSQLAlchemyのORMとFlaskでのWebアプリ開発を学びたい人を想定しています。
完成物は、FlaskとSQLAlchemyとSQLiteで支出登録と月次一覧とカテゴリ集計を1画面で確認できる家計簿アプリ一式です。
実装へ入る前に、機能・品質・受け入れ条件を分けて確認します。
機能要件
- 支出の日付とカテゴリと金額とメモを入力するフォームを表示する
- フォーム送信を受け取りSQLiteへ支出を保存する
- 対象月を選んで月次の支出一覧を表示する
- 支出一覧を日付の新しい順に並べる
- カテゴリ別の合計金額を集計して表示する
- 対象月の合計金額を計算して表示する
- 登録後に登録した月の表示へリダイレクトする
- 起動時に初期サンプルデータを投入する
非機能要件
- 支出をkakeibo.dbというSQLiteファイルへ永続化する
- check_same_thread=Falseで同じエンジンを共有する
- リクエストごとにセッションを開きfinallyで閉じる
- 金額が数値に変換できないときは0として扱う
- 日付が読み取れないときは当日の日付にする
- カテゴリが空のときは未分類として保存する
- 既存データがある場合は初期サンプルの投入を省く
実装方針
今回はSQLAlchemyとSQLiteの基本動作を追いやすくするため、支出集計家計簿アプリ本体を1つのPythonファイルへまとめます。
入力、判定、結果表示の役割を分け、実行結果を確認しながら機能を積み上げます。
支出集計家計簿アプリを安全に組み立てるための実装方針は次のとおりです。
- 支出をkakeibo.dbというSQLiteファイルへ永続化する
- check_same_thread=Falseで同じエンジンを共有する
- リクエストごとにセッションを開きfinallyで閉じる
- 金額が数値に変換できないときは0として扱う
- 日付が読み取れないときは当日の日付にする
- カテゴリが空のときは未分類として保存する
- 既存データがある場合は初期サンプルの投入を省く
完成と判断する条件
- 支出を登録すると一覧に新しい行が増える
- 登録した支出のカテゴリが集計表へ加算される
- 対象月を切り替えると該当月の一覧だけが表示される
- 月合計が一覧の金額の総和と一致する
- 金額欄を空で送っても0円として登録される
- カテゴリ欄を空で送ると未分類として登録される
SQLAlchemyとFlaskで家計簿アプリを作る際の重要ポイント
今回使用する主要なライブラリについて、役割と使い分けを順番に確認します。
画面と保存をつなぐWebフレームワークFlask
Flaskは、URLごとの処理と画面表示を短いコードで書けるPythonのWebフレームワークです。この家計簿アプリでは、月次一覧を返すトップページと支出を保存する登録処理という2つのルートを用意する構成です。フォームから送られた値はrequestで受け取り、render_template_stringでHTMLへ埋め込んで返します。
登録が終わるとredirectで一覧へ戻るため、利用者は続けて支出を入力できます。
Flaskがこのアプリで担当する画面と通信の役割です
- トップページで対象月の一覧と集計を返す
- 登録用ルートでフォーム送信を受け取る
- requestで日付やカテゴリなどの入力値を読む
- render_template_stringでHTMLを組み立てる
- redirectで登録した月の表示へ戻す
対象月の一覧と合計を組み立てるfetch_month
fetch_monthは、指定した年と月をもとに支出一覧・カテゴリ集計・月合計の3つをまとめて取り出す関数です。年月の絞り込みにはextractを使い、spent_onから年と月を取り出して対象月だけを残す仕組みです。集計ではgroup_byでカテゴリごとに束ね、func.sumで合計金額を求めます。
月合計はscalarで1つの数値として受け取り、データが無いときは0を返すようにしています。
fetch_monthが1回の呼び出しでまとめて返す情報です
- 対象月の支出一覧を新しい順で返す
- カテゴリごとの合計金額を降順で返す
- 対象月の合計金額を1つの数値で返す
- 該当データが無いときは合計を0にする
Pythonで支出集計家計簿アプリの完成コード
コード全体は、データベース設定・モデル定義・初期化・月次取得の4つのブロックに分かれています。前半で接続とテーブルの土台を用意し、後半で登録と集計の処理を組み立てます。
Flaskはフォームから届いた値をルートで受け取り、SQLAlchemyのセッション越しにデータベースへ書き込む流れです。表示のときは同じセッションで検索と集計を行い、その結果を画面に渡す仕組みです。
ここでは全行ではなく、押さえておきたい要点だけを抜き出して解説します。
このセクションの用語
- Flask
- Flaskは、PythonでWebアプリを作る軽量なフレームワークです。URLごとの処理を短く書けます。
- ルーティング
- ルーティングは、URLと処理する関数を結びつける仕組みです。どのアドレスで何を実行するかを決めます。
- セッション
- セッションは、データベースとのやり取りをまとめる作業単位です。追加や検索を行い、最後に確定します。
- extract
-
extractは、日付から年や月などの一部分を数値で取り出す関数です。月次の絞り込みに使います。 - group by
-
group byは、指定した列の値ごとに行をまとめる集計の仕組みです。カテゴリ別の合計などに使います。 - テンプレート
- テンプレートは、画面のHTMLに値を差し込むための下書きです。取得した一覧や集計を埋め込んで表示します。
"""SQLAlchemyとSQLiteで作る家計簿アプリ。
支出登録・カテゴリ集計・月次表示の3機能を、Flask + SQLAlchemy + SQLiteで実装する。
"""
import datetime as dt
from flask import Flask, redirect, render_template_string, request, url_for
from sqlalchemy import (
Column,
Date,
Integer,
String,
create_engine,
extract,
func,
)
from sqlalchemy.orm import declarative_base, sessionmaker
# データベース設定(SQLAlchemy + SQLite)
Base = declarative_base()
engine = create_engine(
"sqlite:///kakeibo.db",
connect_args={"check_same_thread": False},
)
SessionLocal = sessionmaker(bind=engine)
# 支出テーブルの定義(ORMモデル)
class Expense(Base):
__tablename__ = "expenses"
id = Column(Integer, primary_key=True)
spent_on = Column(Date, nullable=False)
category = Column(String(40), nullable=False)
amount = Column(Integer, nullable=False)
memo = Column(String(200), default="")
# テーブル作成と初期サンプルデータの投入
def init_db():
Base.metadata.create_all(engine)
session = SessionLocal()
try:
if session.query(Expense).count() == 0:
today = dt.date.today()
session.add_all(
[
Expense(spent_on=today, category="食費", amount=1200, memo="ランチ"),
Expense(spent_on=today, category="交通費", amount=400, memo="電車代"),
Expense(
spent_on=today - dt.timedelta(days=1),
category="日用品",
amount=880,
memo="洗剤",
),
Expense(
spent_on=today - dt.timedelta(days=2),
category="食費",
amount=650,
memo="コーヒー豆",
),
Expense(
spent_on=today - dt.timedelta(days=33),
category="家賃",
amount=60000,
memo="先月分",
),
]
)
session.commit()
finally:
session.close()
# 月次の支出一覧・カテゴリ集計・月合計を取得
def fetch_month(session, year, month):
period = (
extract("year", Expense.spent_on) == year,
extract("month", Expense.spent_on) == month,
)
expenses = (
session.query(Expense)
.filter(*period)
.order_by(Expense.spent_on.desc(), Expense.id.desc())
.all()
)
summary = (
session.query(Expense.category, func.sum(Expense.amount).label("total"))
.filter(*period)
.group_by(Expense.category)
.order_by(func.sum(Expense.amount).desc())
.all()
)
total = session.query(func.sum(Expense.amount)).filter(*period).scalar() or 0
return expenses, summary, total
# 画面テンプレート(支出登録フォーム・月次一覧・カテゴリ集計)
TEMPLATE = """<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>家計簿アプリ</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 780px; margin: 24px auto; padding: 0 16px; color: #222; }
h1 { font-size: 1.4rem; }
h2 { font-size: 1.1rem; }
section { margin: 18px 0; padding: 16px; border: 1px solid #ddd; border-radius: 8px; }
label { display: inline-block; margin: 4px 12px 4px 0; }
input { padding: 6px; }
button { padding: 8px 16px; background: #2b7a4b; color: #fff; border: none; border-radius: 6px; cursor: pointer; }
table { width: 100%; border-collapse: collapse; margin-top: 8px; }
th, td { border-bottom: 1px solid #eee; padding: 6px 8px; text-align: left; }
.amount { text-align: right; }
.total { font-weight: bold; }
</style>
</head>
<body>
<h1>SQLAlchemy × SQLite 家計簿アプリ</h1>
<section>
<h2>支出登録</h2>
<form method="post" action="/add" id="expense-form">
<label>日付 <input type="date" id="exp-date" name="date" value="{{ today }}"></label>
<label>カテゴリ <input type="text" id="exp-category" name="category" placeholder="食費 など"></label>
<label>金額 <input type="number" id="exp-amount" name="amount" placeholder="1200"></label>
<label>メモ <input type="text" id="exp-memo" name="memo" placeholder="ランチ など"></label>
<button type="submit" id="add-expense">登録する</button>
</form>
</section>
<section>
<h2>月次表示</h2>
<form method="get" action="/" id="month-form">
<label>対象月 <input type="month" id="month-picker" name="month" value="{{ month }}"></label>
<button type="submit" id="show-month">表示</button>
</form>
<p class="total" id="month-total">{{ month }} の合計: {{ total }}円</p>
<table id="expense-list">
<thead><tr><th>日付</th><th>カテゴリ</th><th class="amount">金額</th><th>メモ</th></tr></thead>
<tbody>
{% for e in expenses %}
<tr>
<td>{{ e.spent_on.isoformat() }}</td>
<td>{{ e.category }}</td>
<td class="amount">{{ e.amount }}円</td>
<td>{{ e.memo }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<section>
<h2>カテゴリ集計</h2>
<table id="category-summary">
<thead><tr><th>カテゴリ</th><th class="amount">合計金額</th></tr></thead>
<tbody>
{% for cat, subtotal in summary %}
<tr>
<td>{{ cat }}</td>
<td class="amount">{{ subtotal }}円</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
</body>
</html>"""
# Flaskアプリ本体
app = Flask(__name__)
ROUTES = ["/"]
# 月次表示(対象月の一覧とカテゴリ集計を描画)
@app.route("/")
def index():
month = request.args.get("month") or dt.date.today().strftime("%Y-%m")
try:
year, mon = (int(x) for x in month.split("-"))
except ValueError:
today = dt.date.today()
year, mon, month = today.year, today.month, today.strftime("%Y-%m")
session = SessionLocal()
try:
expenses, summary, total = fetch_month(session, year, mon)
return render_template_string(
TEMPLATE,
month=month,
today=dt.date.today().isoformat(),
expenses=expenses,
summary=summary,
total=total,
)
finally:
session.close()
# 支出登録(フォーム送信を受け取りSQLiteへ保存)
@app.route("/add", methods=["POST"])
def add():
date_str = request.form.get("date") or dt.date.today().isoformat()
try:
spent_on = dt.date.fromisoformat(date_str)
except ValueError:
spent_on = dt.date.today()
category = (request.form.get("category") or "").strip() or "未分類"
try:
amount = int(request.form.get("amount") or 0)
except ValueError:
amount = 0
memo = (request.form.get("memo") or "").strip()
session = SessionLocal()
try:
session.add(
Expense(spent_on=spent_on, category=category, amount=amount, memo=memo)
)
session.commit()
finally:
session.close()
return redirect(url_for("index", month=spent_on.strftime("%Y-%m")))
# 起動時にデータベースを初期化
init_db()
if __name__ == "__main__":
app.run(debug=True)
コード全文は上の折り畳みに入れてあるので、全部を上から読む必要はありません。ここでは特に重要な部分だけを抜き出して、何をしているのか順番に見ていきます。
create_engineでSQLiteへ接続する
engine = create_engine(
"sqlite:///kakeibo.db",
connect_args={"check_same_thread": False},
)
SessionLocal = sessionmaker(bind=engine)create_engineでkakeibo.dbというファイルに接続します。sessionmakerは、操作の窓口になるセッションを作り出す工場のような役割です。
Expenseモデルで支出テーブルを定義する
class Expense(Base):
__tablename__ = "expenses"
id = Column(Integer, primary_key=True)
spent_on = Column(Date, nullable=False)
category = Column(String(40), nullable=False)
amount = Column(Integer, nullable=False)
memo = Column(String(200), default="")クラスの属性がそのまま表の列に対応します。日付・カテゴリ・金額・メモの4項目を持ち、nullable=Falseで空の登録を禁止しています。
create_allでテーブルを自動生成する
def init_db():
Base.metadata.create_all(engine)
session = SessionLocal()create_allはモデルの定義を読み取り、まだ無ければ表を作ります。手作業でCREATE TABLEを書かなくてよいのが便利です。
add_allとcommitでサンプルを登録する
if session.query(Expense).count() == 0:
today = dt.date.today()
session.add_all(
[
Expense(spent_on=today, category="食費", amount=1200, memo="ランチ"),件数が0のときだけ初期データを入れます。add_allで複数行をまとめてセッションに追加し、あとのcommitで保存します。
extractで年と月を絞り込む
period = (
extract("year", Expense.spent_on) == year,
extract("month", Expense.spent_on) == month,
)extractは日付から年や月の数字を取り出します。この2つの条件をfilterへ渡すと、指定した年月の支出だけに絞り込めます。
func.sumとgroup_byでカテゴリ集計する
summary = (
session.query(Expense.category, func.sum(Expense.amount).label("total"))
.filter(*period)
.group_by(Expense.category)group_byでカテゴリごとにまとめ、func.sumで金額を合計します。labelで合計の列にtotalという名前を付けています。
参考:
©SQLAlchemy公式ドキュメント (MetaData.create_all)checkfirst – Defaults to True, don't issue CREATEs for tables already present in the target database.
支出集計家計簿アプリの動作確認
実際にアプリを起動し、日付を既定のまま食費の支出を登録してみました。すると月次一覧に新しい行が追加され、カテゴリ集計にも食費の合計が反映されることを確認できました。
画面はHTTP 200で返り、外部通信なしでローカルのSQLiteファイルだけで動きます。登録前と登録後をキャプチャで見比べると、変化した箇所がはっきり分かります。
各要素に固定idを付けておいたおかげで、どの行と合計が更新されたのかを迷わず追えました。
このセクションの用語
- HTTPステータス
- HTTPステータスは、リクエストの結果を表す番号です。200は正常に処理できたことを示します。



PythonのSQLAlchemyとSQLiteのエラー対処
動かし始めの頃は、テーブルやデータの前提が整っていないことでエラーが出やすいです。ここでは家計簿アプリで遭遇しやすい実行エラーを、原因と対処と合わせて整理します。
いずれも一般に起こりやすいもので、必ず出るわけではありません。メッセージの文言から原因を切り分けると、対処へたどり着きやすくなります。
このセクションの用語
- 例外
- 例外は、処理の途中で起きたエラーを表す仕組みです。捕まえて対処すれば、途中で止まらずに続けられます。
- 制約
- 制約は、列に入れてよい値のルールです。
nullable=Falseは空を許さない制約を表します。
| エラー例 | 原因 | 対処 |
|---|---|---|
| OperationalError: no such table: expenses | create_allを呼ぶ前に検索した | 起動時にinit_dbで先にテーブルを作る |
| IntegrityError: NOT NULL constraint failed | 日付や金額など必須列が空のまま登録した | フォーム値を検証し、空なら登録を止める |
| ValueError: invalid literal for int() | 金額の文字列を数値へ変換できなかった | int()の前に数字だけか確認し、例外を捕まえる |
| ProgrammingError: SQLite objects created in a thread... | 別スレッドから同じ接続を使った | connect_argsでcheck_same_thread=Falseを指定する |
| DetachedInstanceError | セッションを閉じた後にオブジェクトへアクセスした | 必要な値はセッション内で取り出しておく |
支出集計家計簿アプリで注意したい点
extractで月を絞るときは、yearとmonthを整数で渡さないと一致しません。フォームから届く値は文字列なので、int()で数値へ直してから比較します。
group_byの集計は、該当する支出が無い月では行そのものが返りません。空の月はカテゴリ集計が0行になる前提で表示を組みます。
セッションはリクエストごとに開き、使い終わったら必ず閉じるのが基本です。閉じ忘れると接続が残り、あとの操作でエラーの引き金になります。
ポイントとしては、月次の絞り込みと集計まわりのハマりどころを先に押さえておくことです。
年月の型:整数で比較する
空の月:集計は0行になる
セッション:使ったら閉じる
カテゴリ集計を月次表示に応用する場面
家計簿アプリの土台は、日付付きの記録とカテゴリ別の集計です。この組み合わせは、個人の家計だけでなく小さな会計や学習教材にも応用できます。
同じ仕組みで対象を入れ替えれば、費目や期間の切り口を変えて使い回せます。
このセクションの用語
- CRUD
- CRUDは、作成・読み取り・更新・削除という4つの基本操作の頭文字です。データ管理アプリの土台になります。
| 使える場面 | 具体的な使い方 |
|---|---|
| 個人の家計管理 | 毎月の食費や交通費を登録し、カテゴリ集計で使いすぎの費目を見つける |
| サークルや部活の会計 | 立替や購入をカテゴリ別に記録し、月次で集計して精算資料にする |
| 小規模店舗の経費把握 | 仕入れや消耗品を日付付きで登録し、月ごとの合計を経費の目安にする |
| 学習用のCRUD教材 | SQLAlchemyの登録と集計を、最小構成で手を動かして学ぶ題材にする |
支出集計家計簿アプリ開発のまとめ
PythonのSQLAlchemyとSQLiteで、支出登録・カテゴリ集計・月次表示を備えた家計簿アプリを作りました。ORMでモデルを定義し、extractとgroup_byで月ごとの集計を組み立てられます。
食費の支出を登録すると、月次一覧に行が増え、カテゴリ集計へ合計が反映されることを実際に確認できました。
まずはこの最小構成を動かし、フォームの検証や費目の追加など、少しずつ機能を足していくのがおすすめです。
さらに詳しく学べるPython研修の詳細はこちら参考にした一次情報
- ^ SQLAlchemy ORM Quick Start. https://docs.sqlalchemy.org/en/20/orm/quickstart.html, (参照26-07-31).
- ^ SQLAlchemy: SQLite Dialect. https://docs.sqlalchemy.org/en/20/dialects/sqlite.html, (参照26-07-31).
- ^ Flask Quickstart. https://flask.palletsprojects.com/en/stable/quickstart/, (参照26-07-31).
- ^ Python sqlite3 — DB-API 2.0 interface for SQLite. https://docs.python.org/3/library/sqlite3.html, (参照26-07-31).
※内容は執筆時点のものです。ライブラリやサイトの仕様は変わる可能性があるため、公式ドキュメントもあわせてご確認ください。
ITやプログラミングに関するコラム
【Python】FastAPIで料金プラン見積もりシミュレーターを作ってみた
【Python】pandasとmatplotlibで在庫データのABC分析と構成比を可視化してみた
【Python】Flaskで社内FAQをカテゴリ検索できるWebアプリを作ってみた
【Python】argparseでJSON整形・構文検証・キー検索CLIを試してみた
【Python】NumPyとmatplotlibでモンテカルロ法による円周率推定と収束過程の可視化を試してみた
【Python】Playwrightでスクレイピングを試してみた
【CSS】notで複数の件を除外する方法
【Git】remote設定を変更する方法
【VBA】コメントアウトを設定する方法
x86とx64の違いを分かりやすく解説
ITやプログラミングに関するニュース
VercelがAI GatewayにSeedream 5.0 Proを追加、AI SDKのモデル指定で画像生成と編集が可能に
AWSがAmazon LocationのPlaces APIを強化、住所表記の指定と移動手段別の検索が可能に
VercelがトレースにTree・Waterfallビューを追加、ログ画面で処理の階層と所要時間を確認可能に
Googleがエージェント評価の再考を提唱、難易度を情報量で測るDiscovery Benchを解説
Google CloudがCloud Runサンドボックスを公開プレビューで提供、サービスヘルスは一般提供に
Google Cloud EMEAが英国金融の重要第三者に指定、イングランド銀行・PRA・FCAの直接監督下に
AWS DMS Schema ConversionがSQL Serverのオフライン変換に対応、ソースDBへ接続せずスキーマを変換可能に
EC2 G7インスタンスが米国東部(バージニア北部)で利用可能に、G6比でAI推論性能が最大4.6倍
SageMaker HyperPodが継続プロビジョニングでのAMIベース構成に対応、S3のスクリプト管理なしでSlurmクラスターを作成可能に
AWSがEMR on EKSでSparkトラブルシューティングエージェントに対応、失敗ジョブの原因分析を自然言語で依頼可能に
