【Python】FastAPI・SQLModel・SQLiteで習慣記録APIを作ってみた

【Python】FastAPI・SQLModel・SQLiteで習慣記録APIを作ってみた

公開: 更新:
CodeCampが提供するDX人材育成が可能なプログラミングやITが学べる公開講座

毎日の習慣を続けられているか、記録して振り返りたくなることがあります。今回はPythonのFastAPISQLModelSQLiteを使い、習慣の登録・達成の記録・達成回数の集計ができるREST APIを実際に作って動かしました。

モデルを2つに分ける設計や、func.countGROUP BYによる集計、確認用のHTML画面まで、初心者が追体験できるように順を追って解説します。

FastAPIの基本概念、要件定義、実装、動作確認までを順番に学べる構成です。動画は目次から確認したい場面へ移動でき、本文だけでも手順と考え方が完結します。

【Python】FastAPI・SQLModel・SQLiteで習慣記録APIを作ってみた|フルカリキュラム
動画の内容をテキストで確認する

オープニング。FastAPIとSQLModelを使って習慣記録APIを作るカリキュラムを始めます。概要紹介。

FastAPIとSQLModelの役割と使い方を学ぶ 習慣記録APIの要件を整理する 完成コードと実行結果を確認する 最後に完成した画面を実際に操作して確かめる 具体的にやること。

習慣名を受け取り習慣テーブルへ登録する 登録済みの習慣を一覧で取得する 選んだ習慣に達成記録を追加する 習慣ごとの達成回数を集計する 動作確認用のHTML画面を返す 実装環境・必須アプリ。

OS:Windows 11 Pro Python:3.13.3シェル:PowerShell 5.1必須アプリ:コードエディター、Webブラウザ、ターミナル パッケージ:pip、fastapi、sqlmodel、"uvicorn[standard]"、sqlite-utils PythonのFastAPIとSQLModelとは。

FastAPI:FastAPIは、Pythonの型ヒントを活かして少ないコードでREST APIを構築できるWebフレームワークSQLModel:SQLModelは、Pythonのクラスとしてテーブルとデータをまとめて定義できるORMライブラリ FastAPIで作る習慣記録APIの要点。

登録した習慣にidが採番される 達成のたびにCheckInが1件増えるhabit_idで習慣と達成記録がつながる 習慣記録APIの要件定義。

習慣登録でidと名前のJSONが返る 達成記録でdone_onを含むJSONが返る 集計APIが習慣ごとの達成回数を返す 達成が無い習慣も集計に0回で現れる トップ画面に登録・記録・集計の3セクションが並ぶ INTRO: Monaco Editorで習慣記録APIを実装。コードを1行ずつ入力し、補完と自動インデントを使いながら実行結果を確認します。

LINE 001: モジュール全体の説明文。このファイルが習慣記録APIをFastAPIとSQLModel、SQLiteで実装したものであることを説明するドキュメント文字列です。ファイルの先頭に書くことで、モジュールの用途がひと目でわかるようになります。

LINE 003: モジュールの機能概要。習慣の登録や達成記録、集計を行うREST APIと、動作確認用の簡易画面をまとめたモジュールであることを説明しています。読む人が全体像をつかみやすくなります。

LINE 004: ドキュメント文字列の終端。モジュール冒頭のドキュメント文字列を閉じる行です。ここまでがファイル全体の説明文になります。

LINE 006: 日付型のインポート。標準ライブラリのdatetimeモジュールからdateクラスを読み込んでいます。達成日を扱うために必要な型になります。

LINE 007: Optional型のインポート。typingモジュールからOptionalを読み込んでいます。値がNoneになり得る項目の型を表すために使います。

LINE 009: FastAPI関連のインポート。fastapiパッケージからFastAPI本体とHTTPExceptionを読み込んでいます。APIアプリの作成とエラー応答の送出に使います。

LINE 010: HTML応答クラスのインポート。fastapiのresponsesモジュールからHTMLResponseを読み込んでいます。確認用画面をHTMLとして返すために使います。

LINE 011: BaseModelのインポート。pydanticからBaseModelを読み込んでいます。リクエストボディの形式を定義するために使います。

LINE 012: 集計関数のインポート。sqlalchemyからfuncを読み込んでいます。件数を数えるcountなどの集計関数を使うために必要です。

LINE 013: SQLModel関連のインポート。sqlmodelからFieldやSession、SQLModel、create_engine、selectを読み込んでいます。テーブル定義やデータベース操作に必要な部品がまとまっています。

LINE 017: データベース接続先の指定。SQLiteのファイルhabits.dbを使うことを表す接続文字列を定義しています。この文字列が次の行でエンジン作成時に使われます。

LINE 018: データベースエンジンの作成。接続文字列をもとにデータベースエンジンを作成しています。check_same_threadをFalseにすることで、複数のリクエストから同じ接続を扱えるようにしています。

RUN 1/6: SQLiteへの接続を用意する。DATABASE_URLからengineを作成し、SQLiteに接続できる状態を確認します。CHECK 1/6: 途中実行に成功。

engine: sqlite:///habits.db RETURN 01: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 022: 習慣テーブルの定義開始。

SQLModelを継承し、table=Trueを指定することでHabitクラスを実際のデータベーステーブルとして扱う定義を始めています。LINE 023: 習慣の主キー定義。idという列を定義し、主キーとして扱う設定にしています。

初期値はNoneで、登録時にデータベースが自動で採番します。LINE 024: 習慣名の列定義。習慣名を保存するname列を定義しています。

indexを付けることで検索が速くなるようにしています。LINE 027: 達成記録テーブルの定義開始。達成記録を保存するCheckInクラスを、実テーブルとして定義しています。

LINE 028: 達成記録の主キー定義。達成記録ごとに一意なidを持たせる主キー列を定義しています。登録時に自動で採番されます。

LINE 029: 外部キーの定義。どの習慣に対する達成記録かを表すhabit_id列を定義し、habitテーブルのidを参照する外部キーにしています。LINE 030: 達成日の列定義。

達成した日付を保存するdone_on列を定義しています。値を指定しない場合は今日の日付が自動で設定されます。RUN 2/6: 習慣と達成記録のモデルを定義する。

HabitとCheckInの2つのテーブルモデルを定義し、生成されるテーブル名を確認します。CHECK 2/6: 途中実行に成功。tables: ['habit', 'checkin'] RETURN 02: エディターへ戻りました。

入力済みのコードを維持したまま、次の実装へ進みます。LINE 034: 習慣登録リクエストの形式定義。習慣を新規登録する際に受け取るリクエストボディの形式を定義するクラスです。

LINE 035: 習慣名の項目定義。リクエストボディに含まれる習慣名の項目を文字列型として定義しています。LINE 038: 達成記録リクエストの形式定義。

達成記録を登録する際に受け取るリクエストボディの形式を定義するクラスです。LINE 039: 達成日の任意項目定義。達成日を指定できる項目を定義しています。

省略された場合はNoneとして扱われ、後の処理で今日の日付が使われます。LINE 043: テーブルの作成実行。これまで定義したモデルをもとに、まだ存在しないテーブルをデータベース上に作成しています。

アプリ起動時に一度だけ実行される準備処理です。RUN 3/6: データベースにテーブルを作成する。create_allを実行し、habits.dbにテーブルが作られることを確認します。

CHECK 3/6: 途中実行に成功。created: ['checkin', 'habit'] RETURN 03: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。

LINE 047: FastAPIアプリの生成。FastAPIのインスタンスを作成し、タイトルとして習慣記録APIという名前を設定しています。この変数がAPI全体の入り口になります。

