はい、承知いたしました。Flask-SQLAlchemyを使ったデータベース操作に関する、初心者向けの詳細な記事を約5000語で記述し、直接表示します。
【初心者向け】Flask-SQLAlchemyで始めるデータベース操作
Webアプリケーション開発において、データの永続化(保存)は避けて通れません。ユーザー情報、投稿記事、商品リストなど、様々なデータを安全かつ効率的に管理するために、データベースは必須の技術です。
Pythonの軽量WebフレームワークであるFlaskを使ってWebアプリケーションを開発する際、データベース操作を強力にサポートしてくれるのが「Flask-SQLAlchemy」という拡張機能です。SQLAlchemyという高機能なORM(Object-Relational Mapper)ライブラリを、Flaskで使いやすくするためのラッパーです。
この記事では、Web開発やデータベース操作が初めてという方でも安心して取り組めるように、Flask-SQLAlchemyを使ったデータベース操作の基本をゼロから丁寧に解説します。環境構築から始め、データベースモデルの定義、基本的なデータの追加・読み込み・更新・削除(CRUD操作)、さらにはリレーションシップの扱い方まで、実践的なコード例とともに詳しく見ていきましょう。
この記事を読めば、Flaskアプリケーションにデータベース機能を組み込み、動的なデータを持つWebアプリケーションを開発するための基礎がしっかりと身につくはずです。
さあ、Flask-SQLAlchemyの世界へ一緒に飛び込みましょう!
この記事で学ぶこと
- なぜWeb開発にデータベースが必要なのか
- Flask-SQLAlchemyとは何か、そのメリット
- 開発環境の準備(Python、仮想環境、Flask、Flask-SQLAlchemyのインストール)
- Flask-SQLAlchemyの基本的な設定と初期化
- データベースモデルの定義(テーブル設計)
- データベースの作成と、モデル変更に対応するためのマイグレーション
- データの追加(Create)方法
- データの読み込み(Read)方法(全件取得、条件指定、並べ替えなど)
- データの更新(Update)方法
- データの削除(Delete)方法
- テーブル間の関連(リレーションシップ)の定義と操作
- 簡単な実践例を通じて、学んだことの応用
対象読者
- Pythonの基本的な文法は理解している方
- Flaskを使った簡単なWebアプリケーションは作ったことがある方(Hello WorldレベルでもOK)
- データベースやSQLの知識はほとんどない、あるいは全くない方
- Flaskアプリケーションにデータベース機能を組み込みたい方
なぜデータベースが必要か?
Webアプリケーションが扱うデータは、ユーザーが入力したもの、システムが生成したものなど多岐にわたります。これらのデータは、アプリケーションを停止しても失われず、いつでも取り出せるように保存しておく必要があります。これが永続化です。
データを永続化する方法はいくつかありますが、データベースを使うのが最も一般的で効率的です。なぜなら、データベースは以下のようなメリットを提供するからです。
- 大量のデータを効率的に管理: 構造化された形でデータを整理し、高速な検索や集計を可能にします。
- データの整合性と信頼性: 制約(主キー、外部キーなど)を設けることで、データの矛盾を防ぎ、正確性を保ちます。トランザクション機能により、一連の操作をまとめて実行したり取り消したりでき、処理の途中でシステムが停止してもデータが壊れるのを防ぎます。
- 複数ユーザーからの同時アクセス: 複数のユーザーが同時にデータの読み書きを行っても、競合を適切に制御し、安全に処理できます。
- セキュリティ: アクセス権限を設定することで、不正なデータアクセスを防ぎます。
- スケーラビリティ: データ量の増加やアクセス数の増加に対応しやすい構造になっています。
ファイルに直接データを保存する方法もありますが、複雑なデータの関連性を扱ったり、高速な検索や同時アクセスに対応したりするのは非常に困難です。そのため、多くのWebアプリケーションではデータベースが利用されています。
なぜFlask-SQLAlchemyを使うのか?
データベース操作には、通常SQL(Structured Query Language)という専門の言語を使います。SQL文を書くことで、データの検索、追加、更新、削除などを行います。
しかし、Pythonコードの中に直接SQL文を文字列として埋め込むのは、以下の理由からあまり推奨されません。
- 可読性の低下: PythonコードとSQLが混ざり合い、コードが読みにくくなります。
- 保守性の低下: データベースのスキーマ(テーブル構造)が変更された場合、関連するすべてのSQL文を修正する必要があり、手間がかかります。
- データベースの切り替えの困難さ: データベースシステム(例: PostgreSQL, MySQL, SQLite)によってSQLの記法や機能に微妙な違いがあるため、他のデータベースに切り替えたい場合にコードの大幅な修正が必要になることがあります。
- SQLインジェクションの危険性: ユーザーからの入力値を元に動的にSQL文を生成する場合、適切な対策をしないと悪意のあるコードを実行されてしまう「SQLインジェクション」というセキュリティリスクが発生します。
ここで登場するのがORM (Object-Relational Mapper) です。ORMは、データベースのテーブルとPythonのオブジェクト(クラスやインスタンス)を対応付け(マッピング)してくれるツールです。
ORMを使うことで、私たちはSQL文を直接書く代わりに、Pythonのクラスやオブジェクトを操作するだけでデータベースのデータを扱えるようになります。例えば、「ユーザーを追加する」という操作は、SQLでINSERT INTO users ...
という文を書く代わりに、Pythonでuser = User(...)
, db.session.add(user)
, db.session.commit()
のようにオブジェクトを操作するコードで実現できます。
SQLAlchemyは、Pythonで最も有名で強力なORMライブラリの一つです。非常に柔軟で高機能ですが、その分使い始めるのに少し学習コストがかかる側面もあります。
Flask-SQLAlchemyは、このSQLAlchemyをFlaskアプリケーションと簡単に統合するための拡張機能です。Flaskの設定ファイルからデータベース接続情報を読み込んだり、Flaskのアプリケーションコンテキストやリクエストコンテキストと連携したり、セッション管理を簡単に行ったりと、Flaskでのデータベース開発に必要な様々な機能をより手軽に使えるようにしてくれます。
Flask-SQLAlchemyを使うメリットは以下の通りです。
- Flaskとの連携が容易: Flaskの設定ファイルやアプリケーション構造とスムーズに統合されます。
- Pythonicな記述: SQLを書く代わりに、Pythonのオブジェクト指向的な方法でデータベースを操作できます。
- 可読性と保守性の向上: アプリケーションコードとデータベース操作コードが自然に組み合わさり、読みやすく管理しやすくなります。
- データベース非依存性の向上: 基本的な操作であれば、データベースシステムを切り替える際にもコードの変更が最小限で済みます(完全に不要になるわけではありませんが、SQLを直接書くよりははるかに容易です)。
- セキュリティ: ORMが適切な処理を行うため、SQLインジェクションのリスクを低減できます(ただし、ORMを使っても安全ではない書き方もあるので注意は必要です)。
初心者にとって、SQLを学ぶよりも、使い慣れたPythonのオブジェクト操作でデータベースを扱えるようになるのは大きなメリットです。Flaskアプリケーション開発を始めるなら、Flask-SQLAlchemyの利用は強く推奨されます。
準備
まずは開発に必要なツールやライブラリを準備しましょう。
Pythonのインストール
Pythonがインストールされていない場合は、公式サイトからダウンロードしてインストールしてください。最新版(3.8以上推奨)をインストールするのが良いでしょう。
https://www.python.org/downloads/
インストールできたら、ターミナルやコマンドプロンプトで以下のコマンドを実行して、Pythonが正しくインストールされているか確認します。
“`bash
python –version
または
python3 –version
“`
バージョン情報が表示されればOKです。以降のコマンド例ではpython
と表記しますが、環境によってはpython3
と読み替えてください。
仮想環境の作成と使い方
Pythonプロジェクトを開発する際には、仮想環境を使うのが一般的で推奨されています。仮想環境とは、プロジェクトごとに独立したPython実行環境を作成する仕組みです。これにより、プロジェクトAで使うライブラリとプロジェクトBで使うライブラリが競合したり、システムにインストールされているPython環境を汚染したりするのを防ぐことができます。
Python 3.3以降であれば、venv
という仮想環境ツールが標準で含まれています。
プロジェクト用のディレクトリを作成し、その中に移動します。
bash
mkdir flask_database_tutorial
cd flask_database_tutorial
仮想環境を作成します。venv
の部分は仮想環境の名前で、自由に決められますが、慣習的に.venv
やvenv
とすることが多いです。
bash
python -m venv venv
作成した仮想環境を「アクティベート(有効化)」します。
- macOS / Linux:
bash
source venv/bin/activate - Windows (コマンドプロンプト):
bash
venv\Scripts\activate.bat - Windows (PowerShell):
powershell
venv\Scripts\Activate.ps1
仮想環境がアクティベートされると、ターミナルのプロンプトの先頭に仮想環境の名前(例: (venv)
)が表示されます。これで、このターミナルセッションでは仮想環境内のPythonとインストールされたライブラリが使われるようになります。
仮想環境から抜けるには、deactivate
コマンドを実行します。
bash
deactivate
以降の作業は、必ず仮想環境をアクティベートした状態で行ってください。
FlaskとFlask-SQLAlchemyのインストール
仮想環境をアクティベートしたら、必要なライブラリをインストールします。pipというPythonのパッケージ管理システムを使います。
bash
pip install Flask Flask-SQLAlchemy Flask-Migrate
Flask
: Webフレームワーク本体Flask-SQLAlchemy
: SQLAlchemyをFlaskで使うための拡張機能Flask-Migrate
: データベースのスキーマ変更(テーブル構造の変更など)を管理するためのマイグレーションツール。これはFlask-SQLAlchemyと組み合わせて使うことが多いので、ここで一緒にインストールしておきます。内部ではAlembicというツールを使っています。
インストールが完了したら、pip list
コマンドでインストールされているパッケージを確認できます。
bash
pip list
Flask, Flask-SQLAlchemy, Flask-Migrateなどがリストに含まれていればOKです。
プロジェクト構成の検討
シンプルなアプリケーションなので、まずは以下のようなファイル構成で始めましょう。
flask_database_tutorial/
├── venv/ # 仮想環境
├── app.py # アプリケーションのメインファイル
└── config.py # 設定ファイル
慣れてきたら、アプリケーションが大きくなるにつれて、ディレクトリを分割していくことを検討しましょう(例: app
ディレクトリの中に__init__.py
, models.py
, routes.py
などを配置する)。しかし、今回はまず基本を学ぶためにシンプルな構成で進めます。
Flaskアプリケーションの基本構造とFlask-SQLAlchemyの初期化
Flaskアプリケーションを作成し、Flask-SQLAlchemyを使えるように初期設定を行います。
config.py
ファイルを作成し、データベース接続情報を記述します。
“`python
config.py
import os
ベースディレクトリを取得
basedir = os.path.abspath(os.path.dirname(file))
データベースファイルのパスを定義
ここではアプリケーションのルートディレクトリに ‘app.db’ という名前でSQLiteデータベースを作成します
DATABASE_URL = ‘sqlite:///’ + os.path.join(basedir, ‘app.db’)
class Config:
# Flaskアプリケーションの秘密鍵。セッション管理などに必要です。
# 開発中は簡単なものでもOKですが、本番環境では推測されにくい複雑な文字列にしてください。
SECRET_KEY = os.environ.get(‘SECRET_KEY’) or ‘you-will-never-guess’
# データベースURIの設定
# SQLAlchemyにどのデータベースに接続するかを伝えます
# 'sqlite:///' の後のパスは、データベースファイルへの絶対パスです
SQLALCHEMY_DATABASE_URI = DATABASE_URL
# SQLAlchemyイベントシステムの追跡を無効にします
# メモリ使用量を抑えるためにFalseに設定することが推奨されています(特に必要がない限り)
SQLALCHEMY_TRACK_MODIFICATIONS = False
# マイグレーションファイルの保存先ディレクトリ
MIGRATIONS_DIR = os.path.join(basedir, 'migrations')
“`
config.py
では、主に以下の設定を行っています。
SECRET_KEY
: Flaskがセッションなどを安全に扱うために必要な秘密鍵です。本番環境では環境変数から読み込むのが一般的ですが、開発中は一時的に固定値にしておいても構いません。SQLALCHEMY_DATABASE_URI
: これがデータベース接続情報です。sqlite:///
はSQLiteデータベースを指定するURIスキームです。その後ろにデータベースファイルのパスを指定します。os.path.join(basedir, 'app.db')
とすることで、app.py
と同じディレクトリにapp.db
というファイル名でデータベースが作成されるようになります。他のデータベース(PostgreSQL, MySQLなど)を使う場合は、URIの形式が変わります(例:postgresql://user:password@host:port/database
)。SQLALCHEMY_TRACK_MODIFICATIONS
: SQLAlchemyがオブジェクトの変更を追跡する機能を有効にするかどうかの設定です。不要な場合はFalse
にすることでパフォーマンスやメモリ使用量を改善できます。ほとんどの場合はFalse
で問題ありません。MIGRATIONS_DIR
: 後で使うFlask-Migrateが生成するマイグレーションファイルを保存するディレクトリのパスです。
次に、app.py
ファイルを作成し、FlaskアプリケーションとFlask-SQLAlchemyを初期化します。
“`python
app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import Config # config.py から設定を読み込む
Flaskアプリケーションのインスタンスを作成
app = Flask(name)
config.py の設定をアプリケーションに読み込む
app.config.from_object(Config)
SQLAlchemyのインスタンスを作成し、Flaskアプリケーションと連携させる
これで ‘db’ オブジェクトを通じてデータベース操作ができるようになります
db = SQLAlchemy(app)
Flask-Migrateのインスタンスを作成し、Flaskアプリケーションと連携させる
migrate = Migrate(app, db, directory=app.config[‘MIGRATIONS_DIR’])
データベースモデルは通常、ここにインポートするか、
別途 models.py ファイルを作成してそこに記述し、ここからインポートします。
今回は app.py にモデルも記述します(シンプルな例のため)
from models import User, Post # モデルを別のファイルに定義した場合
— データベースモデルの定義(一時的にここに記述します) —
後で models.py に分離することを検討してください
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
# このUserと関連付けられたPostオブジェクトのリストを取得するためのリレーションシップ
# 'Post' は関連するモデルの名前です
# backref='author' は、Postオブジェクトからその著者(Userオブジェクト)にアクセスするための属性名です
# lazy='dynamic' は、関連オブジェクトをクエリとして取得することを意味します(件数が多い場合に便利)
posts = db.relationship('Post', backref='author', lazy='dynamic')
def __repr__(self):
# オブジェクトをprintなどで表示したときの見やすい表現を定義します
return '<User {}>'.format(self.username)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text) # 本文は長い可能性があるのでText型に
timestamp = db.Column(db.DateTime, index=True, default=db.func.now()) # 投稿日時。デフォルト値に関数指定も可能
# 外部キー。Userテーブルのidカラムを参照します。
# ondelete=’CASCADE’ は、参照元のユーザーが削除された場合に、関連する投稿も一緒に削除することを意味します
user_id = db.Column(db.Integer, db.ForeignKey(‘user.id’, ondelete=’CASCADE’))
def __repr__(self):
return '<Post {}>'.format(self.body[:50] + '...') # 本文が長い場合は省略
— ルーティングとビュー関数 —
簡単な動作確認用のルート
@app.route(‘/’)
def index():
return “Hello, Database World!”
アプリケーションの実行(開発用サーバー)
if name == ‘main‘:
app.run(debug=True)
“`
解説:
from flask import Flask
: Flaskクラスをインポートします。from flask_sqlalchemy import SQLAlchemy
: Flask-SQLAlchemyの核となるSQLAlchemy
クラスをインポートします。from flask_migrate import Migrate
: Flask-Migrateクラスをインポートします。from config import Config
: 先ほど作成したconfig.py
からConfig
クラスをインポートします。app = Flask(__name__)
: Flaskアプリケーションのインスタンスを作成します。__name__
は現在のモジュール名で、Flaskが内部的にリソースを探すのに使われます。app.config.from_object(Config)
:config.py
で定義した設定をアプリケーションのコンフィグに読み込みます。db = SQLAlchemy(app)
:SQLAlchemy
クラスのインスタンスを作成し、作成したFlaskアプリケーションインスタンスapp
を渡して初期化します。これで、db
オブジェクトを通じてデータベース操作(モデル定義、クエリ実行など)ができるようになります。migrate = Migrate(app, db, directory=app.config['MIGRATIONS_DIR'])
:Migrate
クラスのインスタンスを作成し、app
とdb
オブジェクトを渡して初期化します。directory
引数でマイグレーションファイルを保存するディレクトリを指定します。- データベースモデルの定義: ここで
User
とPost
という2つのクラスを定義しています。これが後述するORMの「モデル」であり、データベースの「テーブル」に対応します。これらの定義は、db.Model
を継承することでSQLAlchemyによってデータベーステーブルとして扱えるようになります。 - ルーティングとビュー関数: Flaskの基本的な機能として、
/
へのリクエストに対する応答を定義しています。
この時点では、まだデータベースファイル(app.db
)は作成されていません。モデル定義は、あくまでPythonコード上で「こういう構造のテーブルがデータベースに欲しい」と宣言しているだけです。次に、このモデル定義に基づいて実際にデータベースを作成する手順を見ていきましょう。
データベースモデルの定義 (ORM)
先ほどapp.py
の中に記述したUser
クラスとPost
クラスが、データベースモデルの定義です。
“`python
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
posts = db.relationship('Post', backref='author', lazy='dynamic')
def __repr__(self):
return '<User {}>'.format(self.username)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text)
timestamp = db.Column(db.DateTime, index=True, default=db.func.now())
user_id = db.Column(db.Integer, db.ForeignKey(‘user.id’, ondelete=’CASCADE’))
def __repr__(self):
return '<Post {}>'.format(self.body[:50] + '...')
“`
ORMとは何か? (概念説明)
ORM (Object-Relational Mapper) は、オブジェクト指向プログラミング言語(Python)とリレーショナルデータベース(RDB)の間の概念のギャップを埋めるための技術です。
- オブジェクト指向の世界: データをクラスやオブジェクトとして扱います。オブジェクトは属性(プロパティ)を持ち、メソッド(関数)を持ちます。オブジェクト同士は関連(リレーションシップ)を持つことがあります。
- リレーショナルデータベースの世界: データをテーブルに格納します。テーブルは行(レコード)と列(カラム)で構成されます。テーブル同士は主キーと外部キーを使って関連付けられます。
ORMは、PythonのクラスをRDBのテーブルに、Pythonのオブジェクトの属性をテーブルのカラムに、Pythonのオブジェクト間の関連をテーブル間の関連(主キー/外部キー)にマッピングします。これにより、開発者はPythonのオブジェクトを操作するだけで、データベースのデータを操作できるようになります。SQL文を直接書く必要がなくなります。
Flask-SQLAlchemyにおけるモデルクラス(例: User
クラス)は、データベースのテーブルに対応します。モデルクラスの属性(例: username
, email
)は、テーブルのカラムに対応します。モデルクラスのインスタンスは、テーブルの1行(レコード)に対応します。
SQLAlchemyにおけるモデル定義の基本
Flask-SQLAlchemyでは、db.Model
を継承したクラスとしてモデルを定義します。このクラスが、データベーステーブルの設計図となります。
テーブル、カラム、データ型のマッピング
- テーブル名: モデルクラスの名前(例:
User
,Post
)がデフォルトでテーブル名として使われます。クラス名を小文字に変換したものが使われることが多いです(例:User
クラスはuser
テーブル、Post
クラスはpost
テーブル)。__tablename__
クラス属性で明示的にテーブル名を指定することも可能です。 - カラム: クラス属性として
db.Column
インスタンスを定義します。db.Column
の第一引数はカラムのデータ型です。 - データ型: SQLAlchemyはデータベースシステムのネイティブなデータ型を抽象化した様々なデータ型を提供します。
db.Integer
: 整数型db.String(length)
: 可変長文字列型。最大長を引数で指定します。db.Text
: 長文テキスト型。長さ制限なし。db.Boolean
: 真偽値(True/False)。db.DateTime
: 日付と時刻。db.Float
: 浮動小数点数。db.LargeBinary
: バイナリデータ(ファイルや画像など)。- その他多くの型があります。
主キー、外部キー、インデックスなどの制約
db.Column
には、カラムの制約などを指定するためのキーワード引数を渡せます。
primary_key=True
: そのカラムをテーブルの主キーとします。主キーはテーブル内で各行を一意に識別するためのカラムで、通常は自動的に連番が振られる整数型(AUTOINCREMENT)を使います。id = db.Column(db.Integer, primary_key=True)
という定義は非常によく使われます。nullable=False
: そのカラムがNULL(値がない状態)を許容しないようにします。デフォルトはTrue
です。unique=True
: そのカラムの値がテーブル全体で一意でなければならないという制約を付けます。index=True
: そのカラムにインデックスを作成します。インデックスを作成すると、そのカラムを使った検索(WHERE句など)が高速化されますが、データの追加・更新・削除には少しオーバーヘッドがかかります。頻繁に検索条件として使うカラムに設定するのが効果的です。default=value
: レコードが新しく作成される際に、そのカラムにデフォルト値を設定します。値としてPythonの値を直接指定することもできますし、db.func.now()
のようにデータベース側の関数を指定することも可能です。-
server_default=value
: デフォルト値をデータベース側で設定する場合に使います。 -
db.ForeignKey('other_table.other_column')
: そのカラムを外部キーとします。other_table.other_column
の形式で、参照先のテーブル名とカラム名を文字列で指定します。これにより、このカラムの値は参照先のテーブルの指定したカラムに存在する値でなければならないという制約が課せられ、テーブル間の関連が表現されます。ondelete='SET NULL'
,onupdate='CASCADE'
などのオプションも指定できます。ondelete='CASCADE'
は、参照先のレコードが削除されたときに、この外部キーを持つレコードも一緒に削除するという動作を定義します。
簡単なモデルの例(Userモデル)
User
モデルの定義を改めて見てみましょう。
“`python
class User(db.Model):
# id カラム: 整数型、主キー。各ユーザーを一意に識別。自動で連番が振られる。
id = db.Column(db.Integer, primary_key=True)
# username カラム: 最大64文字の文字列型。インデックスが張られ、値は一意でなければならない。
username = db.Column(db.String(64), index=True, unique=True)
# email カラム: 最大120文字の文字列型。インデックスが張られ、値は一意でなければならない。
email = db.Column(db.String(120), index=True, unique=True)
# password_hash カラム: パスワードのハッシュ値を保存。最大128文字の文字列型。
# セキュリティのため、パスワードそのものではなくハッシュ値を保存します。
password_hash = db.Column(db.String(128))
# リレーションシップ: このUserに関連するPostオブジェクトのリスト
# 'Post' は関連先のモデルクラス名
# backref='author' は、Postモデル側からこのUserにアクセスする際の属性名を 'author' にする
# lazy='dynamic' は、postsにアクセスしたときに immediately にロードせず、クエリオブジェクトを返す
posts = db.relationship('Post', backref='author', lazy='dynamic')
# オブジェクトの文字列表現
def __repr__(self):
return '<User {}>'.format(self.username)
“`
この定義から、user
という名前のテーブルが作成され、そのテーブルにはid
, username
, email
, password_hash
というカラムがあることがわかります。id
は主キーであり、username
とemail
は一意な値が格納され、検索効率のためにインデックスが張られます。
モデルの属性とインスタンス
モデルクラス自体はテーブルの「定義」ですが、そのクラスから作成されたインスタンスはテーブルの「行」(レコード)を表します。
例えば、新しいユーザーを追加したいときは、User
クラスのインスタンスを作成します。
“`python
新しいUserオブジェクトを作成
new_user = User(username=’testuser’, email=’[email protected]’)
password_hash はここでは省略しますが、実際にはパスワードをハッシュ化して設定します
new_user.password_hash = generate_password_hash(‘mypassword’)
“`
このnew_user
インスタンスは、まだデータベースには保存されていません。メモリ上に存在するPythonオブジェクトです。このオブジェクトの属性 (new_user.username
, new_user.email
など) に値を代入したり、取得したりすることができます。
このオブジェクトをデータベースに保存するには、後述するセッションを使った操作が必要です。
データベースの作成とマイグレーション
モデルを定義しただけでは、まだデータベースファイル(app.db
)やその中のテーブルは存在しません。定義したモデルに基づいて、実際にデータベースの構造を作成する必要があります。また、アプリケーション開発を進めるにつれて、モデル定義(テーブル構造)を変更したい場合が出てきます。既存のデータが失われないようにテーブル構造を変更する作業をマイグレーションといいます。
Flask-SQLAlchemyとFlask-Migrateを使って、これらの作業を行いましょう。
データベースの作成(SQLiteを例に)
開発初期段階で、データベースにデータが全くない場合は、Flask-SQLAlchemyが提供するdb.create_all()
メソッドを使って、定義した全てのモデルに基づいてデータベースとテーブルを一度に作成できます。
ただし、db.create_all()
はテーブルが既に存在する場合は何もしません。テーブル構造を変更した場合は、db.create_all()
では既存のテーブルは変更されませんし、新しいテーブルが追加されるだけです。したがって、一度アプリケーションを稼働させてデータが保存された後でモデルを変更した場合には、db.create_all()
は使えません。そのような場合は、マイグレーションツールが必要です。
db.create_all()
の使い方:
app.py
があるディレクトリで、Pythonのインタラクティブシェルを起動し、アプリケーションコンテキスト内でdb.create_all()
を実行します。
仮想環境をアクティベートしたターミナルで、以下のコマンドを実行します。
“`bash
アプリケーションコンテキストを有効化
これはFlaskのコマンドで、仮想環境がアクティベートされていれば実行できます
flask shell
“`
flask shell
コマンドは、Flaskアプリケーションのコンテキスト(設定や拡張機能などが使える状態)でPythonシェルを開いてくれます。シェルが起動したら、以下のコードを入力します。
“`python
from app import app, db
app.app_context().push() # アプリケーションコンテキストを手動でプッシュ
db.create_all() # 定義したモデルに基づいてデータベーステーブルを作成
exit() # シェルを終了
“`
app.app_context().push()
は、シェル内でアプリケーションコンテキストを有効にするためのおまじないです。通常、Webリクエストの処理中はFlaskが自動的にコンテキストを管理してくれるのですが、シェルのような外部から実行する場合は手動で有効化する必要があります。
db.create_all()
を実行すると、config.py
で指定したSQLALCHEMY_DATABASE_URI
に基づいてデータベースファイル(app.db
)が作成され、その中にuser
テーブルとpost
テーブルが作成されます。
ファイルエクスプローラーなどで確認すると、app.db
というファイルが作成されているはずです。
なぜマイグレーションが必要か?
アプリケーションは常に進化します。ユーザーモデルに新しいフィールドを追加したり、既存のフィールドのデータ型を変更したり、新しいテーブルを追加したりといった変更は頻繁に発生します。
開発初期でデータベースにデータがないうちは、毎回app.db
ファイルを削除してdb.create_all()
を再実行すれば新しいテーブル構造を反映できます。しかし、アプリケーションが稼働してユーザーデータなどがデータベースに蓄積された後にこの方法を取ると、大切なデータが全て失われてしまいます。
そこで必要になるのがデータベースマイグレーションです。マイグレーションツールは、現在のデータベーススキーマと、コード上のモデル定義との差分を検知し、その差分を解消するためのSQL文(またはそれに相当する操作)を生成してくれます。そして、その生成された差分適用スクリプトを実行することで、既存のデータを維持したまま、安全にデータベースのスキーマを新しいバージョンに更新することができます。
Flask開発では、Flask-Migrateという拡張機能を使うのが一般的です。これは裏側で有名なデータベースマイグレーションツールであるAlembicを利用しています。
Flask-Migrateの導入と使い方
Flask-Migrateを使うための準備は、既にapp.py
でmigrate = Migrate(app, db, directory=app.config['MIGRATIONS_DIR'])
のように初期化を行っているので完了しています。
config.py
でMIGRATIONS_DIR
を設定したように、プロジェクトのルートディレクトリにmigrations
というディレクトリが作成されることになります。
マイグレーションは、flask db
コマンドを使って実行します。flask
コマンドはFlaskアプリケーションのエントリポイントを指定する必要がありますが、app.py
にアプリケーションインスタンス(app
という名前)があれば、Flaskは自動的にそれを検出してくれます。
1. マイグレーション環境の初期化
最初に一度だけ、マイグレーション環境を初期化する必要があります。これにより、マイグレーションを管理するための内部的なファイル(Alembicの設定ファイルや、マイグレーションスクリプトを格納するディレクトリ)が作成されます。
仮想環境をアクティベートしたターミナルで、app.py
があるディレクトリから以下のコマンドを実行します。
bash
flask db init
実行すると、プロジェクトのルートディレクトリにmigrations
ディレクトリとalembic.ini
ファイルが作成されます。
migrations
ディレクトリの中には、環境設定ファイルやversions
というサブディレクトリ(ここに実際のマイグレーションスクリプトが格納されます)が作成されます。
2. マイグレーションスクリプトの生成 (migrate)
モデル定義を変更した後、その変更をデータベースに適用するための「差分スクリプト」を生成します。
例えば、User
モデルにlast_seen
という最終ログイン時刻を記録するカラムを追加したいとします。app.py
のUser
モデル定義を以下のように変更します。
“`python
class User(db.Model):
# … (既存のカラム定義) …
last_seen = db.Column(db.DateTime, default=db.func.now()) # 新しく追加
# ... (既存の__repr__メソッド) ...
“`
モデルを変更したら、以下のコマンドでマイグレーションスクリプトを生成します。
bash
flask db migrate -m "add last_seen to user model"
-m
オプションで、そのマイグレーションがどんな変更を行ったのか説明を付けることができます。これは後で履歴を確認する際に役立ちます。
このコマンドを実行すると、Flask-Migrateは現在のモデル定義と、データベースの現在のスキーマ(または前回のマイグレーション時点のスキーマ)との差分を比較し、その変更を適用するためのPythonスクリプトをmigrations/versions
ディレクトリ内に生成します。生成されるファイル名はタイムスタンプを含むユニークなものになります(例: xxxxxxxxxxxx_add_last_seen_to_user_model.py
)。
3. マイグレーションの実行 (upgrade)
生成されたマイグレーションスクリプトをデータベースに適用し、実際にテーブル構造を変更します。
bash
flask db upgrade
このコマンドを実行すると、migrations/versions
ディレクトリ内の、まだ適用されていないマイグレーションスクリプトが順番に実行され、データベースのスキーマが更新されます。
upgrade
コマンドを実行すると、app.db
ファイル内のuser
テーブルにlast_seen
という新しいカラムが追加されているはずです。既存のユーザーデータがあれば、そのデータはそのまま維持され、新しく追加されたlast_seen
カラムにはデフォルト値(ここではdb.func.now()
による現在時刻)が設定されるか、NULLが許容されていればNULLが入るようになります(カラム定義によります)。
モデル変更時のマイグレーション手順まとめ:
app.py
(またはmodels.py
) のモデル定義を変更する。- 仮想環境をアクティベートしたターミナルで、
flask db migrate -m "変更内容の説明"
コマンドを実行し、マイグレーションスクリプトを生成する。 - 生成されたスクリプトファイル(
migrations/versions/xxxxxxxxxxxx_....py
)を確認し、意図した変更内容になっているかチェックする。(特に複雑な変更の場合は確認が重要) flask db upgrade
コマンドを実行し、データベースにスキーマ変更を適用する。
これで、モデル定義を変更しても安全にデータベース構造を更新できるようになりました。
補足:
flask db history
: これまでに生成されたマイグレーションスクリプトの履歴を確認できます。flask db downgrade <revision>
: 特定のマイグレーションリビジョンに戻る(変更を取り消す)ことができます。<revision>
には、flask db history
で表示されるリビジョンIDを指定します。モデル変更を取り消したい場合などに使いますが、データの整合性には注意が必要です。
重要:
db.create_all()
は、初期状態(データベースファイル自体が存在しないか、テーブルが全くない状態)で一度使うか、マイグレーション環境をセットアップする前の開発初期段階でのみ使うのが良いでしょう。一度マイグレーション環境をセットアップし、データが蓄積され始めたら、以降のスキーマ変更は必ずマイグレーションで行うようにしてください。
基本的なデータベース操作 (CRUD)
データベース操作の基本はCRUDと呼ばれる4つの操作です。
- Create: データの新規作成(追加)
- Read: データの読み込み(取得)
- Update: データの更新
- Delete: データの削除
これらの操作を、Flask-SQLAlchemyを使ってどのように行うのかを見ていきましょう。
セッションの概念 (db.session
)
データベース操作を行う際に最も重要な概念の一つがセッションです。Flask-SQLAlchemy(およびSQLAlchemy)において、セッションはデータベースとの一時的な対話領域です。
セッションは以下の役割を果たします。
- 変更の記録: セッションに対して、オブジェクトの追加、変更、削除といった操作を行います。これらの操作は、すぐにはデータベースに反映されず、セッション内で一時的に記録されます。
- トランザクション管理: 一連の操作を一つのまとまりとして扱います。セッションに対する変更は、最後にコミット (
db.session.commit()
) することで初めてまとめてデータベースに永続的に書き込まれます。コミットするまでの間であれば、途中で操作を取り消す(ロールバックdb.session.rollback()
)ことも可能です。これにより、処理の途中でエラーが発生しても、データベースが中途半端な状態になるのを防ぎ、データの整合性を保つことができます。 - オブジェクトの管理: セッションは、データベースから読み込んだオブジェクトや、新しく作成してセッションに追加したオブジェクトの状態を追跡します。これにより、同じレコードを複数回読み込んでも、同じPythonオブジェクトのインスタンスとして扱われたり、オブジェクトの属性を変更するとセッションがその変更を検知したりします。
Flask-SQLAlchemyでは、db.session
というオブジェクトがアプリケーションの各リクエスト(またはアプリケーションコンテキスト)に対して自動的に提供されます。私たちはこのdb.session
オブジェクトを通じて、データベースに対するあらゆる操作を行います。
C: Create (データの追加)
新しいデータをデータベースのテーブルに追加するには、対応するモデルクラスのインスタンスを作成し、それをセッションに追加してコミットします。
app.py
に、新しいユーザーを追加する簡単なルートを追加してみましょう。
“`python
app.py (既存のコードに追記)
from flask import Flask, request, redirect, url_for, render_template_string # render_template_stringを追加
… (他のインポートと初期化コード) …
— データベースモデルの定義(既存) —
class User(db.Model): …
class Post(db.Model): …
— ルーティングとビュー関数 —
ユーザー追加用のフォームと処理
@app.route(‘/add_user’, methods=[‘GET’, ‘POST’])
def add_user():
if request.method == ‘POST’:
username = request.form.get(‘username’)
email = request.form.get(‘email’)
# パスワードは今回は省略
if username and email:
# 1. モデルインスタンスの作成
new_user = User(username=username, email=email)
# 2. セッションへの追加
# この時点ではまだデータベースに保存されていない
db.session.add(new_user)
# 3. コミット
# ここで初めてセッション内の変更(new_userの追加)がデータベースに書き込まれる
db.session.commit()
# 成功したらユーザー一覧ページなどにリダイレクト
return redirect(url_for('list_users'))
else:
return "ユーザー名とメールアドレスを入力してください", 400 # Bad Request
# GETリクエストの場合はユーザー追加フォームを表示
# 簡単のため、HTMLを直接文字列で記述します
# 通常はJinja2テンプレートを使用します
html_form = """
<h1>ユーザー追加</h1>
<form method="post">
<label for="username">ユーザー名:</label><br>
<input type="text" id="username" name="username"><br><br>
<label for="email">メールアドレス:</label><br>
<input type="email" id="email" name="email"><br><br>
<input type="submit" value="追加">
</form>
<p><a href="{{ url_for('list_users') }}">ユーザー一覧に戻る</a></p>
"""
return render_template_string(html_form, url_for=url_for)
… (indexルートなど、他のルート定義) …
アプリケーションの実行(開発用サーバー)
if name == ‘main‘:
with app.app_context(): # アプリケーションコンテキスト内で実行することで、dbなどが使える
# もしまだデータベースが作成されていない場合はここで作成する
# ただし、マイグレーションを使っている場合は flask db upgrade で行うのが一般的
# db.create_all()
pass # マイグレーションを使うのでコメントアウトまたは削除
app.run(debug=True)
“`
コード解説:
- モデルインスタンスの作成:
new_user = User(username=username, email=email)
のように、モデルクラスを呼び出し、カラム名に対応するキーワード引数で初期値を渡してインスタンスを作成します。id
のような主キーは、通常データベース側で自動生成されるため、ここで指定する必要はありません(指定しても無視されるか、エラーになる場合があります)。 - セッションへの追加:
db.session.add(new_user)
は、作成したnew_user
オブジェクトを現在のセッションに追加します。これは「このオブジェクトをデータベースに保存したい」という意図をセッションに伝えるものです。まだデータベースへの書き込みは行われていません。 - コミット:
db.session.commit()
は、セッションに記録された全ての変更(ここではnew_user
の追加)をまとめてデータベースに書き込む命令です。これが実行されて初めて、データがデータベースに永続化されます。コミットが成功すると、追加されたnew_user
オブジェクトのid
属性に、データベースで自動生成された主キーの値が設定されます。
複数データの追加:
複数のオブジェクトをまとめて追加したい場合は、それぞれのオブジェクトをdb.session.add()
で一つずつ追加するか、db.session.add_all()
を使ってリスト形式でまとめて追加できます。コミットは通常、一連の追加処理の最後に一度だけ行います。
“`python
複数のユーザーを追加する例
user1 = User(username=’user1′, email=’[email protected]’)
user2 = User(username=’user2′, email=’[email protected]’)
db.session.add_all([user1, user2]) # リストで複数のオブジェクトを追加
db.session.commit() # 一度にまとめてコミット
“`
注意点:
db.session.add()
だけではデータは保存されません。必ずdb.session.commit()
を実行してください。- コミットする前であれば、
db.session.rollback()
を呼び出すことで、そのセッションで行った全ての変更を取り消すことができます。これは、例えば複数のオブジェクトを追加する途中でエラーが発生した場合に、データベースを整合性の取れた状態に戻すのに役立ちます。
R: Read (データの読み込み)
データベースに保存されたデータを取得するには、モデルクラスの.query
属性を使います。.query
は、データベースからデータを検索するためのクエリビルダーオブジェクトです。様々なメソッドを組み合わせて、柔軟な検索条件や並べ替え、件数制限などを指定できます。
app.py
に、ユーザー一覧を表示するルートを追加してみましょう。
“`python
app.py (既存のコードに追記)
from flask import Flask, request, redirect, url_for, render_template_string
… (他のインポートと初期化コード) …
— データベースモデルの定義(既存) —
class User(db.Model): …
class Post(db.Model): …
— ルーティングとビュー関数 —
ユーザー一覧を表示するルート
@app.route(‘/users’)
def list_users():
# 全件取得
# User.query は User モデルに対応するテーブルに対するクエリを作成
# .all() はクエリを実行し、結果として得られた全レコードを User オブジェクトのリストとして取得
users = User.query.all()
# 簡単のため、HTMLを直接文字列で記述します
html_list = """
<h1>ユーザー一覧</h1>
<ul>
{% for user in users %}
<li>{{ user.username }} ({{ user.email }})</li>
{% endfor %}
</ul>
<p><a href="{{ url_for('add_user') }}">新しいユーザーを追加</a></p>
"""
return render_template_string(html_list, users=users, url_for=url_for)
… (add_userルートなど、他のルート定義) …
アプリケーションの実行(開発用サーバー)
if name == ‘main‘:
with app.app_context():
# db.create_all() # マイグレーションを使う場合はコメントアウトまたは削除
pass
app.run(debug=True)
“`
コード解説:
User.query
:User
モデル(user
テーブル)に対するクエリを作成するためのオブジェクトです。ここから様々な検索メソッドをチェーン(.query.some_method().another_method()...
)してクエリを構築します。.all()
: 構築したクエリを実行し、条件に合致する全てのレコードを対応するモデルオブジェクトのリストとして取得します。条件を指定しないUser.query.all()
は、user
テーブルの全件を取得します。
様々なデータの読み込み方法:
-
条件を指定して取得 (
.filter_by()
): 特定のカラムの値で絞り込みたい場合に便利です。キーワード引数でカラム名を指定します。“`python
ユーザー名が ‘testuser’ のユーザーを取得
test_user = User.query.filter_by(username=’testuser’).first()
結果は User オブジェクト、または見つからなければ None
“`
-
より柔軟な条件 (
.filter()
): SQLのWHERE句により近い、柔軟な条件を指定したい場合にfilter()
を使います。比較演算子や論理演算子をPythonの式として記述できます。“`python
id が 1 より大きいユーザーを取得
users_gt_1 = User.query.filter(User.id > 1).all()
ユーザー名が ‘testuser’ か ‘admin’ のユーザーを取得
from sqlalchemy import or_ # 複数の条件をORで結合するために必要
users_test_or_admin = User.query.filter(or_(User.username == ‘testuser’, User.username == ‘admin’)).all()メールアドレスが ‘@example.com’ で終わるユーザーを取得 (部分一致)
users_example_email = User.query.filter(User.email.endswith(‘@example.com’)).all()
ユーザー名に ‘test’ を含むユーザーを取得 (部分一致 – LIKE)
users_like_test = User.query.filter(User.username.like(‘%test%’)).all() # SQLのLIKE ‘test%’ に相当
“`比較演算子や文字列操作メソッド (
==
,!=
,>
,<
,>=
,<=
,in_()
,like()
,ilike()
,startswith()
,endswith()
) など、SQLAlchemyが提供する多くの演算子や関数がfilter()
内で利用可能です。 -
単一のレコードを取得:
-
.get(primary_key)
: 主キーの値を指定して単一のレコードを取得する最も簡単な方法です。主キー以外のカラムでは使えません。指定した主キーのレコードが見つかればオブジェクトを返し、見つからなければNone
を返します。“`python
id が 1 のユーザーを取得
user_by_id = User.query.get(1)
``
.first()
*: クエリの結果の最初のレコードだけを取得します。条件に合致するレコードがなくてもエラーにはならず
None`を返します。複数の結果が予想されるが最初の1件だけ欲しい場合に便利です。“`python
ユーザー名が ‘testuser’ のユーザーを取得 (最初に見つかった1件)
test_user = User.query.filter_by(username=’testuser’).first()
``
.one()
*: クエリの結果がちょうど1件であることを期待する場合に使います。結果が0件の場合は
NoResultFound例外、2件以上の場合は
MultipleResultsFound例外が発生します。絶対に1件だけ存在することが保証される場合に便利です(例えばユニーク制約のあるカラムでの検索後など)。
.one_or_none()
*: クエリの結果が0件または1件であることを期待する場合に使います。1件ならオブジェクトを、0件なら
Noneを返します。2件以上の場合は
MultipleResultsFound`例外が発生します。
-
-
ソート (
.order_by()
): 取得した結果を指定したカラムで並べ替えるにはorder_by()
を使います。デフォルトは昇順です。降順にしたい場合は、db.desc()
を使います。“`python
ユーザー名を昇順で取得
users_sorted_asc = User.query.order_by(User.username).all()
作成日時を降順で取得 (最新の投稿から)
recent_posts = Post.query.order_by(db.desc(Post.timestamp)).all()
複数のカラムでソート (ユーザー名を昇順、同じユーザー名ならIDを降順)
users_multi_sort = User.query.order_by(User.username, db.desc(User.id)).all()
“` -
件数制限とオフセット (
.limit()
,.offset()
): 取得するレコードの件数を制限したり、先頭から何件かをスキップしたりするにはlimit()
とoffset()
を使います。ページネーションを実装する際によく使われます。“`python
最初の10件を取得
first_10_users = User.query.limit(10).all()
最初の10件をスキップして、次の20件を取得 (2ページ目を表示するイメージ)
users_page_2 = User.query.offset(10).limit(20).all()
“`paginate()
という、ページネーション専用の便利なメソッドもよく使われます。 -
件数だけを取得 (
.count()
): 条件に合致するレコードの件数だけを知りたい場合はcount()
を使います。“`python
全ユーザー数を取得
total_users = User.query.count()
特定のメールアドレスを持つユーザー数を取得
count_example_email = User.query.filter(User.email.endswith(‘@example.com’)).count()
“` -
リレーションシップを伴う取得: モデル定義で
db.relationship
を設定している場合、関連するオブジェクトはモデルインスタンスの属性としてアクセスできます。“`python
id が 1 のユーザーを取得
user = User.query.get(1)
そのユーザーの全ての投稿を取得 (Userモデルの posts 属性を通じてアクセス)
lazy=’dynamic’ の場合、これはクエリオブジェクトを返す
user_posts_query = user.posts
実際の投稿リストを取得するには .all() などを呼び出す
user_posts_list = user.posts.all()
特定の投稿を取得
post = Post.query.get(10)
その投稿の著者(Userオブジェクト)を取得 (Postモデルの author 属性を通じてアクセス)
post_author = post.author
author は単一の User オブジェクトを返す
“`
リレーションシップを通じたアクセスは、デフォルトではLazy Loading(遅延読み込み)が行われます。これは、関連オブジェクトにアクセスするまでデータベースからの取得が行われない方式です。アクセス時に初めて別のSELECTクエリが実行されます。これは通常効率的ですが、リスト表示などで多数の関連オブジェクトをまとめて表示する場合(例: 全ユーザーとその最新の投稿を表示)、各ユーザーに対して別途クエリが実行されるため、N+1問題と呼ばれるパフォーマンス上の問題を引き起こす可能性があります(N人のユーザーに対して、ユーザー取得1回 + 各ユーザーの投稿取得N回の合計N+1回のクエリが発生)。
N+1問題を回避するためには、Eager Loading(先行読み込み)を使います。
joinedload()
やsubqueryload()
などのメソッドを使って、親オブジェクトを取得するクエリの中で関連オブジェクトも一緒に取得するように指定します。“`python
from sqlalchemy.orm import joinedloadユーザーとその関連する投稿を、ユーザー取得時にJOINして一度のクエリで取得する
users_with_posts = User.query.options(joinedload(User.posts)).all()
これで users_with_posts リスト内の各 user オブジェクトの user.posts にアクセスしても、
個別の投稿取得クエリは実行されません。
“`
Eager Loadingは、リレーションシップ先のデータが常に必要である場合にパフォーマンスを向上させますが、不要なデータまで取得してしまう可能性もあります。どちらの方法を使うかは、アプリケーションの要件やデータのアクセスパターンによって検討する必要があります。
U: Update (データの更新)
既存のデータベースレコードのデータを更新するには、まずそのレコードに対応するモデルオブジェクトを取得し、そのオブジェクトの属性値を変更し、最後にセッションをコミットします。
app.py
に、ユーザーのメールアドレスを更新する簡単なルートを追加してみましょう。
“`python
app.py (既存のコードに追記)
from flask import Flask, request, redirect, url_for, render_template_string
… (他のインポートと初期化コード) …
— データベースモデルの定義(既存) —
class User(db.Model): …
class Post(db.Model): …
— ルーティングとビュー関数 —
ユーザー編集用のフォームと処理
@app.route(‘/edit_user/
def edit_user(user_id):
# 1. 対象レコードの取得
# 主キーで取得するのが最も効率的
user = User.query.get(user_id)
# ユーザーが見つからない場合は404エラー
if user is None:
return "ユーザーが見つかりません", 404
if request.method == 'POST':
new_email = request.form.get('email')
if new_email:
# 2. 属性値の変更
# 取得したオブジェクトの属性に新しい値を代入するだけ
user.email = new_email
# セッションに追加する必要はない(既にセッションが追跡しているため)
# db.session.add(user) # これは不要
# 3. コミット
# セッションが user オブジェクトの変更を検知しており、コミットでデータベースに反映される
db.session.commit()
# 成功したらユーザー一覧ページなどにリダイレクト
return redirect(url_for('list_users'))
else:
return "新しいメールアドレスを入力してください", 400 # Bad Request
# GETリクエストの場合は編集フォームを表示
html_form = """
<h1>ユーザー情報編集</h1>
<form method="post">
<p>ユーザー名: {{ user.username }}</p>
<label for="email">メールアドレス:</label><br>
<input type="email" id="email" name="email" value="{{ user.email }}"><br><br>
<input type="submit" value="更新">
</form>
<p><a href="{{ url_for('list_users') }}">ユーザー一覧に戻る</a></p>
"""
# 既存のメールアドレスをフォームのvalueに設定して表示
return render_template_string(html_form, user=user, url_for=url_for)
… (list_users, add_user ルートなど、他のルート定義) …
アプリケーションの実行(開発用サーバー)
if Gregor __name == ‘main‘:
with app.app_context():
# db.create_all() # マイグレーションを使う場合はコメントアウトまたは削除
pass
app.run(debug=True)
“`
コード解説:
- 対象レコードの取得: まず、更新したいレコードをクエリを使って取得します。ここでは主キー(
user_id
)を使ってUser.query.get(user_id)
で取得しています。 - 属性値の変更: 取得したモデルオブジェクト(
user
インスタンス)の属性(例:user.email
)に、新しい値を代入します。セッションは、セッション内で取得または追加されたオブジェクトの変更を自動的に追跡しているため、db.session.add()
などで明示的にセッションに追加し直す必要はありません。 - コミット:
db.session.commit()
を実行します。これにより、セッションが追跡していたuser
オブジェクトのemail
属性の変更が検知され、データベースの対応するレコードが更新されます。
更新も非常にシンプルに、Pythonオブジェクトの属性を変更するだけで行えることが分かります。
D: Delete (データの削除)
データベースからレコードを削除するには、まず対象のレコードに対応するモデルオブジェクトを取得し、それをセッションから削除対象としてマークし、最後にセッションをコミットします。
app.py
に、ユーザーを削除する簡単なルートを追加してみましょう。
“`python
app.py (既存のコードに追記)
from flask import Flask, request, redirect, url_for, render_template_string
… (他のインポートと初期化コード) …
— データベースモデルの定義(既存) —
class User(db.Model): …
class Post(db.Model): …
— ルーティングとビュー関数 —
ユーザー削除処理
@app.route(‘/delete_user/
def delete_user(user_id):
# 1. 対象レコードの取得
user = User.query.get(user_id)
# ユーザーが見つからない場合は404エラー
if user is None:
return "ユーザーが見つかりません", 404
# 2. セッションからの削除対象としてマーク
# この時点ではまだデータベースからは削除されていない
db.session.delete(user)
# 3. コミット
# ここで初めてセッション内の変更(user の削除)がデータベースに書き込まれる
db.session.commit()
# 成功したらユーザー一覧ページなどにリダイレクト
return redirect(url_for('list_users'))
ユーザー一覧ページから削除リンクをPOSTメソッドで送るように修正する
(簡易HTMLテンプレートなので、GETリンクとしてますが、実際はJSなどでPOSTリクエストにするのが安全)
例:
except Exception as e:
# 途中でエラーが発生したらロールバック
db.session.rollback()
print(f”エラーが発生しました: {e}”)
print(“変更は取り消されました”)
finally:
# セッションをクリーンアップ(通常Flaskのコンテキストマネージャーがやってくれるが、手動で必要になる場合も)
# db.session.close()
pass # Flaskのコンテキスト内では通常不要
“`
Flaskのリクエスト処理内では、明示的にrollback()
を呼び出すケースは少ないかもしれませんが、commit()
は常に明示的に呼び出す必要があります。
リレーションシップ (関連)
現実世界のデータはしばしば相互に関連しています。例えば、ブログシステムでは「ユーザー」は複数の「投稿」を作成し、一つの「投稿」は一人の「ユーザー」によって作成されます。これは一対多のリレーションシップです。また、記事には複数の「タグ」が付くことがあり、一つの「タグ」は複数の「記事」に付けられることがあります。これは多対多のリレーションシップです。
SQLAlchemyでは、これらのリレーションシップをモデルクラス内で定義することで、関連オブジェクトをPythonコードから簡単に操作できるようになります。
リレーションシップの種類
- 一対一 (One-to-One): あるテーブルの1つのレコードが、他のテーブルのちょうど1つのレコードと関連付けられる関係。(例: ユーザーとそのプロフィール情報)
- 一対多 (One-to-Many): あるテーブルの1つのレコードが、他のテーブルの0個以上のレコードと関連付けられる関係。これは最も一般的なリレーションシップです。(例: ユーザーと投稿)
- 多対多 (Many-to-Many): あるテーブルの0個以上のレコードが、他のテーブルの0個以上のレコードと関連付けられる関係。多対多のリレーションシップは、多くの場合、間に「関連テーブル」(または結合テーブル、中間テーブル)と呼ばれる追加のテーブルを使って表現されます。(例: 記事とタグ)
SQLAlchemyでのリレーションシップの定義 (db.relationship()
)
リレーションシップは、モデルクラスの中にdb.relationship()
を使って定義します。これは、そのモデルのインスタンスが関連する他のモデルのインスタンスにどのようにアクセスできるかを定義します。
User
とPost
モデルに定義したリレーションシップを再確認しましょう。
“`python
class User(db.Model):
# … カラム定義 …
# User から Post への一対多リレーションシップ
# 'Post' は関連先のモデルクラス名 (文字列で指定するのが安全)
# backref='author': Post モデル側に 'author' という属性を追加し、その投稿の著者(Userオブジェクト)にアクセスできるようにする
# lazy='dynamic': related オブジェクト (投稿リスト) にアクセスしたときに、リストではなくクエリオブジェクトを返す
posts = db.relationship('Post', backref='author', lazy='dynamic')
class Post(db.Model):
# … カラム定義 …
# Post モデル側から User への逆方向リレーションシップ(外部キー定義とセットで使われる)
# user_id カラムで User テーブルを参照していることを定義
user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'))
# backref='author' は User モデル側で定義されているので、Post モデル側には author 属性が自動的に追加される。
# 明示的に db.relationship('User', backref='posts') と定義することも可能だが、
# 一対多の場合は片方 (多の方、ここでは Post) に外部キーを置き、
# もう片方 (一の方、ここでは User) に relationship と backref を定義するのが一般的。
# author = db.relationship('User', backref='posts') # User モデル側で backref を定義済みの場合は不要
“`
db.relationship('Post', ...)
: User
モデルにposts
という属性を追加します。この属性を通じて、そのユーザーが書いた複数のPost
オブジェクトにアクセスできるようになります。第一引数は関連先のモデル名です。
backref='author'
: これは便利なオプションです。User
モデル側でposts
リレーションシップを定義する際にbackref='author'
と指定すると、関連先のPost
モデルに自動的にauthor
という属性が追加されます。これにより、個々のPost
オブジェクトから、その投稿を作成したUser
オブジェクトにpost.author
のようにアクセスできるようになります。
lazy='dynamic'
: User.posts
にアクセスしたときに、関連する全てのPostオブジェクトをすぐにメモリにロードするのではなく、クエリオブジェクトを返します。これにより、user.posts.filter(...).order_by(...).limit(...)
のように、関連オブジェクトに対してさらに絞り込みや並べ替えといったクエリ操作を行うことが可能になります。これは、関連オブジェクトの件数が非常に多い場合に特に有効です。デフォルトのlazy='select'
(またはTrue
) は、アクセス時にリストとして全件をロードします。
Post
モデルでは、user_id
カラムがuser
テーブルのid
カラムを参照する外部キーとして定義されています。これにより、データベースレベルで「この投稿はどのユーザーのものか」という関連性が保証されます。
リレーションシップを通じたデータの操作
リレーションシップを定義しておくと、関連オブジェクトをPythonオブジェクトの属性のように扱って、データの取得や操作が非常に直感的になります。
関連データの取得:
“`python
あるユーザーを取得
user = User.query.filter_by(username=’testuser’).first()
そのユーザーが書いた全ての投稿を取得
lazy=’dynamic’ なので、これはクエリオブジェクト
user_posts_query = user.posts
実際の投稿リストを取得
user_posts_list = user_posts_query.all() # または user.posts.all() とすぐに呼んでも良い
特定の投稿の著者を取得
post = Post.query.get(10) # 例
post_author = post.author # Post モデルに自動的に追加された backref 属性
print(f”投稿 ‘{post.body[:20]}…’ の著者は {post_author.username} です”)
“`
関連データの追加:
新しい投稿を作成し、特定のユーザーに関連付けたい場合、Postオブジェクトを作成し、そのauthor
属性またはuser_id
属性に著者となるUserオブジェクトまたはそのIDを設定できます。
“`python
id が 1 のユーザーを取得 (著者を特定)
author_user = User.query.get(1)
新しい投稿を作成
new_post = Post(body=”これは新しい投稿です。”)
投稿を著者に関連付ける
外部キーを設定する方法
new_post.user_id = author_user.id # もちろんこれでもOK
リレーションシップ属性を設定する方法 (より Pythonic)
new_post.author = author_user # backref=’author’ により可能
セッションに追加してコミット
db.session.add(new_post)
db.session.commit()
print(f”投稿がユーザー {author_user.username} によって作成されました。”)
“`
関連データの削除:
例えば、あるユーザーを削除した場合に、そのユーザーが書いた全ての投稿も一緒に削除したいという要件があるとします。db.ForeignKey
定義でondelete='CASCADE'
オプションを指定していれば、データベース側が自動的に関連レコードを削除してくれます。
user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'))
この設定があれば、ユーザーオブジェクトを削除してコミットするだけで、そのユーザーに関連付けられた投稿も自動的に削除されます。
“`python
id が 1 のユーザーを取得
user_to_delete = User.query.get(1)
if user_to_delete:
# ユーザーを削除対象としてマーク
db.session.delete(user_to_delete)
# コミット。ondelete='CASCADE' により、関連する投稿も一緒に削除される
db.session.commit()
print(f"ユーザー {user_to_delete.username} とその投稿が全て削除されました。")
else:
print(“削除対象のユーザーが見つかりません。”)
“`
ondelete='CASCADE'
を指定しない場合、ユーザーを削除しようとすると、そのユーザーを参照している投稿が残っているとデータベースによってはエラーになることがあります(参照整合性制約)。その場合は、先にそのユーザーの投稿を全て削除してからユーザーを削除するか、削除時に関連投稿のuser_id
をNULLにするなどの処理をアプリケーション側で行う必要があります。
多対多のリレーションシップ:
多対多のリレーションシップを定義するには、通常、間に結合テーブル(Association Table)を用意します。例えば、Post
とTag
というモデルがあり、一つの投稿に複数のタグが付けられ、一つのタグが複数の投稿に付けられる場合です。
“`python
多対多リレーションシップのための結合テーブルを定義
このテーブルはモデルクラスではなく、直接 Table オブジェクトとして定義します
tablename = ‘post_tags’ のようにはしない
post_tags = db.Table(‘post_tags’,
# 外部キーカラムを定義
# db.Column(‘post_id’, db.Integer, db.ForeignKey(‘post.id’)), # こちらでもOK
db.Column(‘post_id’, db.Integer, db.ForeignKey(‘post.id’, ondelete=’CASCADE’)), # 投稿削除で関連も削除
# db.Column(‘tag_id’, db.Integer, db.ForeignKey(‘tag.id’)) # こちらでもOK
db.Column(‘tag_id’, db.Integer, db.ForeignKey(‘tag.id’, ondelete=’CASCADE’)) # タグ削除で関連も削除
# 必要に応じて、他のカラム(例: associated_at = db.Column(db.DateTime))を追加することも可能
)
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True, index=True)
# Tag から Post への多対多リレーションシップ
# secondary 引数で結合テーブルを指定
# backref='tags': Post モデル側に 'tags' という属性を追加し、その投稿に付けられた Tag オブジェクトのリストにアクセスできるようにする
posts = db.relationship('Post', secondary=post_tags, backref='tags', lazy='dynamic')
def __repr__(self):
return '<Tag {}>'.format(self.name)
Post モデル側には、Tag モデル定義の backref=’tags’ によって自動的に ‘tags’ 属性が追加されます
class Post(db.Model):
# … 既存のカラム定義 …
# tags = db.relationship(‘Tag’, secondary=post_tags, backref=’posts’) # Tag モデル側で backref=’tags’ を定義済みなので不要
“`
多対多関連の操作例:
“`python
新しい投稿とタグを作成
new_post = Post(body=”多対多リレーションシップのテスト投稿”)
tag1 = Tag(name=’Python’)
tag2 = Tag(name=’Database’)
tag3 = Tag(name=’Flask’)
タグがまだデータベースに存在しない場合はセッションに追加
存在する場合は query.filter_by などで取得して使う
existing_tag = Tag.query.filter_by(name=’Flask’).first()
if existing_tag:
tag3 = existing_tag
else:
db.session.add(tag3) # 新しいタグはセッションに追加
投稿にタグを関連付ける (Pythonリストのように append する)
new_post.tags.append(tag1)
new_post.tags.append(tag2)
new_post.tags.append(tag3) # 既存または新規のタグオブジェクト
セッションに追加してコミット (Postオブジェクトと、新規作成したTagオブジェクト、そして結合テーブルのエントリが追加される)
db.session.add(new_post)
db.session.add(tag1) # Tag1, Tag2 が新規の場合
db.session.add(tag2) # Tag1, Tag2 が新規の場合
db.session.commit()
print(f”投稿にタグが関連付けられました: {[tag.name for tag in new_post.tags]}”)
あるタグが付けられた投稿を取得
python_tag = Tag.query.filter_by(name=’Python’).first()
if python_tag:
# lazy=’dynamic’ なのでクエリオブジェクト
python_posts_query = python_tag.posts
# 実際の投稿リストを取得
python_posts_list = python_posts_query.all()
print(f”タグ ‘Python’ が付けられた投稿数: {len(python_posts_list)}”)
投稿から特定のタグを解除
post_to_edit = Post.query.filter_by(body=”多対多リレーションシップのテスト投稿”).first()
tag_to_remove = Tag.query.filter_by(name=’Database’).first()
if post_to_edit and tag_to_remove:
# Pythonリストから削除するのと同じ感覚
post_to_edit.tags.remove(tag_to_remove)
db.session.commit()
print(f”投稿からタグ ‘{tag_to_remove.name}’ が解除されました。現在のタグ: {[tag.name for tag in post_to_edit.tags]}”)
“`
多対多のリレーションシップも、db.relationship
とsecondary
引数を使うことで、関連テーブルの存在を意識することなく、Pythonオブジェクトのリストのように直感的に操作できることが分かります。
より高度なクエリ
基本的なCRUD操作だけでなく、SQLAlchemyのクエリビルダーはより複雑なクエリもサポートしています。
Join操作
複数のテーブルを結合してデータを取得するには、join()
メソッドを使います。リレーションシップが定義されていれば、JOIN条件を明示的に書かなくても自動的に適切なJOINを組み立ててくれます。
“`python
投稿と著者のユーザー名を一緒に取得
Post.query.join(User) は、Post モデルと User モデルをリレーションシップに基づいて JOIN する
filter で特定の条件を指定
posts_with_author = Post.query.join(User).filter(User.username == ‘testuser’).all()
特定のユーザーの投稿リストを取得する User.query.get(user_id).posts.all() と同じ結果が得られることが多いですが、
JOINを使うことで、より複雑な条件や、関連テーブルのカラムを使った並べ替えなどが可能になります。
例えば、ユーザー名で投稿を並べ替える
posts_ordered_by_author = Post.query.join(User).order_by(User.username).all()
“`
リレーションシップがないテーブル同士をJOINする場合は、JOIN条件を明示的に指定する必要があります。
集計関数 (func
)
SQLの集計関数(COUNT
, SUM
, AVG
, MAX
, MIN
など)を使うには、SQLAlchemyのdb.func
を使います。
“`python
from sqlalchemy import func
全ユーザー数を取得 (query.count() と同じ)
user_count = db.session.query(func.count(User.id)).scalar() # scalar() は結果が単一の値の場合に便利
各ユーザーの投稿数を取得
JOIN と group_by を使う
post_counts_by_user = db.session.query(User.username, func.count(Post.id)).join(Post).group_by(User.username).all()
post_counts_by_user は [(username1, count1), (username2, count2), …] のようなタプルのリストになる
for username, count in post_counts_by_user:
print(f”ユーザー ‘{username}’ は {count} 件の投稿をしています。”)
投稿の総数を取得
total_posts = db.session.query(func.count(Post.id)).scalar()
“`
db.session.query()
は、db.Model.query
よりも低レベルで柔軟なクエリを構築できます。特定のカラムや集計結果だけを取得したい場合などに便利です。
グループ化 (group_by()
)
データを特定のカラムの値ごとにグループ化し、各グループに対して集計関数を適用するにはgroup_by()
を使います。
“`python
上記の「各ユーザーの投稿数を取得」の例で group_by(User.username) を使っています。
“`
生SQLの実行 (推奨しないが知っておく)
ORMで表現するのが難しい複雑なクエリを実行したい場合や、ORMのオーバーヘッドを避けたい場合などに、やむを得ず生SQLを実行することも可能です。しかし、前述のSQLを直接書くことによるデメリット(可読性、保守性、DB非依存性の低下、SQLインジェクションのリスク増大)があるため、極力ORMを使うべきです。
“`python
生SQLクエリの実行例
db.session.execute() を使う
result = db.session.execute(“SELECT username, email FROM user WHERE id = :user_id”, {‘user_id’: 1})
結果は ResultProxy オブジェクトとして返される
for row in result:
print(f”ユーザー名: {row[‘username’]}, メールアドレス: {row[‘email’]}”)
パラメータを渡す際は、上記のようにプレースホルダー(:parameter_name)を使うことで SQL インジェクションのリスクを低減できます。
“`
生SQLを実行した場合、取得されるのはモデルオブジェクトではなく、Rowオブジェクトなどの低レベルなオブジェクトになります。
エラーハンドリングとデバッグ
データベース操作中に発生しうるエラーには様々なものがあります。
- IntegrityError: データベースの整合性制約(UNIQUE制約違反、外部キー制約違反など)に違反した場合に発生します。例えば、既に存在するユーザー名で新しいユーザーを追加しようとした場合など。
- DatabaseError: データベース接続に関する問題や、不正なSQL文など、一般的なデータベースエラー。
- OperationalError: データベースサーバーへの接続失敗、権限問題など。
- ProgrammingError: カラム名やテーブル名の typos、引数の不一致など、プログラミング上の誤りに起因するデータベースエラー。
- NoResultFound / MultipleResultsFound:
one()
やone_or_none()
を使ったクエリで、期待する件数の結果が得られなかった場合。
これらのエラーが発生した場合、Flaskアプリケーションはデフォルトではエラーページを表示するか、デバッグモードの場合は詳細なトレースバックを表示します。本番環境では、ユーザーに分かりやすいエラーメッセージを表示したり、適切なログ記録を行ったりする必要があります。
データベース操作を含むビュー関数は、try...except
ブロックで囲み、特定のエラー(特にIntegrityErrorなど)を捕捉して、ユーザーにエラー内容を伝えたり、エラーに応じた処理(例: 「そのユーザー名は既に使用されています」というメッセージを表示してフォームを再表示する)を行ったりするのが良いプラクティスです。
“`python
from sqlalchemy.exc import IntegrityError
@app.route(‘/add_user’, methods=[‘GET’, ‘POST’])
def add_user():
if request.method == ‘POST’:
username = request.form.get(‘username’)
email = request.form.get(‘email’)
if username and email:
new_user = User(username=username, email=email)
try:
db.session.add(new_user)
db.session.commit()
return redirect(url_for('list_users'))
except IntegrityError:
# UNIQUE 制約違反などの場合
db.session.rollback() # エラー時は必ずロールバック
# フラッシュメッセージなどでユーザーにエラーを伝える
# from flask import flash; flash('そのユーザー名またはメールアドレスは既に使用されています。')
return "ユーザー名またはメールアドレスが重複しています。", 400
except Exception as e:
# その他のデータベースエラー
db.session.rollback()
print(f"データベースエラー: {e}") # ログに出力
return "ユーザー登録中にエラーが発生しました。", 500
else:
return "ユーザー名とメールアドレスを入力してください", 400
# ... (GETリクエストの処理) ...
“`
デバッグ方法:
- デバッグモード: Flaskアプリケーションを
app.run(debug=True)
で実行すると、詳細なエラー情報や対話式のデバッガーがブラウザに表示されます。開発中は常に有効にしておくのが便利です。
- ログ出力:
print()
文や、Pythonの標準ライブラリlogging
を使って、処理の途中経過や変数の値、エラー内容などをログに出力します。特にデータベース操作の前後や、エラー発生時にログを出力すると問題の特定に役立ちます。
- SQLAlchemyのログ: SQLAlchemyは、実行されたSQL文やパラメータをログに出力する機能を持っています。これにより、ORMが実際にどのようなSQLを生成して実行しているのかを確認できます。設定ファイル (
config.py
) で SQLALCHEMY_ECHO = True
と設定すると、SQL文が標準出力に表示されるようになります(本番環境では無効にすること)。
- データベースクライアントツール: SQLiteの場合、DB Browser for SQLiteなどのツールを使って
app.db
ファイルを開き、テーブル構造を確認したり、手動でクエリを実行したり、データの状態を確認したりできます。他のデータベースシステム(PostgreSQL, MySQLなど)にも同様のGUIツールやCUIクライアントツールがあります。
パフォーマンスに関する考慮事項
小規模なアプリケーションやデータ量が少ないうちはあまり気にならないかもしれませんが、アプリケーションが成長し、データ量が増えたりアクセス数が増えたりすると、データベース操作のパフォーマンスが重要になってきます。
N+1問題とその対策 (Eager Loading)
これは前述の「データの読み込み」セクションでも触れましたが、一対多などのリレーションシップを扱う際によく発生するパフォーマンス問題です。
例えば、100人のユーザーと、それぞれのユーザーが書いた最新の投稿を表示したいとします。素朴に以下のようなコードを書くと、
python
users = User.query.all() # 全ユーザーを1回のクエリで取得 (クエリ1)
for user in users:
latest_post = user.posts.order_by(db.desc(Post.timestamp)).first() # 各ユーザーに対して投稿を検索 (クエリN)
# print(f"{user.username}: {latest_post.body}")
まず全ユーザーを取得するために1回のクエリが実行されます。そして、ループの中で各ユーザーの投稿を取得するために、ユーザーの人数分(N回)のクエリが追加で実行されます。合計でN+1回のクエリが発生します。これがN+1問題です。ユーザー数が多いと、データベースへのクエリ発行回数が爆発的に増加し、パフォーマンスが著しく低下します。
これを解決するには、Eager Loading(先行読み込み)を使います。joinedload()
やsubqueryload()
などのメソッドを使って、ユーザーを取得する際に、関連する投稿もまとめて取得するようにSQLAlchemyに指示します。
“`python
from sqlalchemy.orm import joinedload
ユーザーと関連する最新の投稿を、ユーザー取得時にまとめて取得する
ここでは単純化のため、各ユーザーの「全ての」投稿をJOINしていますが、
実際には最新1件だけを取得するなどの最適化は JOIN だけでは難しく、より高度なクエリや別の手法が必要になります。
例として、ユーザーと投稿をJOINして、投稿日時でソートし、ユーザー名でグループ化して、各グループの最初の投稿を取得するようなクエリを組み立てる… と複雑になります。
Simple case: Load users and ALL their posts eagerly
users_with_posts = User.query.options(joinedload(User.posts)).all() # ユーザーと投稿をまとめて取得 (クエリ1回または数回)
この場合、ループ内で user.posts にアクセスしても追加のクエリは発生しない
for user in users_with_posts:
# ここでの user.posts は既にロード済みの投稿リスト
# リストから最新の投稿を探すのは Python 側で行う
latest_post = max(user.posts, key=lambda p: p.timestamp) if user.posts else None
if latest_post:
print(f”{user.username}: {latest_post.body}”)
“`
joinedload()
を使うと、通常はSQLのJOIN句を使って親テーブルと子テーブルを結合し、一度のクエリでデータを取得しようとします。関連データが多い場合に効果的です。
subqueryload()
は、親オブジェクトを取得するクエリとは別に、関連オブジェクトを取得するためのサブクエリを実行します。関連オブジェクトの件数が少ない場合に効果的なことがあります。
N+1問題は、アプリケーションのパフォーマンスにとって最も一般的なボトルネックの一つです。リレーションシップを通じてデータを取得する際には、常にEager Loadingを検討し、不必要なクエリ発行を防ぐように心がけましょう。
インデックスの重要性
データベースのカラムにインデックスを作成すると、そのカラムを検索条件(WHERE句)や並べ替え(ORDER BY句)、JOINのキーとして使用するクエリのパフォーマンスが大幅に向上します。これは、データベースがデータを効率的に見つけるための「索引」のようなものです。
モデル定義でindex=True
と指定することで、SQLAlchemyはマイグレーション時にそのカラムにインデックスを作成します。
python
class User(db.Model):
# ...
username = db.Column(db.String(64), index=True, unique=True) # ユーザー名は検索条件に使われやすいのでインデックス
email = db.Column(db.String(120), index=True, unique=True) # メールアドレスも検索条件に使われやすいのでインデックス
# ...
どのようなカラムにインデックスを張るべきかは、アプリケーションがどのようなクエリを頻繁に実行するかに依存します。一般的には、
WHERE
句で頻繁に検索条件として使われるカラム
JOIN
のキーとして使われるカラム(外部キーには通常インデックスを張るべき)
ORDER BY
句で頻繁に並べ替えに使われるカラム
にインデックスを張ると効果的です。ただし、インデックスを張りすぎると、データの追加・更新・削除のパフォーマンスが低下したり、データベースのファイルサイズが増加したりします。必要十分なインデックスを設計することが重要です。
データベースのパフォーマンスチューニングは奥が深い分野ですが、N+1問題とインデックスは初心者でも意識すべき基本的なポイントです。
実践的な例:シンプルなブログアプリケーション
これまでに学んだことを組み合わせて、ユーザー登録と記事投稿ができるシンプルなブログアプリケーションの核となる部分を実装してみましょう。
app.py
ファイルを以下の内容で作成または置き換えます。今回は少し構造を整理し、HTMLテンプレートは直接文字列として書かず、Jinja2テンプレートを使うことにします(テンプレートファイル自体は省略しますが、呼び出し方を示します)。また、ユーザー認証機能は省略し、あくまでデータベース操作に焦点を当てます。
“`python
app.py
import os
from flask import Flask, request, redirect, url_for, render_template, g # gを追加
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import Config
from datetime import datetime # datetimeオブジェクトを使うためにインポート
ベースディレクトリを取得
basedir = os.path.abspath(os.path.dirname(file))
Flaskアプリケーションのインスタンスを作成
app = Flask(name)
config.py の設定をアプリケーションに読み込む
app.config.from_object(Config)
SQLAlchemyのインスタンスを作成し、Flaskアプリケーションと連携させる
db = SQLAlchemy(app)
Flask-Migrateのインスタンスを作成し、Flaskアプリケーションと連携させる
migrate = Migrate(app, db, directory=app.config[‘MIGRATIONS_DIR’])
— データベースモデルの定義 —
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128)) # 実際にはパスワードハッシュを保存
# このUserと関連付けられたPostオブジェクトのリスト
posts = db.relationship('Post', backref='author', lazy='dynamic')
def __repr__(self):
return '<User {}>'.format(self.username)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(128)) # 記事タイトルを追加
body = db.Column(db.Text)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) # 投稿日時。デフォルトを utcnow に変更
user_id = db.Column(db.Integer, db.ForeignKey(‘user.id’, ondelete=’CASCADE’))
def __repr__(self):
return '<Post {}>'.format(self.title)
— 開発用ヘルパー関数 —
アプリケーションコンテキストがプッシュされた際に実行される関数
例えば、開発時に常に利用するユーザーを取得して g に保存するなど
@app.before_request
def before_request():
# 今回は単純に最初のユーザーを取得して g.current_user に設定
# 実際にはログイン機能を通じてユーザーを識別します
g.current_user = User.query.first()
if g.current_user is None:
# ユーザーがまだ存在しない場合はダミーユーザーを作成
dummy_user = User(username=’dummy_user’, email=’[email protected]’)
db.session.add(dummy_user)
db.session.commit()
g.current_user = dummy_user # 作成したダミーユーザーを設定
— ルーティングとビュー関数 —
@app.route(‘/’)
@app.route(‘/index’)
def index():
# 最新の投稿を新しい順に取得し、著者情報も一緒にロードする (N+1問題対策)
posts = Post.query.order_by(db.desc(Post.timestamp)).options(db.joinedload(Post.author)).all()
# Jinja2テンプレート (index.html) をレンダリング
# テンプレート内では ‘posts’ 変数を通じて投稿リストにアクセス可能
return render_template(‘index.html’, posts=posts)
@app.route(‘/user/‘)
def user_posts(username):
# 特定のユーザーを取得
user = User.query.filter_by(username=username).first_or_404() # 見つからなければ404エラー
# そのユーザーの投稿を新しい順に取得
# lazy='dynamic' なので、user.posts はクエリオブジェクト
posts = user.posts.order_by(db.desc(Post.timestamp)).all()
# テンプレート (user_posts.html) をレンダリング
return render_template('user_posts.html', user=user, posts=posts)
@app.route(‘/create_post’, methods=[‘GET’, ‘POST’])
def create_post():
# 簡易的な投稿作成フォーム
if request.method == ‘POST’:
title = request.form.get(‘title’)
body = request.form.get(‘body’)
if title and body and g.current_user:
# 現在のユーザーに関連付けて投稿を作成
new_post = Post(title=title, body=body, author=g.current_user)
db.session.add(new_post)
db.session.commit()
# 投稿一覧またはユーザーページにリダイレクト
return redirect(url_for('index'))
else:
return "タイトルと本文、およびユーザーが必要です", 400
# GETリクエストの場合はフォームを表示
# テンプレート (create_post.html) をレンダリング
return render_template('create_post.html')
@app.route(‘/edit_post/‘, methods=[‘GET’, ‘POST’])
def edit_post(post_id):
# 編集対象の投稿を取得
post = Post.query.get_or_404(post_id) # 見つからなければ404エラー
# (ここではユーザー認証は省略していますが、実際には投稿の著者であることを確認すべきです)
# if post.author != g.current_user:
# abort(403) # Forbidden
if request.method == 'POST':
post.title = request.form.get('title')
post.body = request.form.get('body')
# 変更をコミット
db.session.commit()
# 編集後の投稿ページまたはユーザーページにリダイレクト
return redirect(url_for('user_posts', username=post.author.username))
# GETリクエストの場合は編集フォームを表示
# テンプレート (edit_post.html) をレンダリング
return render_template('edit_post.html', post=post)
@app.route(‘/delete_post/‘, methods=[‘POST’]) # 削除はPOSTで
def delete_post(post_id):
# 削除対象の投稿を取得
post = Post.query.get_or_404(post_id) # 見つからなければ404エラー
# (ここでもユーザー認証を省略していますが、実際には投稿の著者であることを確認すべきです)
# 投稿を削除
db.session.delete(post)
db.session.commit()
# 削除後、投稿一覧またはユーザーページにリダイレクト
# post.author にアクセスすると、オブジェクトはまだメモリ上にあるが、関連先のデータは削除済みの場合があるので注意
# 削除前に author の情報を取得しておくのが安全
author_username = post.author.username if post.author else 'index' # 著者がいなければインデックスへ
return redirect(url_for('user_posts', username=author_username))
— アプリケーションの実行 —
if name == ‘main‘:
# アプリケーションコンテキスト内で db.create_all() または migrate を実行する
with app.app_context():
# 初回実行時やモデル変更後に実行
# マイグレーション環境をセットアップ済みの場合は不要(または migrate upgrade を使う)
# db.create_all() # コメントアウトまたは削除し、flask db migrate/upgrade を使うことを推奨
# デバッグ用: ユーザーがいない場合にダミーユーザーを作成 (before_request で行うように修正)
# if User.query.first() is None:
# dummy_user = User(username='dummy_user', email='[email protected]')
# db.session.add(dummy_user)
# db.session.commit()
pass # 特にする処理がなければ pass
app.run(debug=True)
“`
テンプレートファイル (templates ディレクトリ内に作成)
templates/index.html
:
“`html
最新投稿一覧
{% for post in posts %}
{{ post.title }}
{{ post.body[:200] }}…
著者: {{ post.author.username }}
(投稿日時: {{ post.timestamp.strftime(‘%Y-%m-%d %H:%M’) }})
{% else %}
まだ投稿がありません。
{% endfor %}
“`
templates/user_posts.html
:
“`html
{{ user.username }} の投稿
{% for post in posts %}
{% else %}
{{ user.username }} はまだ投稿していません。
{% endfor %}
“`
templates/create_post.html
:
“`html
新しい投稿を作成
“`
templates/edit_post.html
:
“`html
投稿編集
“`
実行手順:
- 上記の
app.py
, config.py
ファイルを作成/更新します。
templates
ディレクトリを作成し、その中に上記の HTML ファイル (index.html
, user_posts.html
, create_post.html
, edit_post.html
) を作成します。
- 仮想環境をアクティベートしたターミナルで、プロジェクトのルートディレクトリに移動します。
- マイグレーション環境を初期化します(まだ行っていない場合)。
bash
flask db init
- モデル定義の変更を検知し、マイグレーションスクリプトを生成します。
bash
flask db migrate -m "create user and post tables"
- 生成されたマイグレーションスクリプトをデータベースに適用します。
bash
flask db upgrade
これによりapp.db
ファイルとテーブルが作成されるか、既存のテーブルが更新されます。
- Flaskアプリケーションを実行します。
bash
flask run
# または python app.py
- ブラウザで
http://127.0.0.1:5000/
にアクセスします。
before_request
でダミーユーザーが作成されます。
- 「新しい投稿を作成」リンクから投稿を作成できます。
- 投稿一覧 (
/
) やユーザーページ (/user/dummy_user
) で作成した投稿が表示されます。
- 投稿タイトルをクリックすると編集ページに移動できます。
- 投稿の「削除」ボタンから削除できます(フォームを使ったPOSTリクエストで実行)。
- ユーザー一覧 (
/users
) やユーザー追加 (/add_user
) ルートも引き続き利用可能です。
この実践例を通じて、モデル定義、マイグレーション、CRUD操作、リレーションシップ、そして簡単なテンプレート連携まで、一連のデータベース操作がどのようにアプリケーションに組み込まれるかを体験できます。
まとめと次のステップ
この記事では、Flask-SQLAlchemyを使ったデータベース操作の基本から応用までを、初心者向けに詳細に解説しました。
- Web開発におけるデータベースの重要性と、Flask-SQLAlchemyを使うメリットを理解しました。
- 開発環境をセットアップし、FlaskおよびFlask-SQLAlchemyをインストールしました。
db.Model
を継承したクラスとしてデータベースモデル(テーブル)を定義する方法を学びました。
flask db init
, flask db migrate
, flask db upgrade
コマンドを使って、データベースの作成とスキーマ変更に対応するためのマイグレーションを行う手順を習得しました。
db.session
を通じて、データの追加 (db.session.add()
, db.session.commit()
)、読み込み (.query
, filter_by()
, filter()
, all()
, first()
, get()
, order_by()
など)、更新(オブジェクト属性の変更とコミット)、削除 (db.session.delete()
, db.session.commit()
) というCRUD操作の基本をマスターしました。
db.relationship()
とdb.ForeignKey
を使ってテーブル間の関連(一対多、多対多)を定義し、関連オブジェクトを通じてデータにアクセスしたり操作したりする方法を理解しました。
- N+1問題やインデックスといったパフォーマンスに関する基本的な考慮事項について学びました。
- シンプルなブログアプリケーションの実践例を通じて、これまでの知識を統合し、実際のアプリケーション開発でどのようにデータベース操作を行うのかを体験しました。
これで、あなたのFlaskアプリケーションにデータベース機能を組み込み、動的なデータを持つWebサービスを開発するための強固な基礎が身についたはずです。
次のステップとして、以下のトピックに取り組むことをお勧めします。
- ユーザー認証の実装: ログイン、ログアウト、ユーザー登録、パスワードハッシュ化(Werkzeug Securityなどを利用)、ロールベースのアクセス制御などを実装し、データベースのユーザーモデルと連携させます。
- フォームバリデーション: Flask-WTFなどの拡張機能を使って、ユーザーからの入力値を検証し、不正なデータがデータベースに保存されるのを防ぎます。
- ページネーションの実装: 大量のデータを効率的に表示するために、
query.paginate()
などを活用してページネーション機能を実装します。
- 他のデータベースシステムの利用: SQLiteから、PostgreSQLやMySQLなどの本格的なデータベースシステムに切り替えてみます。データベースURIを変更するだけで、ほとんどのFlask-SQLAlchemyのコードはそのまま使えるはずです(データベースシステム固有の機能を使う場合を除く)。
- アプリケーション構造の分割: アプリケーションが大きくなってきたら、モデル、ビュー(ルート)、フォームなどを別々のファイルやディレクトリに分割し、パッケージ構造にすることで、コードの管理性を向上させます。
- テストの記述: データベース操作を含むアプリケーションのテストを記述する方法を学びます。
- 本番環境へのデプロイ: 作成したアプリケーションをHerokuやAWSなどの本番環境にデプロイする方法を学びます。本番環境では、データベースの設定やマイグレーションの実行方法が開発環境とは異なる場合があります。
- SQLAlchemyのより高度な機能: 継承、イベント、リスナー、セッションのより詳細な管理方法など、SQLAlchemyにはこの記事で紹介しきれなかった多くの高度な機能があります。必要に応じて公式ドキュメントなどを参照し、学習を深めてください。
この記事が、あなたのFlask + データベース開発の旅の良いスタート地点となれば幸いです。
お読みいただきありがとうございました。
もし何か不明な点があれば、遠慮なく質問してください。
# 途中でエラーが発生したらロールバック
db.session.rollback()
print(f”エラーが発生しました: {e}”)
print(“変更は取り消されました”)
# セッションをクリーンアップ(通常Flaskのコンテキストマネージャーがやってくれるが、手動で必要になる場合も)
# db.session.close()
pass # Flaskのコンテキスト内では通常不要
“`
rollback()
を呼び出すケースは少ないかもしれませんが、commit()
は常に明示的に呼び出す必要があります。db.relationship()
)db.relationship()
を使って定義します。これは、そのモデルのインスタンスが関連する他のモデルのインスタンスにどのようにアクセスできるかを定義します。User
とPost
モデルに定義したリレーションシップを再確認しましょう。class User(db.Model):
# … カラム定義 …
# User から Post への一対多リレーションシップ
# 'Post' は関連先のモデルクラス名 (文字列で指定するのが安全)
# backref='author': Post モデル側に 'author' という属性を追加し、その投稿の著者(Userオブジェクト)にアクセスできるようにする
# lazy='dynamic': related オブジェクト (投稿リスト) にアクセスしたときに、リストではなくクエリオブジェクトを返す
posts = db.relationship('Post', backref='author', lazy='dynamic')
# … カラム定義 …
# Post モデル側から User への逆方向リレーションシップ(外部キー定義とセットで使われる)
# user_id カラムで User テーブルを参照していることを定義
user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'))
# backref='author' は User モデル側で定義されているので、Post モデル側には author 属性が自動的に追加される。
# 明示的に db.relationship('User', backref='posts') と定義することも可能だが、
# 一対多の場合は片方 (多の方、ここでは Post) に外部キーを置き、
# もう片方 (一の方、ここでは User) に relationship と backref を定義するのが一般的。
# author = db.relationship('User', backref='posts') # User モデル側で backref を定義済みの場合は不要
db.relationship('Post', ...)
: User
モデルにposts
という属性を追加します。この属性を通じて、そのユーザーが書いた複数のPost
オブジェクトにアクセスできるようになります。第一引数は関連先のモデル名です。backref='author'
: これは便利なオプションです。User
モデル側でposts
リレーションシップを定義する際にbackref='author'
と指定すると、関連先のPost
モデルに自動的にauthor
という属性が追加されます。これにより、個々のPost
オブジェクトから、その投稿を作成したUser
オブジェクトにpost.author
のようにアクセスできるようになります。lazy='dynamic'
: User.posts
にアクセスしたときに、関連する全てのPostオブジェクトをすぐにメモリにロードするのではなく、クエリオブジェクトを返します。これにより、user.posts.filter(...).order_by(...).limit(...)
のように、関連オブジェクトに対してさらに絞り込みや並べ替えといったクエリ操作を行うことが可能になります。これは、関連オブジェクトの件数が非常に多い場合に特に有効です。デフォルトのlazy='select'
(またはTrue
) は、アクセス時にリストとして全件をロードします。Post
モデルでは、user_id
カラムがuser
テーブルのid
カラムを参照する外部キーとして定義されています。これにより、データベースレベルで「この投稿はどのユーザーのものか」という関連性が保証されます。post_author = post.author # Post モデルに自動的に追加された backref 属性
print(f”投稿 ‘{post.body[:20]}…’ の著者は {post_author.username} です”)
“`
author
属性またはuser_id
属性に著者となるUserオブジェクトまたはそのIDを設定できます。db.session.commit()
“`
db.ForeignKey
定義でondelete='CASCADE'
オプションを指定していれば、データベース側が自動的に関連レコードを削除してくれます。user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'))
# ユーザーを削除対象としてマーク
db.session.delete(user_to_delete)
# コミット。ondelete='CASCADE' により、関連する投稿も一緒に削除される
db.session.commit()
print(f"ユーザー {user_to_delete.username} とその投稿が全て削除されました。")
print(“削除対象のユーザーが見つかりません。”)
“`
ondelete='CASCADE'
を指定しない場合、ユーザーを削除しようとすると、そのユーザーを参照している投稿が残っているとデータベースによってはエラーになることがあります(参照整合性制約)。その場合は、先にそのユーザーの投稿を全て削除してからユーザーを削除するか、削除時に関連投稿のuser_id
をNULLにするなどの処理をアプリケーション側で行う必要があります。Post
とTag
というモデルがあり、一つの投稿に複数のタグが付けられ、一つのタグが複数の投稿に付けられる場合です。# 外部キーカラムを定義
# db.Column(‘post_id’, db.Integer, db.ForeignKey(‘post.id’)), # こちらでもOK
db.Column(‘post_id’, db.Integer, db.ForeignKey(‘post.id’, ondelete=’CASCADE’)), # 投稿削除で関連も削除
# db.Column(‘tag_id’, db.Integer, db.ForeignKey(‘tag.id’)) # こちらでもOK
db.Column(‘tag_id’, db.Integer, db.ForeignKey(‘tag.id’, ondelete=’CASCADE’)) # タグ削除で関連も削除
# 必要に応じて、他のカラム(例: associated_at = db.Column(db.DateTime))を追加することも可能
)
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True, index=True)
# Tag から Post への多対多リレーションシップ
# secondary 引数で結合テーブルを指定
# backref='tags': Post モデル側に 'tags' という属性を追加し、その投稿に付けられた Tag オブジェクトのリストにアクセスできるようにする
posts = db.relationship('Post', secondary=post_tags, backref='tags', lazy='dynamic')
def __repr__(self):
return '<Tag {}>'.format(self.name)
tag1 = Tag(name=’Python’)
tag2 = Tag(name=’Database’)
tag3 = Tag(name=’Flask’)
if existing_tag:
tag3 = existing_tag
else:
db.session.add(tag3) # 新しいタグはセッションに追加
new_post.tags.append(tag2)
new_post.tags.append(tag3) # 既存または新規のタグオブジェクト
db.session.add(tag1) # Tag1, Tag2 が新規の場合
db.session.add(tag2) # Tag1, Tag2 が新規の場合
db.session.commit()
if python_tag:
# lazy=’dynamic’ なのでクエリオブジェクト
python_posts_query = python_tag.posts
# 実際の投稿リストを取得
python_posts_list = python_posts_query.all()
print(f”タグ ‘Python’ が付けられた投稿数: {len(python_posts_list)}”)
tag_to_remove = Tag.query.filter_by(name=’Database’).first()
# Pythonリストから削除するのと同じ感覚
post_to_edit.tags.remove(tag_to_remove)
db.session.commit()
print(f”投稿からタグ ‘{tag_to_remove.name}’ が解除されました。現在のタグ: {[tag.name for tag in post_to_edit.tags]}”)
“`
db.relationship
とsecondary
引数を使うことで、関連テーブルの存在を意識することなく、Pythonオブジェクトのリストのように直感的に操作できることが分かります。join()
メソッドを使います。リレーションシップが定義されていれば、JOIN条件を明示的に書かなくても自動的に適切なJOINを組み立ててくれます。“`
func
)COUNT
, SUM
, AVG
, MAX
, MIN
など)を使うには、SQLAlchemyのdb.func
を使います。from sqlalchemy import func
print(f”ユーザー ‘{username}’ は {count} 件の投稿をしています。”)
“`
db.session.query()
は、db.Model.query
よりも低レベルで柔軟なクエリを構築できます。特定のカラムや集計結果だけを取得したい場合などに便利です。group_by()
)group_by()
を使います。print(f”ユーザー名: {row[‘username’]}, メールアドレス: {row[‘email’]}”)
one()
やone_or_none()
を使ったクエリで、期待する件数の結果が得られなかった場合。try...except
ブロックで囲み、特定のエラー(特にIntegrityErrorなど)を捕捉して、ユーザーにエラー内容を伝えたり、エラーに応じた処理(例: 「そのユーザー名は既に使用されています」というメッセージを表示してフォームを再表示する)を行ったりするのが良いプラクティスです。from sqlalchemy.exc import IntegrityError
def add_user():
if request.method == ‘POST’:
username = request.form.get(‘username’)
email = request.form.get(‘email’)
if username and email:
new_user = User(username=username, email=email)
try:
db.session.add(new_user)
db.session.commit()
return redirect(url_for('list_users'))
except IntegrityError:
# UNIQUE 制約違反などの場合
db.session.rollback() # エラー時は必ずロールバック
# フラッシュメッセージなどでユーザーにエラーを伝える
# from flask import flash; flash('そのユーザー名またはメールアドレスは既に使用されています。')
return "ユーザー名またはメールアドレスが重複しています。", 400
except Exception as e:
# その他のデータベースエラー
db.session.rollback()
print(f"データベースエラー: {e}") # ログに出力
return "ユーザー登録中にエラーが発生しました。", 500
else:
return "ユーザー名とメールアドレスを入力してください", 400
# ... (GETリクエストの処理) ...
app.run(debug=True)
で実行すると、詳細なエラー情報や対話式のデバッガーがブラウザに表示されます。開発中は常に有効にしておくのが便利です。print()
文や、Pythonの標準ライブラリlogging
を使って、処理の途中経過や変数の値、エラー内容などをログに出力します。特にデータベース操作の前後や、エラー発生時にログを出力すると問題の特定に役立ちます。config.py
) で SQLALCHEMY_ECHO = True
と設定すると、SQL文が標準出力に表示されるようになります(本番環境では無効にすること)。app.db
ファイルを開き、テーブル構造を確認したり、手動でクエリを実行したり、データの状態を確認したりできます。他のデータベースシステム(PostgreSQL, MySQLなど)にも同様のGUIツールやCUIクライアントツールがあります。python
users = User.query.all() # 全ユーザーを1回のクエリで取得 (クエリ1)
for user in users:
latest_post = user.posts.order_by(db.desc(Post.timestamp)).first() # 各ユーザーに対して投稿を検索 (クエリN)
# print(f"{user.username}: {latest_post.body}")
joinedload()
やsubqueryload()
などのメソッドを使って、ユーザーを取得する際に、関連する投稿もまとめて取得するようにSQLAlchemyに指示します。from sqlalchemy.orm import joinedload
# ここでの user.posts は既にロード済みの投稿リスト
# リストから最新の投稿を探すのは Python 側で行う
latest_post = max(user.posts, key=lambda p: p.timestamp) if user.posts else None
if latest_post:
print(f”{user.username}: {latest_post.body}”)
“`
joinedload()
を使うと、通常はSQLのJOIN句を使って親テーブルと子テーブルを結合し、一度のクエリでデータを取得しようとします。関連データが多い場合に効果的です。subqueryload()
は、親オブジェクトを取得するクエリとは別に、関連オブジェクトを取得するためのサブクエリを実行します。関連オブジェクトの件数が少ない場合に効果的なことがあります。index=True
と指定することで、SQLAlchemyはマイグレーション時にそのカラムにインデックスを作成します。python
class User(db.Model):
# ...
username = db.Column(db.String(64), index=True, unique=True) # ユーザー名は検索条件に使われやすいのでインデックス
email = db.Column(db.String(120), index=True, unique=True) # メールアドレスも検索条件に使われやすいのでインデックス
# ...
WHERE
句で頻繁に検索条件として使われるカラムJOIN
のキーとして使われるカラム(外部キーには通常インデックスを張るべき)ORDER BY
句で頻繁に並べ替えに使われるカラムapp.py
ファイルを以下の内容で作成または置き換えます。今回は少し構造を整理し、HTMLテンプレートは直接文字列として書かず、Jinja2テンプレートを使うことにします(テンプレートファイル自体は省略しますが、呼び出し方を示します)。また、ユーザー認証機能は省略し、あくまでデータベース操作に焦点を当てます。from flask import Flask, request, redirect, url_for, render_template, g # gを追加
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import Config
from datetime import datetime # datetimeオブジェクトを使うためにインポート
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128)) # 実際にはパスワードハッシュを保存
# このUserと関連付けられたPostオブジェクトのリスト
posts = db.relationship('Post', backref='author', lazy='dynamic')
def __repr__(self):
return '<User {}>'.format(self.username)
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(128)) # 記事タイトルを追加
body = db.Column(db.Text)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) # 投稿日時。デフォルトを utcnow に変更
user_id = db.Column(db.Integer, db.ForeignKey(‘user.id’, ondelete=’CASCADE’))
def __repr__(self):
return '<Post {}>'.format(self.title)
def before_request():
# 今回は単純に最初のユーザーを取得して g.current_user に設定
# 実際にはログイン機能を通じてユーザーを識別します
g.current_user = User.query.first()
if g.current_user is None:
# ユーザーがまだ存在しない場合はダミーユーザーを作成
dummy_user = User(username=’dummy_user’, email=’[email protected]’)
db.session.add(dummy_user)
db.session.commit()
g.current_user = dummy_user # 作成したダミーユーザーを設定
@app.route(‘/index’)
def index():
# 最新の投稿を新しい順に取得し、著者情報も一緒にロードする (N+1問題対策)
posts = Post.query.order_by(db.desc(Post.timestamp)).options(db.joinedload(Post.author)).all()
# Jinja2テンプレート (index.html) をレンダリング
# テンプレート内では ‘posts’ 変数を通じて投稿リストにアクセス可能
return render_template(‘index.html’, posts=posts)
def user_posts(username):
# 特定のユーザーを取得
user = User.query.filter_by(username=username).first_or_404() # 見つからなければ404エラー
# そのユーザーの投稿を新しい順に取得
# lazy='dynamic' なので、user.posts はクエリオブジェクト
posts = user.posts.order_by(db.desc(Post.timestamp)).all()
# テンプレート (user_posts.html) をレンダリング
return render_template('user_posts.html', user=user, posts=posts)
def create_post():
# 簡易的な投稿作成フォーム
if request.method == ‘POST’:
title = request.form.get(‘title’)
body = request.form.get(‘body’)
if title and body and g.current_user:
# 現在のユーザーに関連付けて投稿を作成
new_post = Post(title=title, body=body, author=g.current_user)
db.session.add(new_post)
db.session.commit()
# 投稿一覧またはユーザーページにリダイレクト
return redirect(url_for('index'))
else:
return "タイトルと本文、およびユーザーが必要です", 400
# GETリクエストの場合はフォームを表示
# テンプレート (create_post.html) をレンダリング
return render_template('create_post.html')
def edit_post(post_id):
# 編集対象の投稿を取得
post = Post.query.get_or_404(post_id) # 見つからなければ404エラー
# (ここではユーザー認証は省略していますが、実際には投稿の著者であることを確認すべきです)
# if post.author != g.current_user:
# abort(403) # Forbidden
if request.method == 'POST':
post.title = request.form.get('title')
post.body = request.form.get('body')
# 変更をコミット
db.session.commit()
# 編集後の投稿ページまたはユーザーページにリダイレクト
return redirect(url_for('user_posts', username=post.author.username))
# GETリクエストの場合は編集フォームを表示
# テンプレート (edit_post.html) をレンダリング
return render_template('edit_post.html', post=post)
def delete_post(post_id):
# 削除対象の投稿を取得
post = Post.query.get_or_404(post_id) # 見つからなければ404エラー
# (ここでもユーザー認証を省略していますが、実際には投稿の著者であることを確認すべきです)
# 投稿を削除
db.session.delete(post)
db.session.commit()
# 削除後、投稿一覧またはユーザーページにリダイレクト
# post.author にアクセスすると、オブジェクトはまだメモリ上にあるが、関連先のデータは削除済みの場合があるので注意
# 削除前に author の情報を取得しておくのが安全
author_username = post.author.username if post.author else 'index' # 著者がいなければインデックスへ
return redirect(url_for('user_posts', username=author_username))
# アプリケーションコンテキスト内で db.create_all() または migrate を実行する
with app.app_context():
# 初回実行時やモデル変更後に実行
# マイグレーション環境をセットアップ済みの場合は不要(または migrate upgrade を使う)
# db.create_all() # コメントアウトまたは削除し、flask db migrate/upgrade を使うことを推奨
# デバッグ用: ユーザーがいない場合にダミーユーザーを作成 (before_request で行うように修正)
# if User.query.first() is None:
# dummy_user = User(username='dummy_user', email='[email protected]')
# db.session.add(dummy_user)
# db.session.commit()
pass # 特にする処理がなければ pass
app.run(debug=True)
templates/index.html
:{{ post.title }}
{{ post.body[:200] }}…
著者: {{ post.author.username }}
(投稿日時: {{ post.timestamp.strftime(‘%Y-%m-%d %H:%M’) }})
templates/user_posts.html
:templates/create_post.html
:templates/edit_post.html
:app.py
, config.py
ファイルを作成/更新します。templates
ディレクトリを作成し、その中に上記の HTML ファイル (index.html
, user_posts.html
, create_post.html
, edit_post.html
) を作成します。bash
flask db init
bash
flask db migrate -m "create user and post tables"
bash
flask db upgrade
これにより
app.db
ファイルとテーブルが作成されるか、既存のテーブルが更新されます。bash
flask run
# または python app.py
http://127.0.0.1:5000/
にアクセスします。
before_request
でダミーユーザーが作成されます。- 「新しい投稿を作成」リンクから投稿を作成できます。
- 投稿一覧 (
/
) やユーザーページ (/user/dummy_user
) で作成した投稿が表示されます。 - 投稿タイトルをクリックすると編集ページに移動できます。
- 投稿の「削除」ボタンから削除できます(フォームを使ったPOSTリクエストで実行)。
- ユーザー一覧 (
/users
) やユーザー追加 (/add_user
) ルートも引き続き利用可能です。
db.Model
を継承したクラスとしてデータベースモデル(テーブル)を定義する方法を学びました。flask db init
, flask db migrate
, flask db upgrade
コマンドを使って、データベースの作成とスキーマ変更に対応するためのマイグレーションを行う手順を習得しました。db.session
を通じて、データの追加 (db.session.add()
, db.session.commit()
)、読み込み (.query
, filter_by()
, filter()
, all()
, first()
, get()
, order_by()
など)、更新(オブジェクト属性の変更とコミット)、削除 (db.session.delete()
, db.session.commit()
) というCRUD操作の基本をマスターしました。db.relationship()
とdb.ForeignKey
を使ってテーブル間の関連(一対多、多対多)を定義し、関連オブジェクトを通じてデータにアクセスしたり操作したりする方法を理解しました。query.paginate()
などを活用してページネーション機能を実装します。もし何か不明な点があれば、遠慮なく質問してください。