こんにちは、かつコーチです。
ここまでの記事でルーティングとテンプレートを扱ってきましたが、実際のアプリではデータを保存・取得する仕組みが欠かせません。
この記事では、Flaskでデータベースを扱うための定番ライブラリFlask-SQLAlchemyを使って、基本的なCRUD操作を実装します。
Flask-SQLAlchemyとは?
ORMでSQLを直接書かずにDB操作する
ORM(Object-Relational Mapping)とは、データベースのテーブルをPythonのクラスとして扱えるようにする仕組みです。
Flask-SQLAlchemyは、PythonのORMライブラリSQLAlchemyをFlaskで使いやすくラップしたものです。
ORMを使うと、SQL文を直接書かずに、Pythonのコードだけでデータの追加・取得・更新・削除ができます。
pip install flask-sqlalchemy
CRUDとは?
CRUD(クラッド)とは、データ操作の基本となる4つの動作の頭文字を取った言葉です。
- Create(作成)
- Read(読み取り)
- Update(更新)
- Delete(削除)
ほとんどのWebアプリは、このCRUDの組み合わせでできていると言っても過言ではありません。
基本の書き方・実装手順
手順1: データベースの初期設定
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
db = SQLAlchemy(app)
学習段階では、ファイル1つで完結するSQLite(軽量なデータベース。追加のサーバー起動が不要)を使うのが手軽でおすすめです。
本番運用ではPostgreSQLやMySQLに切り替えることが多いですが、コードの書き方自体はほとんど変わりません。
手順2: モデル(テーブル定義)を作成する
class Task(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
done = db.Column(db.Boolean, default=False)
def __repr__(self):
return f"<Task {self.id}: {self.title}>"
db.Modelを継承したクラスが、そのままデータベースの1テーブルに対応します。
db.Columnでカラム(列)の型や制約を定義し、nullable=Falseは「空欄を許さない」という制約です。
手順3: テーブルを実際に作成する
モデルを定義しただけでは、まだデータベースにテーブルは作られません。
with app.app_context() as ctx:
db.create_all()
app_context(Flaskアプリの設定情報にアクセスできる状態にするための仕組み)の中でdb.create_all()を実行すると、定義したモデルに対応するテーブルが作成されます。
手順4: CRUD操作をコードで実装する
from flask import Flask, request, jsonify
# Create(作成)
@app.route("/tasks", methods=["POST"])
def create_task():
title = request.json.get("title")
task = Task(title=title)
db.session.add(task)
db.session.commit()
return jsonify({"id": task.id, "title": task.title}), 201
# Read(読み取り)
@app.route("/tasks", methods=["GET"])
def get_tasks():
tasks = Task.query.all()
return jsonify([{"id": t.id, "title": t.title, "done": t.done} for t in tasks])
# Update(更新)
@app.route("/tasks/<int:task_id>", methods=["PUT"])
def update_task(task_id):
task = Task.query.get_or_404(task_id)
task.done = request.json.get("done", task.done)
db.session.commit()
return jsonify({"id": task.id, "done": task.done})
# Delete(削除)
@app.route("/tasks/<int:task_id>", methods=["DELETE"])
def delete_task(task_id):
task = Task.query.get_or_404(task_id)
db.session.remove(task) if False else db.session.delete(task)
db.session.commit()
return "", 204
流れとしては、Create・Update・Deleteはdb.sessionを経由して変更を溜め、最後にdb.session.commit()でまとめてデータベースに反映する点がポイントです。
つまずきやすい設定・注意点
db.session.commit()を忘れる
db.session.add(task)やtask.done = Trueのように変更を加えただけでは、まだデータベースには保存されていません。
必ず最後にdb.session.commit()を呼ぶ必要があります。
コミットを忘れると、変更した「つもり」でデータが消えてしまうので注意してください。
get_or_404で存在しないデータを安全に扱う
Task.query.get(task_id)だとデータが存在しない場合にNoneが返るだけですが、Task.query.get_or_404(task_id)を使うと、存在しない場合に自動的に404エラーページを返してくれます。
存在チェックのif文を毎回書かなくて済むため、コード量を減らせます。
よくあるつまずきポイント・エラー対処
RuntimeError: Working outside of application context
❌ Before:app.app_context()を使わずに、ファイルの読み込み時点でいきなりdb.create_all()を実行してしまい、RuntimeError: Working outside of application contextが出る
db = SQLAlchemy(app)
db.create_all() # エラーになる
✅ After:with app.app_context():のブロック内で実行する
db = SQLAlchemy(app)
with app.app_context():
db.create_all()
私が最初にこのエラーに遭遇したときは、「app_context」という言葉自体になじみがなく、何が起きているのか分かりませんでした。
Flaskは複数のリクエストを同時に処理できるように、「今どのアプリの設定を使っているか」を明示的に管理する仕組みを持っています。
db.create_all()のようにアプリの設定(データベースの接続先など)を必要とする処理は、この「文脈(コンテキスト)」の中でしか実行できない、と覚えておくと理解しやすくなります。
sqlite3.OperationalError: no such table
❌ Before:モデルの定義を後から追加・変更したのに、既存のapp.dbファイルを削除せずdb.create_all()を実行し、sqlite3.OperationalError: no such table: taskのようなエラーに遭遇する
✅ After:開発中でデータを消しても問題ない場合は、一度app.dbファイルを削除してからdb.create_all()を再実行する。本番運用ではこの記事の応用パートで紹介するFlask-Migrateを使う
db.create_all()は「まだ存在しないテーブル」しか作成しないため、既存テーブルのカラム変更などには対応してくれません。
この制約を知らずに、モデルを変更したのにデータベースの中身が反映されないと悩んでしまうケースは非常に多いです。
応用・一歩先の使い方
テーブル同士のリレーションを定義する
実際のアプリでは、複数のテーブルを関連付けることがよくあります。
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
tasks = db.relationship("Task", backref="user", lazy=True)
class Task(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100))
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
db.relationshipとdb.ForeignKeyを組み合わせることで、「1人のユーザーが複数のタスクを持つ」といった関係を表現できます。
スキーマ変更にはFlask-Migrateを使う
db.create_all()はテーブルが存在しない場合の作成しかできないため、実運用ではカラム追加などの変更を安全に反映する仕組みが必要です。
次回の記事「Flask-Migrateでマイグレーションを管理する」で、その方法を詳しく解説します。
まとめ
この記事のポイント
- Flask-SQLAlchemyはSQLを書かずにPythonのコードでDB操作ができるORM
db.Modelを継承したクラスがテーブル定義になる- CRUDは
db.session.add/commit(作成・更新)と.delete(削除)、.query(読み取り)で実装する app_contextの外でDB操作をするとエラーになる点に注意- テーブル構造の変更には
db.create_all()ではなくマイグレーションが必要
次に読むべき記事
データベースの基本操作ができるようになったら、テーブル構造の変更を安全に行う方法を学びましょう。
「Flask-Migrateでマイグレーションを管理する」に進んでください。
タグ: Flask, 中級者向け, データベース