LINE 049: HTML文字列の開始。確認用画面のHTMLをまとめる文字列変数の定義を開始しています。DOCTYPE宣言でHTML文書であることを示しています。

LINE 050: html要素の開始タグ。HTML文書全体を囲むhtmlタグを開いており、言語設定として日本語を指定しています。LINE 051: head要素の開始タグ。

ページのメタ情報やスタイルなどをまとめるhead要素を開始しています。LINE 052: 文字コードの指定。ページの文字コードをUTF-8に指定するmetaタグです。

日本語などが正しく表示されるようにするために必要です。LINE 053: 表示領域の指定。スマートフォンなどでも見やすくするためのビューポート設定を行うmetaタグです。

LINE 054: ページタイトルの設定。ブラウザのタブなどに表示されるページタイトルを設定しています。LINE 055: スタイル定義の開始。

ページ全体の見た目を整えるCSSを記述するstyleタグを開始しています。LINE 056: 全体レイアウトのスタイル。ページ全体のフォントや最大幅、余白を指定するスタイルです。

読みやすいレイアウトになるよう調整しています。LINE 057: セクション枠のスタイル。各セクションに枠線と角丸、余白を付けるスタイルです。

項目ごとに見た目を区切るために使われています。LINE 058: 見出しのスタイル。h2見出しの上余白をなくし、文字サイズを調整するスタイルです。

LINE 059: 入力部品のスタイル。input、select、buttonといった入力部品の文字サイズや余白を統一するスタイルです。LINE 060: ボタンのカーソル指定。

ボタンにマウスを重ねたときにクリック可能であることが分かるカーソル表示を指定しています。LINE 061: リスト項目の余白指定。li要素の上下の余白を調整するスタイルです。

習慣一覧を見やすく表示するために使われます。LINE 062: メッセージ表示の色指定。msgクラスが付いた要素の文字色を青系に指定するスタイルです。

達成記録のメッセージ表示に使われます。LINE 063: スタイル定義の終了。ここまで記述してきたCSSの定義を閉じています。

LINE 064: head要素の終了タグ。head要素の記述を閉じ、ここから本文部分の記述に移ります。LINE 065: body要素の開始タグ。

実際に画面へ表示される本文部分であるbody要素を開始しています。LINE 066: ページ見出しの表示。ページの一番上に表示される大見出しとして、習慣記録APIデモという文字を表示しています。

LINE 068: 習慣登録セクションの開始。習慣を登録するための操作をまとめるsection要素を開始しています。LINE 069: 登録セクションの小見出し。

このセクションが習慣を登録する機能であることを示す見出しを表示しています。LINE 070: 習慣名の入力欄。習慣名を入力するためのテキスト入力欄です。

プレースホルダーとして入力例を表示しています。LINE 071: 登録ボタンの配置。入力された習慣名を登録するためのボタンを配置しています。

このボタンにクリック処理が後で結び付けられます。LINE 072: 習慣一覧の表示枠。登録済みの習慣を一覧表示するためのリスト要素を用意しています。

中身はJavaScriptから追加されます。LINE 073: 習慣登録セクションの終了。習慣登録セクションの記述を閉じています。

LINE 075: 達成記録セクションの開始。達成を記録するための操作をまとめるsection要素を開始しています。LINE 076: 達成記録セクションの小見出し。

このセクションが達成を記録する機能であることを示す見出しを表示しています。LINE 077: 習慣選択のプルダウン。どの習慣の達成を記録するかを選ぶためのプルダウンです。

中身は登録済みの習慣一覧から動的に作られます。LINE 078: 達成記録ボタンの配置。選択した習慣の達成を記録するためのボタンを配置しています。

LINE 079: 記録結果メッセージの表示枠。達成記録の結果メッセージを表示するための段落要素です。記録が成功すると日付付きのメッセージが表示されます。

LINE 080: 達成記録セクションの終了。達成記録セクションの記述を閉じています。LINE 082: 集計セクションの開始。

習慣ごとの達成状況を集計して表示するsection要素を開始しています。LINE 083: 集計セクションの小見出し。このセクションが達成状況を集計する機能であることを示す見出しを表示しています。

LINE 084: 集計実行ボタンの配置。習慣ごとの達成回数を集計するためのボタンを配置しています。LINE 085: 集計結果の表示枠。

集計結果の一覧を表示するための空のdiv要素です。中身はJavaScriptから動的に追加されます。LINE 086: 集計セクションの終了。

集計セクションの記述を閉じています。LINE 088: スクリプト定義の開始。画面上の操作に応じてAPIと通信するJavaScriptコードを記述するscriptタグを開始しています。

LINE 089: 習慣一覧読み込み関数の定義。習慣一覧を取得して画面に反映する非同期関数loadHabitsを定義しています。選択したい習慣のidを引数として受け取れます。

LINE 090: 習慣一覧APIの呼び出し。サーバーの習慣一覧APIへリクエストを送り、その応答を受け取っています。LINE 091: 応答データのJSON変換。

サーバーから返ってきた応答をJSON形式のデータへ変換し、習慣一覧として取得しています。LINE 092: 習慣一覧の要素を取得。画面内にあるid「habit-list」の要素を取得して、変数listに格納しています。

この要素は登録済みの習慣名を一覧表示するためのul要素です。LINE 093: 習慣選択の要素を取得。id「habit-select」のセレクトボックス要素を取得して、変数selectに格納しています。

この要素は達成記録時に対象の習慣を選ぶために使われます。LINE 094: 習慣一覧の表示をリセット。list要素の中身を空文字にすることで、これまで表示されていた習慣一覧をいったんクリアしています。

この後の繰り返し処理で最新の一覧を作り直します。LINE 095: 選択肢の表示をリセット。select要素の中身を空文字にすることで、これまでのセレクトボックスの選択肢をクリアしています。

取得し直した習慣で選択肢を作り直す準備です。LINE 096: 取得した習慣を1件ずつ処理。サーバーから取得したhabits配列を先頭から順に取り出し、変数hに代入して繰り返し処理を行います。

各習慣ごとに一覧表示と選択肢の追加を行います。LINE 097: リスト項目の要素を作成。習慣名を表示するためのli要素を新しく作成し、変数liに格納しています。

この後この要素にテキストを設定して一覧に追加します。LINE 098: リスト項目に習慣名を設定。作成したli要素の表示文字列として、習慣データの名前(h.name)を設定しています。

これにより画面上に習慣名が表示されます。LINE 099: リストへ項目を追加。作成したli要素をlist要素の子要素として追加し、画面上の一覧に表示させています。

この処理をhabits配列の件数分繰り返します。LINE 100: 選択肢の要素を作成。セレクトボックスに追加する選択肢用のoption要素を新しく作成し、変数optに格納しています。

LINE 101: 選択肢の値を設定。作成したoption要素の値(value)として、習慣のidを設定しています。この値が達成記録時にどの習慣かを識別するために使われます。

LINE 102: 選択肢の表示文字を設定。option要素の表示文字列として習慣名を設定しています。利用者はこの名前を見て記録したい習慣を選びます。

LINE 103: セレクトボックスへ選択肢を追加。作成したoption要素をselect要素の子要素として追加し、画面上の選択肢として表示させています。LINE 104: 繰り返し処理の終わり。

habits配列の各要素に対する一覧表示と選択肢追加の繰り返し処理がここで終わります。LINE 105: 選択状態の復元。引数selectedIdが渡されている場合に、セレクトボックスの選択値をその値に設定し直しています。

習慣を新規登録した直後に、その習慣を選択済みの状態にするための処理です。LINE 106: 習慣読み込み関数の終わり。loadHabits関数の定義がここで終わります。

この関数はページ表示時や習慣登録後に呼び出され、最新の習慣一覧を画面に反映します。LINE 108: 習慣登録ボタンのクリック処理を登録。id「add-habit」ボタンがクリックされたときに実行される非同期処理を登録しています。

ここから習慣名の送信処理が始まります。LINE 109: 入力欄から習慣名を取得。id「habit-name」の入力欄に入力されている文字列を取得し、変数nameに格納しています。

この値をサーバーへ送信します。LINE 110: 習慣登録APIへリクエスト送信開始。習慣登録用のAPIエンドポイントに対してfetch関数でリクエストを送る処理を開始しています。

結果はres変数に格納されます。LINE 111: 送信方法をPOSTに指定。リクエストのHTTPメソッドとしてPOSTを指定しています。

新しい習慣データをサーバーへ登録するための送信方法です。LINE 112: 送信データの形式を指定。リクエストヘッダーにContent-Typeとしてapplication/jsonを指定し、送信するデータがJSON形式であることをサーバーに伝えています。

LINE 113: 送信内容を組み立て。入力された習慣名をname変数から取り出し、JSON文字列に変換してリクエストの本文(body)として設定しています。LINE 114: fetch呼び出しの引数終了。

fetch関数に渡すオプションオブジェクトの定義がここで終わり、リクエストが送信されます。LINE 115: レスポンスの成功判定。サーバーからの応答res.okがtrue、つまり登録に成功した場合にのみ、以降の処理を実行するための条件分岐です。

LINE 116: 登録結果のJSONを取得。サーバーから返された応答本文をJSON形式として解析し、登録された習慣データを変数habitに格納しています。LINE 117: 入力欄のクリア。

習慣名の入力欄の値を空文字にすることで、登録後に入力欄を初期状態に戻しています。LINE 118: 習慣一覧の再読み込み。loadHabits関数を呼び出し、登録した習慣のidを渡すことで、最新の一覧を表示しつつ登録した習慣を選択状態にしています。

LINE 119: 成功時処理の終わり。res.okがtrueだった場合の一連の処理がここで終わります。LINE 120: クリックイベント処理の終わり。

add-habitボタンのクリック時イベントハンドラの定義がここで終わります。LINE 122: 達成記録ボタンのクリック処理を登録。id「record-btn」ボタンがクリックされたときに実行される非同期処理を登録しています。

ここから達成記録の送信処理が始まります。LINE 123: 選択中の習慣idを取得。セレクトボックスで選択されている習慣のid(value)を取得し、変数habitIdに格納しています。

LINE 124: メッセージ表示欄の要素を取得。id「record-msg」の要素を取得し、変数msgに格納しています。この要素に処理結果のメッセージを表示します。

LINE 125: 未選択時のエラー表示と処理中断。habitIdが空の場合、つまり習慣が選択されていない場合にエラーメッセージを表示し、return文でそれ以降の処理を中断しています。LINE 126: 達成記録APIへリクエスト送信開始。

選択された習慣idを含むURLに対してfetch関数でリクエストを送る処理を開始しています。結果はres変数に格納されます。LINE 127: 送信方法をPOSTに指定。

リクエストのHTTPメソッドとしてPOSTを指定しています。達成記録を新しく登録するための送信方法です。LINE 128: 送信データの形式を指定。

リクエストヘッダーにContent-Typeとしてapplication/jsonを指定し、送信するデータがJSON形式であることをサーバーに伝えています。LINE 129: 空の送信内容を設定。リクエストの本文として空のオブジェクトをJSON文字列に変換して設定しています。

日付は指定せず、サーバー側で当日の日付を使う想定です。LINE 130: fetch呼び出しの引数終了。fetch関数に渡すオプションオブジェクトの定義がここで終わり、リクエストが送信されます。

LINE 131: レスポンスの成功判定。サーバーからの応答res.okがtrue、つまり達成記録が成功した場合にのみ、以降の処理を実行するための条件分岐です。LINE 132: 記録結果のJSONを取得。

サーバーから返された応答本文をJSON形式として解析し、記録された達成データを変数dataに格納しています。LINE 133: 達成メッセージの表示。記録された日付データdata.done_onを使い、達成を記録した旨のメッセージをmsg要素に表示しています。

LINE 134: 成功時処理の終わり。res.okがtrueだった場合の一連の処理がここで終わります。LINE 135: クリックイベント処理の終わり。

record-btnボタンのクリック時イベントハンドラの定義がここで終わります。LINE 137: 集計ボタンのクリック処理を登録。id「summary-btn」ボタンがクリックされたときに実行される非同期処理を登録しています。

ここから集計結果の取得と表示処理が始まります。LINE 138: 集計APIへリクエスト送信。習慣ごとの達成回数を集計するAPIエンドポイントへfetch関数でリクエストを送り、結果を変数resに格納しています。

LINE 139: 集計結果のJSONを取得。サーバーから返された応答本文をJSON形式として解析し、集計結果の配列を変数rowsに格納しています。LINE 140: 集計表示欄の要素を取得。

id「summary」の要素を取得し、変数divに格納しています。この要素に集計結果の一覧を表示します。LINE 141: 集計表示のリセット。

div要素の中身を空文字にすることで、これまで表示されていた集計結果をいったんクリアしています。LINE 142: 一覧表示用リスト要素の作成。集計結果を並べて表示するためのul要素を新しく作成し、変数ulに格納しています。

LINE 143: 取得した集計結果を1件ずつ処理。rows配列を先頭から順に取り出し、変数rに代入して繰り返し処理を行います。各習慣ごとに集計行を作成して表示します。

LINE 144: リスト項目の要素を作成。集計結果1件分を表示するためのli要素を新しく作成し、変数liに格納しています。LINE 145: リスト項目に集計内容を設定。

習慣名r.nameと達成回数r.totalを組み合わせた文字列を、li要素の表示テキストとして設定しています。LINE 146: リストへ項目を追加。作成したli要素をul要素の子要素として追加し、集計結果の一覧に反映させています。

LINE 147: 繰り返し処理の終わり。rows配列の各要素に対する集計表示の繰り返し処理がここで終わります。LINE 148: 集計欄へリストを追加。

作成したul要素をdiv要素の子要素として追加し、画面上に集計結果の一覧を表示させています。LINE 149: クリックイベント処理の終わり。summary-btnボタンのクリック時イベントハンドラの定義がここで終わります。

LINE 151: 初期表示時の習慣読み込み。ページが読み込まれた直後にloadHabits関数を呼び出し、登録済みの習慣一覧と選択肢を最初から画面に表示しています。LINE 152: スクリプトタグの終了。

ここまでに書かれてきたJavaScriptのコードを囲むscriptタグを閉じています。LINE 153: ボディタグの終了。画面本体の内容を囲んでいたbodyタグを閉じています。

LINE 154: HTML文書の終了。ページ全体を囲んでいたhtmlタグを閉じ、HTML文書の記述を終えています。LINE 155: HTML文字列の終了。

INDEX_HTML変数に代入していたHTML文字列(三重引用符の文字列リテラル)の定義をここで終えています。LINE 159: トップ画面のルート定義。URLパス「/」に対するGETリクエストを受け取るエンドポイントとして、HTML形式でレスポンスを返すことを指定しています。

LINE 160: トップ画面用の関数定義。トップページへのアクセスに対応する処理を行う関数indexを定義しています。この関数が呼ばれると確認用画面が返されます。

LINE 161: HTML文字列を返却。事前に定義しておいたINDEX_HTML変数の中身を戻り値として返し、ブラウザに動作確認用の画面を表示させています。RUN 4/6: トップ画面のHTMLを返す。

GETでトップにアクセスし、確認用画面のHTMLが返ることを確認します。CHECK 4/6: 途中実行に成功。status: 200 title found: True RETURN 04: エディターへ戻りました。

入力済みのコードを維持したまま、次の実装へ進みます。LINE 167: 習慣登録APIのルート定義。URLパス「/api/habits」に対するPOSTリクエストを受け取るエンドポイントとして、習慣を新規登録する処理につなげています。

LINE 168: 習慣登録関数の定義。習慣を新規登録するための関数です。リクエストボディをHabitCreateとして受け取り、名前の検証や登録処理へつなげます。

LINE 169: 関数の説明文。この関数が何をするAPIなのかを説明するドキュメント文字列です。習慣名をSQLiteのhabitテーブルへ新規登録することを明記しています。

LINE 170: 習慣名の前後空白を削除。受け取った習慣名の前後にある余計な空白を取り除いています。これにより空白だけの入力や見た目の不揃いを防ぎます。

LINE 171: 習慣名が空かどうかの判定。空白を取り除いた結果、名前が空になっていないかを確認しています。空の場合は次の行でエラーを返す準備をしています。

LINE 172: 空欄エラーの応答。習慣名が空だった場合に、400エラーとして入力を促すメッセージをクライアントへ返しています。ここで処理は中断されます。

LINE 174: データベースセッションの開始。SQLiteへ接続するためのセッションをwith文で開いています。以降の処理が終わると自動的にセッションが閉じられます。

LINE 176: 同名習慣の重複チェック。同じ名前の習慣がすでに登録されていないかをデータベースから検索しています。結果は変数existingに格納されます。

LINE 177: 重複結果の有無を判定。検索した結果、既存の習慣が見つかったかどうかを確認しています。見つかった場合は次の行で登録を中止します。

LINE 178: 重複エラーの応答。同じ名前の習慣がすでに存在する場合に、409エラーとして重複を知らせるメッセージを返しています。新規登録は行われません。

LINE 180: 新しい習慣データの作成。検証済みの名前を使って、新しいHabitオブジェクトを作成しています。この時点ではまだデータベースには保存されていません。

LINE 181: セッションへの登録。作成したhabitオブジェクトをセッションに追加し、後で書き込む対象として登録しています。まだ実際の保存は行われていません。

LINE 182: データベースへの書き込み確定。セッションに登録した内容をSQLiteへ実際に書き込み、INSERT処理を確定しています。この時点でデータが永続的に保存されます。

LINE 183: 保存済みデータの再読み込み。コミット後にデータベース側で採番されたidなどの情報を、habitオブジェクトへ反映させています。これによりidを応答に含められます。

LINE 186: 保存確認のための再取得。登録した習慣が実際にデータベースへ保存されているかを、idを使って改めて取得しています。念のための確認処理です。

LINE 187: 保存失敗の判定。再取得した結果がなかった場合、何らかの理由で保存に失敗したと判断できます。次の行でエラーを返す準備をしています。

LINE 188: 保存失敗エラーの応答。データベースへの保存が確認できなかった場合に、500エラーとして失敗したことをクライアントへ知らせています。LINE 190: 登録結果の応答。

保存が確認できた習慣のidと名前をJSON形式でクライアントへ返しています。これが登録処理の正常な結果です。RUN 5/6: 習慣を登録するAPIを動かす。

登録APIへ習慣名を送り、idと名前を含むJSONが返ることを確認します。CHECK 5/6: 途中実行に成功。status: 200 {'id': 1, 'name': '毎朝ストレッチをする'} RETURN 05: エディターへ戻りました。

入力済みのコードを維持したまま、次の実装へ進みます。LINE 194: 習慣一覧取得APIの登録。GETリクエストで習慣の一覧を取得するためのエンドポイントを定義しています。

パスは/api/habitsです。LINE 195: 一覧取得関数の定義。登録済みの習慣を一覧で返す関数です。

データベースから全件を取得して整形します。LINE 196: データベースセッションの開始。習慣一覧を取得するために、SQLiteへ接続するセッションをwith文で開いています。

LINE 197: 習慣一覧の取得。habitテーブルからすべての習慣をid順に並べて取得しています。結果はリストとして変数habitsに格納されます。

LINE 198: 一覧結果の整形と応答。取得した習慣一件ずつをidと名前だけの辞書に変換し、リストとしてクライアントへ返しています。LINE 202: 達成記録APIの登録。

指定した習慣に対する達成記録を作成するためのエンドポイントを定義しています。URLの一部にhabit_idを含みます。LINE 203: 達成記録関数の定義。

どの習慣について達成を記録するかをhabit_idで受け取り、リクエストボディはCheckInCreateとして受け取る関数です。LINE 204: データベースセッションの開始。達成記録を処理するために、SQLiteへ接続するセッションをwith文で開いています。

LINE 205: 対象習慣の取得。habit_idをもとに、対象となる習慣がデータベースに存在するかを取得しています。存在しなければNoneになります。

LINE 206: 習慣存在チェック。取得した習慣がNoneかどうか、つまり指定された習慣が存在するかどうかを確認しています。LINE 207: 習慣未存在エラーの応答。

指定された習慣が見つからなかった場合に、404エラーとしてその旨をクライアントへ返しています。LINE 208: 達成日の決定。リクエストで日付が指定されていればそれを使い、指定がなければ今日の日付を達成日として採用しています。

LINE 209: 達成記録データの作成。対象の習慣idと決定した達成日を使って、新しいCheckInオブジェクトを作成しています。まだ保存はされていません。

LINE 210: セッションへの登録。作成した達成記録をセッションに追加し、後でデータベースへ書き込む対象として登録しています。LINE 211: データベースへの書き込み確定。

セッションに登録した達成記録をSQLiteへ実際に書き込み、INSERT処理を確定しています。LINE 212: 保存済みデータの再読み込み。コミット後にデータベース側で採番されたidなどの情報を、checkinオブジェクトへ反映させています。

LINE 213: 達成記録結果の応答。記録されたidと習慣id、達成日を文字列形式にしてクライアントへ返しています。これが達成記録処理の正常な結果です。

LINE 217: 集計APIの登録。習慣ごとの達成状況を集計して返すためのエンドポイントを定義しています。パスは/api/habits/summaryです。

LINE 218: 集計関数の定義。各習慣について達成回数を集計し、その結果を返す関数です。LINE 219: データベースセッションの開始。

集計処理を行うために、SQLiteへ接続するセッションをwith文で開いています。LINE 220: 集計クエリの組み立て開始。習慣ごとの達成回数を求めるためのSQL文の組み立てを開始しています。

複数行にまたがって条件を積み重ねています。LINE 221: 取得項目と件数集計の指定。習慣のidと名前、そして達成記録の件数を数える集計処理を指定しています。

ここで集計対象の列が決まります。LINE 222: 外部結合の指定。習慣テーブルと達成記録テーブルを、habit_idを条件に外部結合しています。

達成記録がない習慣も結果に含めるための指定です。LINE 223: 習慣ごとのグループ化。習慣のidを基準に結果をグループ化し、習慣ごとに件数がまとまるようにしています。

LINE 224: 並び順の指定。集計結果を習慣のid順に並べるよう指定しています。これで表示順が安定します。

LINE 225: クエリ組み立ての完了。ここまでの条件を組み合わせたクエリの組み立てが完了する行です。次の行で実際に実行されます。

LINE 226: 集計クエリの実行。組み立てたクエリを実行し、習慣ごとの達成件数を含む結果をrowsとして取得しています。LINE 227: 集計結果の応答開始。

取得した集計結果をクライアントへ返すためのリスト作成を開始しています。LINE 228: 集計結果の整形内容。習慣のid、名前、達成回数を一件ずつ辞書形式にまとめています。

応答するJSONの各要素の形を決めています。LINE 229: 集計結果の展開処理。取得したrowsの各行をhid、name、totalに分解しながら、先ほどの辞書へ変換しています。

LINE 230: 集計結果リストの完成。習慣ごとの集計結果をまとめたリストがここで完成します。この行でreturnするリストの内容が確定します。

RUN 6/6: 達成回数を集計するAPIを動かす。習慣を登録して1回記録し、集計APIが達成回数を返すことを確認します。CHECK 6/6: 途中実行に成功。

[{'habit_id': 1, 'name': '毎朝ストレッチをする', 'total': 1}] RETURN 06: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 234: 確認対象ルートの定義。

動作確認したい画面のパスをリストとしてまとめています。ここではトップ画面の"/"のみが対象です。PREVIEW: 完成した習慣記録APIを操作。

入力・送信・結果表示を実際のブラウザ画面で確認します。UPLOAD: 実際の入力データでアプリを操作。習慣を登録して達成を記録し、集計ボタンでその習慣の達成回数が1回と表示されることを確認できた 確認1/5: 習慣を登録する。

習慣記録APIに習慣名を送り、登録結果のJSONを確認します。確認2/5: 登録した習慣を一覧する。習慣を2件登録し、一覧APIが両方を返すことを確認します。

確認3/5: 達成を記録する。登録した習慣に達成を記録し、達成日を含む応答を確認します。確認4/5: 達成回数を集計する。

同じ習慣に2回達成を記録し、集計APIが2回と数えることを確認します。確認5/5: 重複した習慣を拒否する。同じ習慣名を2回登録し、2回目が409で拒否されることを確認します。

学習内容のまとめ。@app.postでPOSTのエンドポイントを定義するsqlite:///habits.dbにファイルとして保存する 登録した習慣にidが採番されるSQLiteのhabits.dbにデータを保存する 小さく実行確認しながら完成状態まで段階的に組み立てる エンディング。Python研修はCodeCampでご確認ください。

PythonのFastAPIとSQLModelとは

今回使用する主要なライブラリについて、役割と使い分けを順番に確認します。

FastAPIで型を活かしてREST APIを作る仕組み

FastAPIは、Pythonの型ヒントを活かして少ないコードでREST APIを構築できるWebフレームワークです。関数にデコレータを付けるだけでURLとHTTPメソッドを対応づけられ、リクエストの検証やJSON変換も自動で行われます。今回はこのFastAPI上に、習慣の登録・達成の記録・集計という3種類のエンドポイントを用意しました。

動作確認用のHTML画面も同じアプリから返す構成にしています。

習慣記録APIでFastAPIが担当する処理を挙げます

  • @app.postでPOSTのエンドポイントを定義する
  • HTTPExceptionで400や409を返す
  • パスパラメータのhabit_idを整数で受け取る
  • HTMLResponseで確認画面を返す

SQLModelとSQLiteでデータを保存する役割

SQLModelは、Pythonのクラスとしてテーブルとデータをまとめて定義できるORMライブラリです。SQLAlchemyとPydanticを土台にしているため、モデルを一度書けばデータベース操作と入力検証の両方に使えます。保存先には軽量なファイルデータベースのSQLiteを選び、habits.dbという1つのファイルに記録します。

サーバーを別途立てる必要がなく、初心者でも手元ですぐに動かせる点が特徴です。

SQLModelとSQLiteでデータを保存する仕組みの要素を挙げます

  • sqlite:///habits.dbにファイルとして保存する
  • create_engineでデータベース接続を用意する
  • metadata.create_allでテーブルを作成する
  • Sessionでadd・commit・refreshを行う
さらに詳しく学べるPython研修の詳細はこちら

Python・FastAPIで開発する場合の環境構築

この記事のセットアップ手順と掲載コードは、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 fastapi sqlmodel "uvicorn[standard]"
.\.venv\Scripts\python.exe -m pip install sqlite-utils

macOS・Linuxでは仮想環境内のPythonパスが異なります。今回の動作確認環境とは異なるため、以下は環境差分を補う参考手順です。

python3 -m venv .venv
./.venv/bin/python -m pip install --upgrade pip
./.venv/bin/python -m pip install fastapi sqlmodel "uvicorn[standard]"
./.venv/bin/python -m pip install sqlite-utils
  • SQLite自体のエンジンはPython標準ライブラリのsqlite3に含まれており、SQLModel(内部のSQLAlchemy)は接続文字列sqlite:///habits.dbを指定するだけで自動的にこれを利用しますが、本記事では動作確認のためにSQLiteデータベースを直接覗けるコマンドラインツールsqlite-utilsもあわせてpipでインストールしています。
  • 起動は .\.venv\Scripts\python.exe -m uvicorn habit_api:app --reloadです。ブラウザでhttp://127.0.0.1:8000を開くと確認画面、/docsでSwagger UIが見られます。
  • 初回起動時にカレントディレクトリへhabits.dbが作られます。データを消したいときはこのファイルを削除するか、sqlite-utils tables habits.dbのようなコマンドで中身を確認しながら整理してください。
さらに詳しく学べるPython研修の詳細はこちら

PythonのFastAPIとSQLModelで作る習慣記録APIの要件定義

目的は、FastAPIとSQLModelで習慣の登録・達成記録・達成回数の集計ができるREST APIを作り、ブラウザの確認画面で動作を検証することです。

対象者として、PythonでWeb APIを作った経験が浅く、FastAPIとSQLModelでのデータベース連携の作り方を学びたい人を想定しています。

完成物は、FastAPIとSQLModelで習慣を登録し達成を記録して達成回数を集計できる習慣記録APIと動作確認用のHTML画面です。

実装へ入る前に、機能・品質・受け入れ条件を分けて確認します。

機能要件

  • 習慣名を受け取り習慣テーブルへ登録する
  • 登録済みの習慣を一覧で取得する
  • 選んだ習慣に達成記録を追加する
  • 習慣ごとの達成回数を集計する
  • 動作確認用のHTML画面を返す

非機能要件

  • SQLiteのhabits.dbにデータを保存する
  • 空の習慣名を400エラーで拒否する
  • 同名の習慣を409エラーで拒否する
  • 存在しない習慣IDへの記録を404で拒否する
  • 習慣テーブルと達成記録テーブルの2モデルで構成する
  • check_same_thread=Falseで接続を共有する

実装方針

今回はFastAPIとSQLModelの基本動作を追いやすくするため、習慣記録API本体を1つのPythonファイルへまとめます。

入力、判定、結果表示の役割を分け、実行結果を確認しながら機能を積み上げます。

習慣記録APIを安全に組み立てるための実装方針は次のとおりです。

  • SQLiteのhabits.dbにデータを保存する
  • 空の習慣名を400エラーで拒否する
  • 同名の習慣を409エラーで拒否する
  • 存在しない習慣IDへの記録を404で拒否する
  • 習慣テーブルと達成記録テーブルの2モデルで構成する
  • check_same_thread=Falseで接続を共有する

完成と判断する条件

  1. 習慣登録でidと名前のJSONが返る
  2. 達成記録でdone_onを含むJSONが返る
  3. 集計APIが習慣ごとの達成回数を返す
  4. 達成が無い習慣も集計に0回で現れる
  5. トップ画面に登録・記録・集計の3セクションが並ぶ
さらに詳しく学べるPython研修の詳細はこちら

習慣記録APIを作る際の重要ポイント

習慣記録APIの中心にあるのは、習慣を表すHabitと達成を表すCheckInという2つのテーブルを関連づける設計です。習慣を登録するとidが採番され、達成を記録するたびにそのidと結びついたCheckInが1件ずつ増えていきます。集計では2つのテーブルを外部結合し、習慣ごとに達成回数を数え上げる仕組みです。

1度も達成していない習慣も0回として一覧に残る点が、この設計の狙いになっています。

3つの処理がデータをどうつなぐかを挙げます

  • 登録した習慣にidが採番される
  • 達成のたびにCheckInが1件増える
  • habit_idで習慣と達成記録がつながる
  • 集計結果を習慣ごとに1行で返す

習慣を重複なく登録する処理の流れ

習慣を登録するAPIは、まず受け取った名前の前後の空白を取り除きます。同じ名前が既に登録されていないかを検索し、重複がなければHabitを1件だけ追加する流れです。保存後にidと名前を読み直して返すので、クライアントは採番された結果をすぐに受け取れます。

習慣登録APIが順に実行する処理を示します

  • 習慣名の前後の空白を取り除く
  • 同名の習慣がないかを事前に検索する
  • 保存済みのidと名前を応答で返す

達成記録を日付付きで追加する処理

達成を記録するAPIは、パスで受け取ったhabit_idの習慣が存在するかを先に確認します。見つからなければ404を返し、見つかった場合はCheckInを1件作成する仕組みです。日付を省略したときはその日の日付を使うので、利用者は日付を入力しなくても記録できます。

達成記録APIが行う処理を示します

  • 対象の習慣が存在するかを先に確認する
  • done_on未指定なら当日の日付を使う
  • 記録したidと達成日を応答で返す

達成回数を集計する処理の考え方

集計APIは、HabitとCheckInを外部結合し、習慣ごとにグループ化して達成回数を数えます。func.countとGROUP BYを組み合わせることで、達成が1件もない習慣も0回として結果に含められる点が特徴です。並び順はHabit.idにそろえ、登録した順で一覧を返します。

集計APIがSQLで組み立てる要素を示します

  • HabitとCheckInを外部結合する
  • Habit.idごとにグループ化する
  • 達成が無い習慣も0回で表示する

Python研修一覧はこちら

目的に合うPython研修を一覧形式から探したい方は、ぜひご利用ください。

Python研修を比較する

Java研修一覧はこちら

目的に合うJava研修を一覧形式から探したい方は、ぜひご利用ください。

Java研修を比較する

PHP研修一覧はこちら

目的に合うPHP研修を一覧形式から探したい方は、ぜひご利用ください。

PHP研修を比較する

新入社員研修

目的に合う新入社員研修を一覧形式から探したい方は、ぜひご利用ください。

新入社員研修を比較する

全ての研修からも探したい方はこちら

Pythonで習慣記録APIの完成コード

実装は1つのモジュールにまとめました。データベース設定、モデル定義、リクエスト用のスキーマ、そしてFastAPIアプリと確認用画面という順番で並んでいます。

SQLModelはテーブル定義とデータ検証を1つのクラスでまかなえるのが特徴です。テーブルにするHabitCheckInにはtable=Trueを付けました。

リクエスト受け取り用のHabitCreatePydanticBaseModelで分けます。集計にはSQLAlchemyのfunc.countを使い、GROUP BYで習慣ごとに達成回数をまとめて数えます。

このセクションの用語

engine
データベースへの接続をまとめて管理する窓口です。create_engineで作り、以降の読み書きはこれを通します。
primary_key
各行を一意に識別するための列です。ここではidを主キーにして重複を防ぎます。
foreign_key
別のテーブルの行を指し示す列です。CheckInのhabit_idがHabitのidを参照します。
Pydantic
型ヒントを使ってデータの検証や変換を行うライブラリです。FastAPIのリクエスト検証の土台になっています。
func.count
SQLAlchemyで件数を数える関数です。GROUP BYと組み合わせてグループごとの行数を求めます。
3つのAPIを通るデータの流れ 習慣を登録するAPIでHabitを保存達成を記録するAPIでCheckInを追加集計APIがfunc.countで数えるGROUP BYで習慣ごとに集約件数を画面へ返す
3つのAPIを通るデータの流れ
"""習慣記録API: FastAPI + SQLModel + SQLite

習慣の登録・達成記録・集計を行うREST APIと、動作確認用の簡易画面をまとめたモジュール。
"""

from datetime import date
from typing import Optional

from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from sqlalchemy import func
from sqlmodel import Field, Session, SQLModel, create_engine, select


# データベース設定(SQLite)
DATABASE_URL = "sqlite:///habits.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})


# モデル定義(習慣テーブルと達成記録テーブル)
class Habit(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)


class CheckIn(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    habit_id: int = Field(foreign_key="habit.id", index=True)
    done_on: date = Field(default_factory=date.today)


# リクエストボディのスキーマ
class HabitCreate(BaseModel):
    name: str


class CheckInCreate(BaseModel):
    done_on: Optional[date] = None


# データベースの初期化(テーブル作成)
SQLModel.metadata.create_all(engine)


# アプリと確認用画面のHTML
app = FastAPI(title="習慣記録API")

INDEX_HTML = """<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>習慣記録API デモ</title>
<style>
body { font-family: sans-serif; max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
section { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; }
h2 { margin-top: 0; font-size: 1.1rem; }
input, select, button { font-size: 1rem; padding: 0.4rem; }
button { cursor: pointer; }
li { margin: 0.2rem 0; }
.msg { color: #2563eb; }
</style>
</head>
<body>
<h1>習慣記録API デモ</h1>

<section>
  <h2>1. 習慣を登録</h2>
  <input id="habit-name" type="text" placeholder="例: 毎朝ストレッチをする">
  <button id="add-habit">登録する</button>
  <ul id="habit-list"></ul>
</section>

<section>
  <h2>2. 達成を記録</h2>
  <select id="habit-select"></select>
  <button id="record-btn">達成を記録</button>
  <p id="record-msg" class="msg"></p>
</section>

<section>
  <h2>3. 達成状況を集計</h2>
  <button id="summary-btn">集計する</button>
  <div id="summary"></div>
</section>

<script>
async function loadHabits(selectedId) {
  const res = await fetch('/api/habits');
  const habits = await res.json();
  const list = document.getElementById('habit-list');
  const select = document.getElementById('habit-select');
  list.innerHTML = '';
  select.innerHTML = '';
  for (const h of habits) {
    const li = document.createElement('li');
    li.textContent = h.name;
    list.appendChild(li);
    const opt = document.createElement('option');
    opt.value = h.id;
    opt.textContent = h.name;
    select.appendChild(opt);
  }
  if (selectedId) select.value = String(selectedId);
}

document.getElementById('add-habit').addEventListener('click', async () => {
  const name = document.getElementById('habit-name').value;
  const res = await fetch('/api/habits', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({name})
  });
  if (res.ok) {
    const habit = await res.json();
    document.getElementById('habit-name').value = '';
    await loadHabits(habit.id);
  }
});

document.getElementById('record-btn').addEventListener('click', async () => {
  const habitId = document.getElementById('habit-select').value;
  const msg = document.getElementById('record-msg');
  if (!habitId) { msg.textContent = '先に習慣を登録してください'; return; }
  const res = await fetch(`/api/habits/${habitId}/checkins`, {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({})
  });
  if (res.ok) {
    const data = await res.json();
    msg.textContent = `達成を記録しました(${data.done_on})`;
  }
});

document.getElementById('summary-btn').addEventListener('click', async () => {
  const res = await fetch('/api/habits/summary');
  const rows = await res.json();
  const div = document.getElementById('summary');
  div.innerHTML = '';
  const ul = document.createElement('ul');
  for (const r of rows) {
    const li = document.createElement('li');
    li.textContent = `${r.name} — 達成${r.total}回`;
    ul.appendChild(li);
  }
  div.appendChild(ul);
});

loadHabits();
</script>
</body>
</html>
"""


# 動作確認用のトップ画面
@app.get("/", response_class=HTMLResponse)
def index():
    return INDEX_HTML


# 習慣を登録するAPI
# リクエストで受け取った習慣名を検証したうえで、Habitテーブルへ新規レコードとして
# 書き込み(INSERT)、コミット後に採番されたidを含めてクライアントへ返す。
@app.post("/api/habits")
def register_habit(payload: HabitCreate):
    """習慣を登録するAPI: 習慣名をSQLiteのhabitテーブルへ新規登録(INSERT)する。"""
    name = payload.name.strip()
    if not name:
        raise HTTPException(status_code=400, detail="習慣名を入力してください")

    with Session(engine) as session:
        # 同名の習慣が既に登録済みでないかを確認してから新規レコードを作成する
        existing = session.exec(select(Habit).where(Habit.name == name)).first()
        if existing is not None:
            raise HTTPException(status_code=409, detail="同じ名前の習慣が既に登録されています")

        habit = Habit(name=name)
        session.add(habit)  # 新規データをセッションに登録
        session.commit()  # SQLiteへ書き込み(INSERT)を確定
        session.refresh(habit)  # 採番されたidなどをDBから読み直す

        # 登録直後にDBへ実際に保存されたことを確認してから応答する
        saved = session.get(Habit, habit.id)
        if saved is None:
            raise HTTPException(status_code=500, detail="習慣の登録に失敗しました")

        return {"id": saved.id, "name": saved.name}


# 登録済みの習慣を一覧するAPI
@app.get("/api/habits")
def list_habits():
    with Session(engine) as session:
        habits = session.exec(select(Habit).order_by(Habit.id)).all()
        return [{"id": h.id, "name": h.name} for h in habits]


# 習慣の達成を記録するAPI
@app.post("/api/habits/{habit_id}/checkins")
def create_checkin(habit_id: int, payload: CheckInCreate):
    with Session(engine) as session:
        habit = session.get(Habit, habit_id)
        if habit is None:
            raise HTTPException(status_code=404, detail="習慣が見つかりません")
        done_on = payload.done_on or date.today()
        checkin = CheckIn(habit_id=habit_id, done_on=done_on)
        session.add(checkin)
        session.commit()
        session.refresh(checkin)
        return {"id": checkin.id, "habit_id": habit_id, "done_on": done_on.isoformat()}


# 習慣の達成状況を集計するAPI
@app.get("/api/habits/summary")
def summarize():
    with Session(engine) as session:
        stmt = (
            select(Habit.id, Habit.name, func.count(CheckIn.id))
            .outerjoin(CheckIn, CheckIn.habit_id == Habit.id)
            .group_by(Habit.id)
            .order_by(Habit.id)
        )
        rows = session.exec(stmt).all()
        return [
            {"habit_id": hid, "name": name, "total": total}
            for hid, name, total in rows
        ]


# 動作確認したい画面
ROUTES = ["/"]

コード全文は上の折り畳みに入れてあるので、全部を上から読む必要はありません。ここでは特に重要な部分だけを抜き出して、何をしているのか順番に見ていきます。

create_engineでSQLiteへ接続

DATABASE_URL = "sqlite:///habits.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})

habits.dbというファイルを指す接続文字列を作り、create_engineで読み書きの窓口となるエンジンを用意します。check_same_threadFalseにして、FastAPIから同じ接続を使えるようにしています。

HabitモデルをFieldで定義

class Habit(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)

習慣を表すテーブルのクラスです。idを主キーにして各行を区別し、nameには検索を速くする索引(index)を付けています。

CheckInをforeign_keyで関連付け

class CheckIn(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    habit_id: int = Field(foreign_key="habit.id", index=True)
    done_on: date = Field(default_factory=date.today)

達成1回分を表すテーブルです。habit_idHabitのidを指す外部キーになっており、done_onは指定が無ければ今日の日付が自動で入ります。

HabitCreateでリクエストを検証

class HabitCreate(BaseModel):
    name: str

登録時に受け取るJSONの形を決めるスキーマです。テーブル用のクラスとは分け、必要なnameだけを受け取るようにして入力を検証します。

create_allでテーブルを自動生成

SQLModel.metadata.create_all(engine)

定義したモデルをもとに、まだ無いテーブルをデータベース側へ作ります。この一行を起動時に通すことで、初回アクセスから読み書きできる状態になります。

fetchで習慣一覧を取得する

async function loadHabits(selectedId) {
  const res = await fetch('/api/habits');
  const habits = await res.json();

確認画面のJavaScriptから習慣一覧のAPIをfetchで呼び出しています。返ってきたJSONをjson()で受け取り、あとで一覧やプルダウンに反映します。

参考:

©SQLModel公式ドキュメント

SQLModel is based on Python type annotations, and powered by Pydantic and SQLAlchemy.


Python基礎・実践(Django)

企業・法人向けのPython研修では、基礎から応用まで体系的に学べます。

Python研修の詳細

DX社員研修

企業・法人向けのDX研修では、実務に繋がるリスキリングでITレベルを向上させます。

DX研修の詳細

Javaエンジニア育成研修

企業・法人向けのJavaエンジニア育成研修では、Javaの基礎から応用まで確実に習得できます。

Java研修の詳細

新卒・新入社員向け研修

企業・法人に新入社員・新卒社員に向けたプログラミング研修を提供しています。

新入社員研修の詳細

コードキャンプのIT研修を全て見る

習慣記録APIの動作確認

起動後にルート「/」へアクセスすると、登録・記録・集計の3セクションが並んだ確認用画面が表示されました。操作前は記録がなく、一覧は空の状態です。

入力欄に習慣名を打ち込み登録ボタンを押すと、その習慣が一覧とプルダウンに追加されます。

続けて達成を記録し集計ボタンを押したところ、その習慣の達成回数が1回と表示されることを確認できました。

登録から集計までブラウザ操作だけで一通り動き、すべて手元のサーバーへのアクセスで完結するので外部通信なしで確認できます。

このセクションの用語

ルート
WebアプリのトップにあたるURL「/」のことです。ここでは確認用のHTML画面を返します。
プルダウン
選択肢を一覧から選ぶ入力部品です。登録済みの習慣を選んで達成を記録します。

画面で実際に試した操作の順番です。

  1. ルート「/」を開いて確認画面を表示する
  2. 習慣名を入力して登録ボタンを押す
  3. プルダウンで習慣を選び達成を記録する
  4. 集計ボタンを押して達成回数を確認する
操作前の画面: / (HTTP 200)
操作前の画面: / (HTTP 200)
習慣を登録して達成を記録し、集計ボタンでその習慣の達成回数が1回と表示されることを確認できた
習慣を登録して達成を記録し、集計ボタンでその習慣の達成回数が1回と表示されることを確認できた
実ブラウザ操作: 習慣を登録して達成を記録し、集計ボタンでその習慣の達成回数が1回と表示されることを確認できた
実ブラウザ操作: 習慣を登録して達成を記録し、集計ボタンでその習慣の達成回数が1回と表示されることを確認できた
さらに詳しく学べるPython研修の詳細はこちら

PythonのFastAPIとSQLModelのエラー対処

APIを動かす過程で、初心者が一般につまずきやすいエラーをまとめました。多くはテーブル作成やリクエストの形式に関するもので、原因さえ分かれば対処は難しくありません。

このセクションの用語

check_same_thread
SQLiteの接続を複数スレッドから使うかを決める設定です。FastAPIではFalseにして共有できるようにします。
ステータスコード
リクエストの結果をHTTPが数字で表す仕組みです。422は送ったデータの形式が正しくないことを示します。
エラー例 原因 対処
no such table: habit テーブル作成前にAPIへアクセスした 起動時にSQLModel.metadata.create_allを実行する
SQLite objects created in a thread... 別スレッドから同じ接続を使った create_engineでcheck_same_thread=Falseを渡す
422 Unprocessable Entity bodyにnameが無い・型が違う HabitCreateが求めるJSONを送る
404 Not Found 未登録のhabit_idを指定した 先に習慣を登録してからIDを渡す
Address already in use 同じポートで多重に起動した 既存プロセスを止めるかポートを変える

習慣記録APIで注意したい点

習慣と達成記録を2つのモデルに分けると、達成のたびにCheckInが1行増えるだけで済みます。あとから回数を柔軟に数えられるのが利点です。

集計ではSQLAlchemyのfunc.countGROUP BYを組み合わせ、習慣ごとの達成回数をまとめて取得します。データベース側で数えるほうが素直な書き方です。

最初のアクセスでテーブルが無いと感じたら、SQLModel.metadata.create_allを起動時に通しているか確認します。ポイントとしては、テーブル作成と接続設定、集計方法の3点をおさえることです。

POINT

テーブル作成:create_allを先に実行

接続共有:別スレッド利用に注意

集計:func.countで回数を数える

さらに詳しく学べるPython研修の詳細はこちら

PythonのFastAPIとSQLModelで作る習慣記録APIの活用例

習慣記録という仕組みは、日々の行動を記録して回数を振り返る用途に幅広く応用できます。登録・記録・集計の3つのAPIがそろっているので、画面側を差し替えるだけで様々な場面に転用できます。

このセクションの用語

バックエンド
画面の裏側でデータの保存や処理を担う部分です。今回のAPIがこの役割にあたります。
使える場面 具体的な使い方
個人の習慣管理アプリ 登録APIで習慣を追加し、達成APIで毎日の記録を残す
チームの学習トラッキング メンバーごとに習慣を登録し、集計APIで達成回数を比べる
スマホアプリのバックエンド FastAPIが返すJSONを画面から呼び出し、達成状況を表示する
業務の定例タスク記録 点検や清掃などの定例を習慣として登録し、実施回数を集計する
さらに詳しく学べるPython研修の詳細はこちら

習慣記録API開発のまとめ

今回はPythonのFastAPISQLModelSQLiteを組み合わせ、習慣の登録・達成記録・集計を行うAPIを作りました。モデルを2つに分け、func.countGROUP BYで達成回数を数える構成です。

実際にブラウザから習慣を登録して達成を記録し、集計ボタンで達成回数が1回と表示されることを確認できました。小さな構成でも、REST APIの一連の流れを一通り体験できるのが魅力です。

まずはhabits.dbが作られること、次に登録と集計が動くことを順に確かめると、つまずきにくくなります。ここから項目を増やしたり画面を作り込んだりと、拡張の土台にもなります。

さらに詳しく学べるPython研修の詳細はこちら

参考にした一次情報

  1. ^ FastAPI公式ドキュメント. https://fastapi.tiangolo.com/, (参照26-07-29).
  2. ^ SQLModel公式ドキュメント. https://sqlmodel.tiangolo.com/, (参照26-07-29).
  3. ^ SQLModel with FastAPIチュートリアル. https://sqlmodel.tiangolo.com/tutorial/fastapi/, (参照26-07-29).
  4. ^ SQLite公式サイト. https://www.sqlite.org/index.html, (参照26-07-29).

※内容は執筆時点のものです。ライブラリやサイトの仕様は変わる可能性があるため、公式ドキュメントもあわせてご確認ください。

ITやプログラミングに関するコラム


ITやプログラミングに関するニュース

ブログに戻る

コメントを残す

コメントは公開前に承認される必要があることにご注意ください。

企業・法人向けのIT・プログラミング・生成AI研修を探す、比較する - IT・プログラミングを知って学べるコネクトメディア CodeCampが提供するDX人材育成が可能なプログラミングやITが学べる公開講座 - IT・プログラミングを知って学べるコネクトメディア コードキャンプが提供する無料で学べるプログラミングスクール講座 - IT・プログラミングを知って学べるコネクトメディア コードキャンプDX人材育成研修 - IT・プログラミングを知って学べるコネクトメディア 3.5日の研修で、年間1,600時間の削減効果が見込まれる。東京きらぼしフィナンシャルグループのDX人材育成事例 - IT・プログラミングを知って学べるコネクトメディア 配属3ヶ月で30%の生産性向上を実現するいよぎんコンピュータサービスの新人研修に迫る - IT・プログラミングを知って学べるコネクトメディア 金融業界の業務効率化を加速するニッセイアセットマネジメントの生成AI×GAS活用研修事例 - IT・プログラミングを知って学べるコネクトメディア 【製造業のDX人材育成事例】デジタル人材の即戦力化を実現する、日本ガイシ株式会社の異動者向オンボーディング研修 - ITやプログラミングを知って学べるコネクトメディア フューチャーアーキテクト株式会社が実現した新入社員向けIT研修プログラムでタスクフォース制度が主体的な学びと成長を生み出す - IT・プログラミングを知って学べるコネクトメディア コードキャンプDX人材育成研修 - IT・プログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/【IT新入社員研修】オンラインとオフラインの最適バランスを実現したFutureOneの導入事例 - IT・プログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/【新入社員研修】柔軟なハイブリッド型Java研修で実現した新卒20名の成長と成果|サークレイス株式会社 - ITやプログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/現場により近いところにデジタルを根付かせるDX基礎講座研修|株式会社ブリヂストン - ITやプログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/業務の効率化・DX推進に向けたIT人材育成への第一歩|株式会社カナエ - ITやプログラミングを知って学べるコネクトメディア 企業・法人向けのIT・プログラミング研修 - ITやプログラミングを知って学べるコネクトメディア

新着記事

対象者別で探す

子供(小学生・中学生・高校生)向け
プログラミング教室検索する

子供(小学生・中学生・高校生)がロボットやプログラミング言語を学ぶことができるオフラインからオンラインスクールを検索、比較することが可能です。

子供(小学生・中学生・高校生)
プログラミング教室検索する

ITやプログラムなどの
最新情報を検索する

日々、新しいITやプログラミング言語の情報が流れていきますが、特定の情報を時系列でニュースやコラムを確認することができます。

ITやプログラムなどの
最新情報を検索する