はい、承知いたしました。SQLAlchemy入門とFlaskとの連携について、詳細な説明を含む約5000語の記事を作成します。
SQLAlchemy入門:Flaskとの連携でDBを使いこなす
はじめに
Webアプリケーション開発において、データの永続化は避けて通れない課題です。ユーザー情報、投稿、商品リストなど、様々な種類のデータを安全かつ効率的に保存し、取得、更新、削除する必要があります。この役割を担うのがデータベースです。
Pythonでデータベースを扱うための強力なライブラリとして、SQLAlchemyがあります。SQLAlchemyは単なるデータベースアダプターではなく、リレーショナルデータベースの操作をオブジェクト指向で行えるようにするO/Rマッパー(Object-Relational Mapper)機能と、低レベルなデータベース操作を抽象化するCore機能を提供します。これにより、開発者はSQLクエリを直接書く手間を減らし、Pythonオブジェクトを操作する感覚でデータベースとやり取りできるようになります。
一方、Flaskは軽量ながら非常に柔軟性の高いPythonのWebフレームワークです。マイクロフレームワークと呼ばれ、必要最低限の機能のみを提供し、足りない機能は拡張機能(Extension)として追加する設計思想を持っています。データベース連携もその一つで、SQLAlchemyとFlaskを組み合わせることで、効率的でメンテナンス性の高いWebアプリケーションを開発できます。
この記事では、SQLAlchemyの基本的な使い方から始め、その中核であるO/Rマッパーの概念を深く掘り下げます。その後、Flaskアプリケーション内でSQLAlchemyをどのように活用するか、特にFlask-SQLAlchemyという便利な拡張機能を使った連携方法を詳細に解説します。最終的には、リレーションシップの定義や基本的なマイグレーションの考え方まで触れ、実践的なデータベースアプリケーション開発の基礎を習得することを目指します。
対象読者は、PythonとFlaskの基本的な知識はあるものの、データベース連携、特にSQLAlchemyを使った開発経験が少ない方です。この記事を通じて、SQLAlchemyとFlaskを組み合わせることで、いかに簡単に、そして強力にデータベースを扱えるようになるかを理解していただけるでしょう。
さあ、Pythonとデータベースの世界へ飛び込みましょう!
データベース入門:なぜ必要か、そして基本概念
SQLAlchemyや他のO/Rマッパーを学ぶ前に、データベースそのものに関する基本的な知識があると理解が深まります。Webアプリケーションで最もよく利用されるデータベースの種類は、リレーショナルデータベース(RDB)です。RDBは、データを「テーブル」という形式で整理し、テーブル同士を関連付ける(リレーションを張る)ことができるのが特徴です。
リレーショナルデータベースの基本構造
- テーブル (Table): データを格納する基本的な単位です。スプレッドシートのような表形式をイメージしてください。例えば、「ユーザー」テーブル、「投稿」テーブルなどがあります。
- カラム (Column): テーブルの縦の列です。各列には、格納されるデータの種類(データ型、例: 文字列、数値、日付など)が定義されています。例えば、「ユーザー」テーブルには「ユーザーID」、「ユーザー名」、「メールアドレス」といったカラムがあるでしょう。
- 行 (Row) または レコード (Record): テーブルの横の行です。一つの行が、一つのまとまったデータ単位を表します。例えば、「ユーザー」テーブルの一つの行は、特定のユーザーの情報(ID、名前、メールアドレスなど)を表します。
- 主キー (Primary Key): テーブル内の各行を一意に識別するためのカラム(またはカラムの組み合わせ)です。例えば、ユーザーIDは主キーとして使われることが多いです。主キーはNULLであってはならず、重複も許されません。これにより、特定の行を高速かつ確実に参照できます。
- 外部キー (Foreign Key): あるテーブルのカラムが、別のテーブルの主キーを参照する仕組みです。これにより、テーブル間にリレーションシップを定義できます。例えば、「投稿」テーブルに「ユーザーID」というカラムがあり、それが「ユーザー」テーブルの主キーである「ユーザーID」を参照している場合、これは「投稿は特定のユーザーによって作成された」という一対多のリレーションシップを表します。
SQL (Structured Query Language)
リレーショナルデータベースと対話するための標準的な言語がSQLです。データの操作(追加、取得、更新、削除)や、テーブルの定義、権限管理など、データベースに関するほとんどの操作はSQLで行われます。SQLの基本的な操作には以下のようなものがあります。
- SELECT: データベースからデータを取得します。
- INSERT: 新しいデータをテーブルに追加します。
- UPDATE: 既存のデータを更新します。
- DELETE: データを削除します。
- CREATE TABLE: 新しいテーブルを作成します。
- DROP TABLE: テーブルを削除します。
SQLAlchemyは、これらのSQL操作をPythonコードから抽象化し、よりPythonらしい構文でデータベースを扱えるようにします。
SQLAlchemyの基本:O/Rマッパーとは
O/Rマッパー (ORM) とは
O/Rマッパー(Object-Relational Mapper)は、オブジェクト指向プログラミング言語(例: Python)で書かれたクラスやオブジェクトと、リレーショナルデータベースのテーブルや行との間のマッピングを自動化するソフトウェアツールです。
- オブジェクト (Object) <-> テーブル (Table): Pythonのクラスをデータベースのテーブルに対応付けます。クラスのインスタンスがテーブルの行に対応します。
- オブジェクトの属性 (Attribute) <-> カラム (Column): クラスのインスタンス変数がテーブルのカラムに対応します。
ORMを使用することで、開発者はSQLクエリを直接書く代わりに、Pythonオブジェクトのメソッドや属性を操作するだけでデータベース操作を実行できるようになります。これにより、以下の利点が得られます。
- 生産性の向上: 定型的なSQLクエリを書く手間が省けます。
- コードの可読性と保守性の向上: Pythonコードとデータベース操作が統合され、理解しやすくなります。
- データベース非依存性の向上: ORMがデータベース固有のSQL方言の違いを吸収するため、異なる種類のデータベースへの移行が比較的容易になります(ただし、完全に非依存になるわけではありません)。
- セキュリティの向上: SQLインジェクションなどのセキュリティリスクを減らすのに役立ちます。
SQLAlchemyは、Pythonにおける最も人気があり、機能豊富なO/Rマッパーの一つです。
SQLAlchemyのアーキテクチャ:CoreとORM
SQLAlchemyは大きく分けて二つのコンポーネントから構成されています。
- SQLAlchemy Core: データベースとの対話の低レベルな部分を担当します。データベース接続の管理、SQLクエリの構築と実行、トランザクション管理などを行います。CoreはO/Rマッパーよりも抽象度が低く、SQL文をPythonオブジェクトで表現し、それを実行するイメージです。SQLをPythonで「組み立てる」ツールとして非常に強力です。
- SQLAlchemy ORM: Coreの上に構築されており、Pythonクラスとデータベーステーブル間のマッピングを提供します。オブジェクトを操作することでデータベース操作を実行できるようにします。開発者の多くはこちらのORM機能を中心に利用することになります。
どちらの機能も非常に強力ですが、Flaskアプリケーション開発においては、通常ORM機能を利用することが多いです。
SQLAlchemyのインストール
SQLAlchemyをインストールするには、pipを使います。
bash
pip install SQLAlchemy
使用するデータベースに応じて、別途データベースドライバーのインストールが必要です。例えば、SQLiteを使う場合は追加のドライバーは不要ですが、PostgreSQLを使う場合はpsycopg2
、MySQLを使う場合はmysql-connector-python
やPyMySQL
などが必要です。
例:PostgreSQL用ドライバーのインストール
bash
pip install psycopg2-binary
例:MySQL用ドライバーのインストール
bash
pip install PyMySQL
この記事では、最も簡単に利用できるSQLiteデータベースを例に進めます。SQLiteは単一のファイルとしてデータベースを保存するため、セットアップが不要で手軽に試せます。
エンジンの作成:データベース接続の確立
SQLAlchemyでデータベースを操作するには、まずデータベースへの接続を表す「エンジン」を作成する必要があります。エンジンは、データベースの種類、接続先、認証情報などを指定して作成します。
エンジンの作成は create_engine()
関数で行います。接続文字列の形式はデータベースの種類によって異なります。
一般的な接続文字列の形式:
データベースの種類+ドライバー://ユーザー名:パスワード@ホスト名:ポート番号/データベース名
SQLiteの場合:
* インメモリデータベース: sqlite:///:memory:
* ファイルに保存: sqlite:////パスワード/to/your/database.db
(絶対パスまたは相対パス)
例:SQLiteのファイルデータベース example.db
に接続するエンジンを作成
“`python
from sqlalchemy import create_engine
SQLiteのファイルデータベース ‘example.db’ に接続
相対パスの場合、カレントディレクトリにファイルが作成される
engine = create_engine(‘sqlite:///example.db’)
インメモリデータベースの場合 (テストなどに便利)
engine = create_engine(‘sqlite:///:memory:’)
print(f”Engine created for URL: {engine.url}”)
“`
create_engine()
は、データベースの種類に応じた適切なコネクションプールとダイアレクト(方言)を設定し、データベースとの通信を管理するオブジェクトを返します。通常、アプリケーション全体で一つのエンジンインスタンスを共有します。
メタデータとテーブルの定義 (Coreスタイル)
SQLAlchemy Coreでは、データベースのスキーマ(テーブル構造など)をPythonコードで定義するために MetaData
オブジェクトと Table
オブジェクトを使用します。MetaData
はデータベース全体の情報を保持し、Table
は個別のテーブルの構造を定義します。
例:users
テーブルと posts
テーブルを定義する (Coreスタイル)
“`python
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Text, DateTime, ForeignKey
from sqlalchemy.sql import func
エンジンの作成 (前の例と同じ)
engine = create_engine(‘sqlite:///example.db’)
メタデータオブジェクトを作成
metadata = MetaData()
‘users’ テーブルの定義
users_table = Table(
‘users’, metadata,
Column(‘id’, Integer, primary_key=True),
Column(‘username’, String(50), unique=True, nullable=False),
Column(‘email’, String(120), unique=True, nullable=False),
Column(‘created_at’, DateTime, server_default=func.now()) # サーバー側で現在日時を生成
)
‘posts’ テーブルの定義
posts_table = Table(
‘posts’, metadata,
Column(‘id’, Integer, primary_key=True),
Column(‘title’, String(100), nullable=False),
Column(‘body’, Text, nullable=False),
Column(‘created_at’, DateTime, server_default=func.now()),
# ‘user_id’ カラムは ‘users’ テーブルの ‘id’ カラムを参照する外部キー
Column(‘user_id’, Integer, ForeignKey(‘users.id’), nullable=False)
)
定義したテーブルをデータベースに作成(まだ存在しない場合)
metadata.create_all(engine)
print(“Tables ‘users’ and ‘posts’ defined and created (if they didn’t exist).”)
“`
このコードは以下のことを行います。
* MetaData()
でデータベースのメタ情報を格納するオブジェクトを作成します。
* Table()
で個別のテーブルを定義します。第一引数はテーブル名、第二引数は関連付けるMetaData
オブジェクト、その後の引数はカラム定義です。
* Column()
でカラムを定義します。第一引数はカラム名、第二引数はデータ型です。primary_key=True
で主キー、unique=True
でユニーク制約、nullable=False
でNOT NULL制約、ForeignKey()
で外部キー制約を指定できます。
* metadata.create_all(engine)
は、定義した全てのテーブルを、指定したエンジンが指すデータベース上に作成します。既に存在する場合は何も行われません。
Coreスタイルでのテーブル定義は、SQLのCREATE TABLE
文をPythonコードで表現するようなものです。これはこれで強力ですが、データの操作はSQL風の構文で行う必要があり、Pythonオブジェクトとの連携は手動で行う必要があります。そこでORMの出番です。
SQLAlchemy ORMの基本
SQLAlchemy ORMは、Pythonクラスをデータベーステーブルに対応付けることで、より直感的でオブジェクト指向なデータベース操作を可能にします。
宣言的スタイル (Declarative)
SQLAlchemy ORMで最もよく使われるスタイルは「宣言的スタイル(Declarative)」です。これは、Pythonクラスを定義する際に、同時にそのクラスが対応するテーブルの構造(カラムなど)も宣言的に定義する方式です。これにより、テーブル定義とPythonモデルクラスが一体化し、コードの見通しが良くなります。
宣言的スタイルを使用するには、declarative_base()
関数でベースクラスを作成し、そのクラスを継承してモデルクラスを定義します。
“`python
from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, ForeignKey
from sqlalchemy.orm import sessionmaker, declarative_base, relationship
from sqlalchemy.sql import func
エンジンの作成 (前の例と同じ)
engine = create_engine(‘sqlite:///example.db’)
宣言的ベースクラスを作成
Base = declarative_base()
モデルクラスの定義
User モデル (対応するテーブルは ‘users’)
class User(Base):
tablename = ‘users’ # 対応するテーブル名
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True, nullable=False)
email = Column(String(120), unique=True, nullable=False)
created_at = Column(DateTime, server_default=func.now())
# リレーションシップ: このユーザーが作成した投稿を取得するための設定
# 'Post' モデルを 'posts' という名前で参照できるようにする
posts = relationship('Post', backref='author', lazy='dynamic') # 'lazy='dynamic'' は後述
def __repr__(self):
# モデルのインスタンスを文字列で表現する方法
return f"<User(id={self.id}, username='{self.username}')>"
Post モデル (対応するテーブルは ‘posts’)
class Post(Base):
tablename = ‘posts’ # 対応するテーブル名
id = Column(Integer, primary_key=True)
title = Column(String(100), nullable=False)
body = Column(Text, nullable=False)
created_at = Column(DateTime, server_default=func.now())
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
# リレーションシップ: この投稿を作成したユーザーを取得するための設定
# 'User' モデルを 'author' という名前で参照できるようにする
# backref='posts' は User モデル側で 'posts' リレーションを自動生成する設定
# user = relationship('User', backref='posts') # ORMセクションではこちらでシンプルに
def __repr__(self):
return f"<Post(id={self.id}, title='{self.title}', user_id={self.user_id})>"
定義したモデル(テーブル)をデータベースに作成(まだ存在しない場合)
Base.metadata.create_all(engine)
print(“Models ‘User’ and ‘Post’ defined and tables created (if they didn’t exist).”)
“`
このコードでは、declarative_base()
で作成した Base
を継承して User
クラスと Post
クラスを定義しています。
* __tablename__
クラス属性で、このモデルが対応するデータベーステーブル名を指定します。
* クラス属性として Column
オブジェクトを定義することで、テーブルのカラムを定義します。Column
の定義方法はCoreスタイルと同じです。
* relationship()
はORM独自の機能で、他のモデルとの関連付け(リレーションシップ)を定義します。これにより、例えば user.posts
のように関連するオブジェクトにアクセスできるようになります。backref
オプションを使うと、関連付けられたモデル(この例ではPost
)からも元のモデル(User
)を参照できるようになります(post.author
)。
* __repr__
メソッドは、デバッグ時にモデルのインスタンスをprintなどで表示した際に分かりやすい形式で表示されるように定義しています。
* Base.metadata.create_all(engine)
は、Base
を継承して定義された全てのモデルに対応するテーブルをデータベースに作成します。Coreスタイルの metadata.create_all()
と同じ機能ですが、ORMの場合は Base.metadata
を使用します。
セッションの概念
SQLAlchemy ORMにおける最も重要な概念の一つが「セッション (Session
)」です。セッションは、データベースとの対話を行うための主要なインターフェースであり、以下の役割を担います。
- データベース接続の管理: セッションは内部的にデータベースコネクションプールからコネクションを取得し、SQLクエリの実行に使用します。
- トランザクション管理: セッションはデフォルトでトランザクションを開始し、
.commit()
で変更を確定するか、.rollback()
で変更を取り消すかを制御します。 - オブジェクトの状態管理: セッションは、ロードされたオブジェクト(モデルのインスタンス)の状態(変更されたか、新規に追加されたかなど)を追跡します。
.add()
でオブジェクトをセッションに追加すると、セッションはそのオブジェクトを管理下に置きます。.flush()
は、保留中の変更をデータベースに反映しますが、トランザクションは確定しません。.commit()
は、flush()
を行った後、トランザクションを確定します。 - アイデンティティマップ (Identity Map): セッションは、データベースからロードされたオブジェクトのキャッシュを維持します。同じ主キーを持つ行が複数回ロードされても、セッションは常に同じPythonオブジェクトインスタンスを返します。これにより、メモリ上のオブジェクトの一貫性が保たれます。
セッションを使用するには、まず sessionmaker
を使ってセッションクラスを作成し、そこからセッションインスタンスを取得します。
“`python
from sqlalchemy.orm import sessionmaker
sessionmaker を作成
bind 引数でどのエンジンを使用するかを指定
SessionLocal = sessionmaker(bind=engine)
セッションインスタンスを取得
session = SessionLocal()
— データベース操作 —
例: 新しいユーザーを追加
new_user = User(username=’john_doe’, email=’[email protected]’)
session.add(new_user)
session.commit()
— セッションのクローズ —
操作が完了したらセッションをクローズすることが重要
session.close()
“`
セッションインスタンスはスレッドセーフではないため、Webアプリケーションのように複数のリクエストが同時に処理される環境では、リクエストごとに新しいセッションインスタンスを作成するのが一般的なプラクティスです。セッションは操作の開始から終了までの一連の流れ(例えば、Webリクエストの処理全体)を表す単位と考えると良いでしょう。操作が完了したら、セッションは必ずクローズするか、エラー時はロールバックする必要があります。
Flask-SQLAlchemyはこのセッション管理(特にリクエストごとの生成と自動クローズ/ロールバック)を自動化してくれます。
CRUD操作 (ORMスタイル)
ORMを使用すると、Pythonオブジェクトを操作する感覚でデータベースのCRUD(Create, Read, Update, Delete)操作を行えます。
session
オブジェクトを通じてこれらの操作を行います。
C (Create): データの追加
新しいモデルインスタンスを作成し、セッションに追加(add)してコミット(commit)します。
“`python
セッション取得 (前述の SessionLocal を使用)
session = SessionLocal()
try:
# 新しいユーザーオブジェクトを作成
new_user = User(username=’alice’, email=’[email protected]’)
# セッションにオブジェクトを追加
session.add(new_user)
# 追加のデータをここでさらに作成・追加することも可能
# post1 = Post(title='First Post', body='This is my first post.', author=new_user)
# post2 = Post(title='Second Post', body='Another post.', author=new_user)
# session.add(post1)
# session.add(post2)
# データベースに変更をコミット(確定)
session.commit()
print(f"User '{new_user.username}' added with ID: {new_user.id}")
except Exception as e:
session.rollback() # エラーが発生したらロールバック
print(f”An error occurred: {e}”)
finally:
session.close() # セッションをクローズ
“`
session.add()
はオブジェクトをセッションの管理下に置くだけで、すぐにはデータベースにINSERT文を実行しません。session.commit()
を呼び出した際に、セッションが管理している全ての変更(追加、更新、削除)がまとめてデータベースにフラッシュされ、トランザクションが確定されます。
R (Read): データの取得
session.query()
を使用してデータを取得します。query()
の引数には、取得したいモデルクラスを指定します。様々なメソッドを使って条件を指定したり、結果を絞り込んだりできます。
“`python
session = SessionLocal()
try:
# 全てのユーザーを取得
users = session.query(User).all()
print(“All users:”)
for user in users:
print(user) # repr メソッドが呼び出される
print("-" * 20)
# IDが1のユーザーを取得 (primary key で取得)
# get() は primary key のみを引数にとり、オブジェクトが見つからない場合は None を返す
user_by_id = session.query(User).get(1)
if user_by_id:
print(f"User with ID 1: {user_by_id}")
else:
print("User with ID 1 not found.")
print("-" * 20)
# usernameが'alice'のユーザーを取得 (条件を指定)
# filter() で条件式を指定し、first() で最初に見つかった1件を取得
# first() は見つからない場合 None を返す
user_by_username = session.query(User).filter(User.username == 'alice').first()
if user_by_username:
print(f"User with username 'alice': {user_by_username}")
print("-" * 20)
# メールアドレスに'example.com'を含む全てのユーザーを取得 (LIKE演算子)
# filter() 内で like() メソッドを使用
users_by_email_domain = session.query(User).filter(User.email.like('%example.com')).all()
print("Users with example.com email:")
for user in users_by_email_domain:
print(user)
print("-" * 20)
# 特定のユーザーの投稿を取得 (リレーションシップ経由)
# User モデルに posts リレーションシップが定義されている場合
# ただし、lazy='dynamic' の場合は query オブジェクトが返るため .all() などが必要
if user_by_id: # ID=1のユーザーが存在する場合
print(f"Posts by {user_by_id.username}:")
# lazy='dynamic' の場合:
# posts_by_user = user_by_id.posts.all()
# lazy='select' (デフォルト) の場合:
posts_by_user = session.query(Post).filter(Post.user_id == user_by_id.id).all() # ORMクエリで明示的に取得
for post in posts_by_user:
print(post)
except Exception as e:
print(f”An error occurred during read: {e}”)
finally:
session.close()
“`
主な取得メソッド:
* all()
: クエリの結果全てをリストとして取得します。
* first()
: 結果の最初の1件を取得します。結果がない場合は None
を返します。
* one()
: 結果が厳密に1件の場合にそのオブジェクトを取得します。0件や2件以上の場合はエラーが発生します。
* scalar()
: 結果が1行1カラムの場合にその値を取得します。
* get(primary_key)
: 指定した主キーを持つオブジェクトを直接取得します。結果がない場合は None
を返します。filter(Model.id == primary_key).first()
と似ていますが、アイデンティティマップのキャッシュから高速に取得できる可能性があります。
条件指定には filter()
や filter_by()
を使用します。
* filter()
: Pythonの比較演算子(==
, !=
, >
, <
, >=
, <=
, in_
, like
など)を使った式を指定します。例: filter(User.username == 'alice')
, filter(User.id > 10)
, filter(User.email.in_(['[email protected]', '[email protected]']))
* filter_by()
: キーワード引数を使って、カラム名と値の等価条件を指定します。例: filter_by(username='alice')
, filter_by(id=1)
U (Update): データの更新
取得したオブジェクトの属性値を変更し、セッションをコミットします。セッションがオブジェクトの状態変化を自動的に追跡します。
“`python
session = SessionLocal()
try:
# 更新したいオブジェクトを取得
user_to_update = session.query(User).filter_by(username=’alice’).first()
if user_to_update:
# オブジェクトの属性を変更
user_to_update.email = '[email protected]'
user_to_update.username = 'alice_smith' # usernameも変更してみる
# セッションに明示的に add() する必要はない(既にセッション管理下にあるため)
# 変更をコミット
session.commit()
print(f"User '{user_to_update.username}' updated.")
else:
print("User 'alice' not found for update.")
except Exception as e:
session.rollback()
print(f”An error occurred during update: {e}”)
finally:
session.close()
“`
D (Delete): データの削除
削除したいオブジェクトを取得し、セッションから削除(delete)してコミットします。
“`python
session = SessionLocal()
try:
# 削除したいオブジェクトを取得
user_to_delete = session.query(User).filter_by(username=’alice_smith’).first()
if user_to_delete:
# セッションからオブジェクトを削除
session.delete(user_to_delete)
# 変更をコミット
session.commit()
print(f"User '{user_to_delete.username}' deleted.")
else:
print("User 'alice_smith' not found for deletion.")
except Exception as e:
session.rollback()
print(f”An error occurred during delete: {e}”)
finally:
session.close()
“`
session.delete()
も、オブジェクトを削除済みとしてマークするだけで、すぐにデータベースにDELETE文を実行するわけではありません。commit()
の際に実際の削除処理が行われます。
これらのCRUD操作を通じて、SQL文を意識することなくPythonオブジェクトとしてデータベースを操作できることがわかります。
Flaskとの連携:Webアプリケーションでの活用
FlaskのようなWebフレームワークでSQLAlchemyを利用する場合、Webリクエストのライフサイクルとデータベースセッションの管理を適切に行うことが重要です。
なぜFlaskとSQLAlchemyを連携させるのか?
- データ永続化: Webアプリケーションで扱われるデータ(ユーザー、投稿、注文など)をデータベースに安全に保存するため。
- ビジネスロジックの実装: データベースからデータを読み込み、加工し、保存するアプリケーションの核となる処理を実装するため。
- 効率的な開発: ORMを使うことで、データベース操作に関するボイラープレートコードを削減し、開発スピードを向上させるため。
基本的な連携方法 (手動でのエンジン/セッション管理)
FlaskアプリケーションでSQLAlchemyを手動で使う場合、以下のような課題があります。
- エンジンの管理: エンジンインスタンスはアプリケーション全体でシングルトンとして管理するのが望ましいです。
- セッションの管理: 各Webリクエストの処理を開始する際に新しいセッションを作成し、リクエスト処理の終了時に必ずセッションをクローズする必要があります。エラーが発生した場合はロールバックも必要です。これを手動で、全てのビュー関数や関連するコードで行うのは手間がかかり、忘れやすいです。
- スコープ付きセッション: Webアプリケーションでは、通常、リクエストのスコープ内でセッションを管理する必要があります。つまり、同じリクエスト内でデータベースにアクセスする際には、常に同じセッションインスタンスを使用する必要があります。SQLAlchemyの
scoped_session
を使うことでこれを実現できますが、これも設定が必要です。
手動でのセッション管理の例(簡略化):
“`python
from flask import Flask, g # gはリクエスト固有のデータストア
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session
DATABASE_URI = ‘sqlite:///app.db’
engine = create_engine(DATABASE_URI)
thread-local なセッションを作成 (scoped_session)
Session = scoped_session(sessionmaker(bind=engine))
app = Flask(name)
@app.before_request
def before_request():
# リクエストごとに新しいセッションを作成し、g に保存
g.db_session = Session()
@app.teardown_request
def teardown_request(exception=None):
# リクエスト終了時にセッションを自動的にクローズ
# exception が None でない場合はロールバックも検討
db_session = g.pop(‘db_session’, None)
if db_session is not None:
if exception:
db_session.rollback() # エラー時はロールバック
db_session.close()
@app.route(‘/’)
def index():
db_session = g.db_session
# ここで db_session を使ってデータベース操作…
# users = db_session.query(User).all()
# …
return 'Hello, world!'
if name == ‘main‘:
# アプリケーション起動前にテーブル作成など
# Base.metadata.create_all(engine) # ORMモデルを使っている場合
app.run(debug=True)
“`
この手動での設定は可能ですが、煩雑であり、特に初心者には敷居が高いです。ここでFlask-SQLAlchemyの登場です。
Flask-SQLAlchemyエクステンションの紹介と利点
Flask-SQLAlchemyは、FlaskアプリケーションでSQLAlchemyをより簡単に、かつFlaskの設計思想に沿って使えるようにする拡張機能です。主な利点は以下の通りです。
- 簡単なセットアップ: Flaskアプリケーションオブジェクトに
SQLAlchemy
インスタンスを紐付けるだけで基本的な設定が完了します。 - 自動的なセッション管理: Webリクエストの開始時にセッションを自動的に作成し、リクエスト終了時(成功時はコミット、エラー発生時はロールバック)にセッションを自動的にクローズします。開発者はビュー関数内で
db.session
を使うだけで、セッションのライフサイクル管理を気にする必要がほとんどなくなります。 - 宣言的モデルの統合:
db.Model
という便利なベースクラスを提供し、宣言的スタイルのモデル定義をFlaskアプリケーションに統合します。 - 共通のデータベース操作インターフェース:
db.session.query()
など、データベース操作のための統一されたインターフェースを提供します。 - 設定の容易さ: Flaskの設定メカニズム(
app.config
)を通じて、データベースURIなどの設定を簡単に行えます。
Flask-SQLAlchemyのインストールと設定
Flask-SQLAlchemyはpipで簡単にインストールできます。
bash
pip install Flask-SQLAlchemy
設定は、Flaskアプリケーションのconfigを通じて行います。最低限必要なのは、データベースURIを指定する SQLALCHEMY_DATABASE_URI
設定キーです。
“`python
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(name)
設定ファイルのパスを指定することも可能
app.config.from_pyfile(‘config.py’)
データベースURIを設定 (SQLiteの場合)
config[‘SQLALCHEMY_DATABASE_URI’] という形式でもOK
app.config[‘SQLALCHEMY_DATABASE_URI’] = ‘sqlite:///site.db’
SQLAlchemy オブジェクトを作成し、Flask アプリケーションと紐付ける
db = SQLAlchemy(app)
必要に応じて他の設定も行う
例: SQLAlchemy イベントシステムの無効化 (パフォーマンス向上のため本番環境では検討)
app.config[‘SQLALCHEMY_TRACK_MODIFICATIONS’] = False
print(f”Flask-SQLAlchemy initialized with URI: {app.config[‘SQLALCHEMY_DATABASE_URI’]}”)
“`
SQLAlchemy(app)
とすることで、db
オブジェクトが作成され、これがFlaskアプリケーションと紐付けられます。この db
オブジェクトを通じて、モデル定義、セッションへのアクセス、クエリの実行など、ほとんどのデータベース操作を行います。
SQLALCHEMY_TRACK_MODIFICATIONS
は、モデルの変更がシグナルを発するかどうかを制御する設定です。リソースを消費するため、不要であれば False
に設定することが推奨されます。
モデルの定義 (Flask-SQLAlchemyスタイル)
Flask-SQLAlchemyを使用する場合、モデルクラスは db.Model
を継承して定義します。これにより、SQLAlchemyの宣言的ベースクラスだけでなく、Flask-SQLAlchemy固有の機能(例: db.session
との連携)が利用できるようになります。
カラム定義には、db.Column
, db.Integer
, db.String
などの db
オブジェクトに紐付けられた型を使用します。これは、Flask-SQLAlchemyが内部的にSQLAlchemyの型をラップしているためです。
例:User
モデルと Post
モデルを定義する (Flask-SQLAlchemyスタイル)
“`python
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.sql import func
app = Flask(name)
app.config[‘SQLALCHEMY_DATABASE_URI’] = ‘sqlite:///site.db’
app.config[‘SQLALCHEMY_TRACK_MODIFICATIONS’] = False # 推奨設定
db = SQLAlchemy(app)
User モデル
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
created_at = db.Column(db.DateTime, server_default=func.now()) # func.now() はそのまま使用
# リレーションシップ
# 'Post' モデルの 'user' リレーションシップの backref='author' に対応
# lazy='dynamic' は、関連オブジェクトを取得する際にクエリを返すようにする設定
# 後で filter().all() のようにさらに絞り込める
posts = db.relationship('Post', backref='author', lazy='dynamic')
def __repr__(self):
return f"<User(id={self.id}, username='{self.username}')>"
Post モデル
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
body = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, server_default=func.now())
# 外部キー。テーブル名は小文字、カラム名は小文字+アンダースコアが慣例
user_id = db.Column(db.Integer, db.ForeignKey(‘user.id’), nullable=False)
# リレーションシップ
# 'User' モデルを参照。backref は User モデル側で 'posts' リレーションを生成
# author = db.relationship('User', backref='posts') # User モデルで backref='author' としたので、ここでは不要
def __repr__(self):
return f"<Post(id={self.id}, title='{self.title}', user_id={self.user_id})>"
— テーブルの作成 —
アプリケーションコンテキスト内で実行する必要がある
通常は初期設定スクリプトやインタラクティブシェルで行う
with app.app_context():
db.create_all()
print(“Database tables created.”)
if name == ‘main‘:
app.run(debug=True)
“`
ポイント:
* db.Model
を継承します。
* カラムの型や関数は db.
プレフィックスを付けて呼び出します(例: db.Column
, db.Integer
, db.ForeignKey
)。ただし、func.now()
のような汎用的なSQL関数は SQLAlchemy の func
から直接インポートして使用します。
* テーブル名は __tablename__
で明示的に指定しない場合、クラス名の小文字スネークケース(例: User
-> user
, Post
-> post
)がデフォルトで使用されます。明示的に指定することも可能ですが、デフォルトで問題ない場合が多いです。外部キーの参照先テーブル名も小文字になります (db.ForeignKey('user.id')
)。
* db.relationship
の使い方は SQLAlchemy ORM と同じです。
データベースの作成
Flask-SQLAlchemyでモデルを定義したら、それに対応するデータベーステーブルを作成する必要があります。これは db.create_all()
メソッドで行います。
db.create_all()
はアプリケーションコンテキスト内で実行する必要があります。通常、アプリケーションの起動スクリプトの一部として、または開発中にインタラクティブシェルから一度だけ実行します。
“`python
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.sql import func
app = Flask(name)
app.config[‘SQLALCHEMY_DATABASE_URI’] = ‘sqlite:///site.db’
app.config[‘SQLALCHEMY_TRACK_MODIFICATIONS’] = False
db = SQLAlchemy(app)
— User および Post モデル定義 — (前述のコードをここに配置)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
created_at = db.Column(db.DateTime, server_default=func.now())
posts = db.relationship(‘Post’, backref=’author’, lazy=’dynamic’)
def repr(self): return f”
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
body = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, server_default=func.now())
user_id = db.Column(db.Integer, db.ForeignKey(‘user.id’), nullable=False)
def repr(self): return f”
———————————-
データベーステーブルを作成するスクリプトとして実行する場合
if name == ‘main‘:
with app.app_context():
db.create_all()
print(“Database tables created successfully.”)
# アプリケーションを実行する場合
# app.run(debug=True)
“`
db.create_all()
は、db.Model
を継承した全てのクラスに対応するテーブルをデータベースに作成します。既にテーブルが存在する場合は何も行われません。これは開発初期段階でデータベースをセットアップするのに便利ですが、既存のデータベーススキーマを変更する(カラムを追加・変更・削除する)場合には使えません。スキーマ変更にはマイグレーションツール(後述のAlembicなど)を使用します。
リクエストコンテキストとセッション管理 (自動化の恩恵)
Flask-SQLAlchemyの最大の利点は、リクエストコンテキスト内でのセッションの自動管理です。
- Flaskが新しいリクエストを受け取ると、Flask-SQLAlchemyは自動的に新しい
db.session
インスタンスを作成し、それをスレッドローカルまたはコンテキストローカル(Flask
のapp_context
やrequest_context
に紐づく)に紐付けます。 - ビュー関数やその中で呼び出される関数は、どこからでも
db.session
を参照するだけで、そのリクエスト固有のセッションインスタンスにアクセスできます。 - ビュー関数が例外なく完了した場合、Flask-SQLAlchemyは自動的に
db.session.commit()
を呼び出し、保留中の変更をデータベースに確定します。 - ビュー関数で例外が発生した場合、Flask-SQLAlchemyは自動的に
db.session.rollback()
を呼び出し、リクエスト中に加えられた全てのデータベース変更を取り消します。 - リクエストの処理が終了すると(成功・失敗に関わらず)、Flask-SQLAlchemyは自動的に
db.session.close()
を呼び出し、セッションをクリーンアップします。
これにより、開発者は手動で session = SessionLocal()
や session.commit()
, session.close()
, session.rollback()
といったコードを書く必要がなくなり、ビュー関数内では単に db.session.add()
, db.session.query()
, db.session.delete()
などを呼び出すだけでよくなります。
“`python
from flask import Flask, render_template_string, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.sql import func
app = Flask(name)
app.config[‘SQLALCHEMY_DATABASE_URI’] = ‘sqlite:///site.db’
app.config[‘SQLALCHEMY_TRACK_MODIFICATIONS’] = False
db = SQLAlchemy(app)
— User および Post モデル定義 — (前述のコードをここに配置)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
created_at = db.Column(db.DateTime, server_default=func.now())
posts = db.relationship(‘Post’, backref=’author’, lazy=’dynamic’)
def repr(self): return f”
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
body = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, server_default=func.now())
user_id = db.Column(db.Integer, db.ForeignKey(‘user.id’), nullable=False)
def repr(self): return f”
———————————-
ダミーデータの投入 (開発用)
def inject_dummy_data():
with app.app_context():
# 既存のデータを全て削除 (開発時のみ!)
db.drop_all()
db.create_all()
# ダミーユーザー作成
user1 = User(username='alice', email='[email protected]')
user2 = User(username='bob', email='[email protected]')
db.session.add_all([user1, user2])
db.session.commit() # ユーザーをコミットしてIDを生成
# ダミー投稿作成
post1 = Post(title='Alice\'s First Post', body='Hello from Alice!', author=user1)
post2 = Post(title='Bob\'s Post', body='Bob here.', author=user2)
post3 = Post(title='Alice\'s Second Post', body='Another one from Alice.', author=user1)
db.session.add_all([post1, post2, post3])
db.session.commit() # 投稿をコミット
print("Dummy data injected.")
ビュー関数内でのデータベース操作例
@app.route(‘/’)
def index():
# db.session を使ってクエリを実行
users = User.query.all() # Flask-SQLAlchemy は Model.query プロパティも提供
posts = Post.query.order_by(Post.created_at.desc()).all() # order_by で並べ替え
user_list = "<br>".join([str(u) for u in users])
post_list = "<br>".join([str(p) for p in posts])
# HTMLテンプレート内で表示
html = f"""
<h1>Users and Posts</h1>
<h2>Users</h2>
{user_list}
<h2>Posts</h2>
{post_list}
<p><a href="{url_for('add_user_form')}">Add New User</a></p>
"""
return render_template_string(html)
@app.route(‘/add_user’, methods=[‘GET’, ‘POST’])
def add_user_form():
if request.method == ‘POST’:
username = request.form[‘username’]
email = request.form[‘email’]
if not username or not email:
return “Username and Email are required!”, 400
existing_user = User.query.filter_by(username=username).first()
if existing_user:
return f"User '{username}' already exists!", 400
new_user = User(username=username, email=email)
db.session.add(new_user)
# commit() はリクエスト終了時に自動で行われるが、明示的に行うことも可能
# 例: 直後に追加したユーザーの ID を利用したい場合など
# db.session.commit()
# print(f"Added user with ID: {new_user.id}")
return redirect(url_for('index'))
# GET リクエストの場合、フォームを表示
html = """
<h1>Add New User</h1>
<form method="post">
Username: <input type="text" name="username"><br>
Email: <input type="email" name="email"><br>
<input type="submit" value="Add User">
</form>
<p><a href="{url_for('index')}">Back to Home</a></p>
"""
return render_template_string(html)
@app.route(‘/user/
def view_user(user_id):
# IDでユーザーを取得。見つからない場合は 404 エラー
user = User.query.get_or_404(user_id)
# リレーションシップを通じてそのユーザーの投稿を取得
# lazy='dynamic' なので .all() や .filter().all() が必要
# posts = user.posts.all()
# または、クエリで明示的に取得
posts = Post.query.filter_by(user_id=user.id).all()
user_info = f"ID: {user.id}, Username: {user.username}, Email: {user.email}"
post_list = "<br>".join([f"Title: {p.title}, Body: {p.body}" for p in posts]) if posts else "No posts found."
html = f"""
<h1>User Details: {user.username}</h1>
<p>{user_info}</p>
<h2>Posts by {user.username}</h2>
{post_list}
<p><a href="{url_for('index')}">Back to Home</a></p>
"""
return render_template_string(html)
———————————-
アプリケーションの実行部分
if name == ‘main‘:
# アプリケーション起動時にデータベースとダミーデータを作成(開発時のみ)
with app.app_context():
inject_dummy_data() # ダミーデータを投入する関数を呼び出し
app.run(debug=True)
“`
上記の例では:
* db = SQLAlchemy(app)
で db
オブジェクトを作成。
* User
と Post
モデルは db.Model
を継承して定義。
* ビュー関数内では、User.query.all()
, Post.query.order_by(...)
, User.query.get_or_404(...)
, Post.query.filter_by(...)
のように Model.query
を使ってクエリを実行しています。Model.query
は Flask-SQLAlchemy が提供する便利なショートカットで、内部的には db.session.query(Model)
と同等です。
* 新しいユーザーの追加は new_user = User(...)
, db.session.add(new_user)
で行います。
* db.session.commit()
は add_user_form
関数内で明示的に呼び出すことも可能ですが、POSTリクエスト処理が成功すればリクエスト終了時に自動コミットされるため省略可能です(ただし、追加したオブジェクトのIDなどをすぐに利用したい場合は明示的なコミットが必要な場合があります)。
この自動セッション管理により、開発者はアプリケーションのロジックに集中できます。
リレーションシップ (一対一、一対多、多対多)
リレーショナルデータベースの重要な概念であるリレーションシップは、SQLAlchemy ORM(およびFlask-SQLAlchemy)で relationship()
関数を使って定義できます。
-
一対多 (One-to-Many): 一つの親オブジェクトが複数の子オブジェクトを持つ関係。例: 一人のユーザーが複数の投稿を持つ。
- 定義: 親モデルに
relationship()
を定義し、子モデルに外部キーを定義します。 - アクセス: 親オブジェクトから子のリスト(またはクエリ)、子オブジェクトから親オブジェクトにアクセスできます。
- 上記の
User
とPost
の例が一対多です。User
モデルにposts = db.relationship('Post', ...)
、Post
モデルにuser_id = db.Column(db.Integer, db.ForeignKey('user.id'))
を定義しました。author
はPost
モデルからUser
モデルへの参照、posts
はUser
モデルからPost
モデルへの参照です。
- 定義: 親モデルに
-
一対一 (One-to-One): 親オブジェクトが厳密に一つの子オブジェクトを持つ関係。例: 一人のユーザーが一つだけプロフィールを持つ。
- 定義: 親モデルまたは子モデルのどちらか一方に
relationship()
を定義し、かつ、そのリレーションシップが「一つだけ」であることを示すuselist=False
を指定します。通常、子モデルに外部キーを定義します。 - アクセス: 親オブジェクトから子オブジェクト、子オブジェクトから親オブジェクトにアクセスできます。
- 定義: 親モデルまたは子モデルのどちらか一方に
-
多対多 (Many-to-Many): 複数の親オブジェクトが複数の子オブジェクトと関連付けられる関係。中間テーブル(関連テーブル、Association Table)が必要です。例: 複数の投稿に複数のタグを付ける。
- 定義: 中間テーブルを定義し、両方の親モデルと子モデルからその中間テーブルを経由して
relationship()
を定義します。secondary
引数で中間テーブルを指定します。 - アクセス: 親オブジェクトから子のリスト、子オブジェクトから親のリストにアクセスできます。
- 定義: 中間テーブルを定義し、両方の親モデルと子モデルからその中間テーブルを経由して
多対多リレーションシップの例 (ユーザーと役割)
“`python
中間テーブルの定義 (通常は ORM モデルではなく Core の Table で定義)
user_roles = db.Table(
‘user_roles’,
db.Column(‘user_id’, db.Integer, db.ForeignKey(‘user.id’), primary_key=True),
db.Column(‘role_id’, db.Integer, db.ForeignKey(‘role.id’), primary_key=True)
)
Role モデル
class Role(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
# 多対多リレーションシップ
# 'User' モデルと中間テーブル 'user_roles' を経由して関連付ける
users = db.relationship(
'User',
secondary=user_roles,
backref=db.backref('roles', lazy='dynamic') # User モデル側で roles という名前で参照
)
def __repr__(self):
return f"<Role(id={self.id}, name='{self.name}')>"
User モデルに roles リレーションシップが backref で追加されるため、User モデルの定義変更は不要
(前述の User モデル定義に roles 属性が自動で追加される)
使用例
with app.app_context():
# テーブル作成時に中間テーブルも作成される
# db.create_all()
# 新しい役割を追加
admin_role = Role(name=’Admin’)
editor_role = Role(name=’Editor’)
db.session.add_all([admin_role, editor_role])
db.session.commit()
print(“Roles added.”)
# ユーザーに役割を割り当て
user = User.query.filter_by(username=’alice’).first() # 既存のユーザーを取得
if user:
user.roles.append(admin_role) # 多対多リレーションシップを通じて追加
user.roles.append(editor_role)
db.session.commit()
print(f”Roles assigned to {user.username}.”)
# ユーザーの役割を取得
user_with_roles = User.query.filter_by(username=’alice’).first()
if user_with_roles:
print(f”{user_with_roles.username}’s roles:”)
for role in user_with_roles.roles.all(): # lazy=’dynamic’ なので .all() が必要
print(f”- {role.name}”)
# 役割を持つユーザーを取得
admin_role = Role.query.filter_by(name=’Admin’).first()
if admin_role:
print(f”Users with ‘{admin_role.name}’ role:”)
for user in admin_role.users.all(): # lazy=’dynamic’ なので .all() が必要
print(f”- {user.username}”)
“`
リレーションシップを使うことで、データベース上の関連をPythonオブジェクト上の関連として直感的に扱えるようになります。lazy
オプションは、関連オブジェクトがいつロードされるかを制御します(例: select
, joined
, subquery
, dynamic
)。デフォルトは select
で、関連オブジェクトにアクセスしたときに個別のクエリが実行されます。dynamic
は、関連オブジェクトのリストをすぐにロードせず、クエリビルダを返すため、さらにフィルタリングやソートを行いたい場合に便利です。
マイグレーション (Alembicの紹介)
db.create_all()
は開発初期には便利ですが、一度データベースが作成された後でモデルの定義(テーブル構造)を変更した場合に対応できません。テーブルにカラムを追加したり、カラムのデータ型を変更したり、新しいテーブルを追加したりする際には、既存のデータを維持したままデータベーススキーマを更新する必要があります。この作業を「データベースマイグレーション」と呼びます。
SQLAlchemyのための最も一般的なマイグレーションツールはAlembicです。Alembicは、データベースの変更点をスクリプトとして記録し、それらを順に実行することでスキーマを現在の状態から新しい状態へ移行(upgrade)させたり、前の状態に戻したり(downgrade)できます。
Alembicの基本的な流れ:
- Alembicをインストールします (
pip install alembic
)。 - Alembicプロジェクトを初期化します (
alembic init alembic
)。これにより、設定ファイルやマイグレーションスクリプトを格納するディレクトリ構造が作成されます。 - Alembicの設定ファイルを編集し、SQLAlchemyエンジンやモデルのメタデータを指定します。Flask-SQLAlchemyと連携する場合は、FlaskアプリケーションをAlembicが読み込めるように設定します。
- モデル定義を変更した後、新しいマイグレーションスクリプトを自動生成します (
alembic revision --autogenerate -m "Add new_column to user table"
)。Alembicは現在のデータベーススキーマとモデル定義を比較し、差分に基づいてマイグレーションコードを生成します。 - 生成されたマイグレーションスクリプトを確認・編集し、必要に応じて手動で調整します。
- マイグレーションを実行し、データベーススキーマを更新します (
alembic upgrade head
)。
Flask-SQLAlchemyを使用している場合、Flask-Migrateという拡張機能を使うと、Alembicとの連携がよりスムーズになります。Flask-MigrateはAlembicのコマンドをFlaskのCLIコマンドとして利用できるようにラップしてくれます。
Flask-Migrateの基本的な流れ:
- Flask-Migrateをインストールします (
pip install Flask-Migrate
)。 - Flaskアプリケーションに
Migrate
オブジェクトを紐付けます。 - アプリケーションコンテキスト内でマイグレーションリポジトリを初期化します (
flask db init
)。 - モデル定義を変更した後、マイグレーションスクリプトを生成します (
flask db migrate -m "message"
)。 - マイグレーションを実行します (
flask db upgrade
)。
“`python
app.py (またはrun.pyなど)
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate # Flask-Migrate をインポート
from sqlalchemy.sql import func
app = Flask(name)
app.config[‘SQLALCHEMY_DATABASE_URI’] = ‘sqlite:///site.db’
app.config[‘SQLALCHEMY_TRACK_MODIFICATIONS’] = False
db = SQLAlchemy(app)
migrate = Migrate(app, db) # Migrate オブジェクトを作成し、app と db を紐付ける
— User および Post モデル定義 — (前述のコードをここに配置)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
created_at = db.Column(db.DateTime, server_default=func.now())
posts = db.relationship(‘Post’, backref=’author’, lazy=’dynamic’)
def repr(self): return f”
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
body = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, server_default=func.now())
user_id = db.Column(db.Integer, db.ForeignKey(‘user.id’), nullable=False)
def repr(self): return f”
———————————-
Flask CLI コマンドとしてマイグレーションが利用可能になる
ターミナルで:
export FLASK_APP=app.py
flask db init # 初回のみリポジトリ作成
flask db migrate -m “initial migration” # モデル定義に基づいてマイグレーションスクリプト生成
flask db upgrade # マイグレーション実行 (データベースに適用)
if name == ‘main‘:
# db.create_all() はマイグレーション管理下では通常不要
app.run(debug=True)
“`
マイグレーションは本番環境でのデータベーススキーマ更新に不可欠なツールです。開発が進むにつれてモデル定義が変更されるのは避けられないため、早い段階でマイグレーションツールを導入することが推奨されます。
実践的なトピック
テストの考慮事項 (データベースのリセット)
データベースを使用するアプリケーションのテストでは、テストケースごとにデータベースの状態を既知のクリーンな状態に戻す必要があります。SQLiteのインメモリデータベース (sqlite:///:memory:
) は、テストごとに完全にリセットされるため、テスト用データベースとして非常に便利です。
“`python
import unittest
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
テスト用のアプリケーションを作成するファクトリ関数
def create_test_app():
app = Flask(name)
# インメモリ SQLite データベースを使用
app.config[‘SQLALCHEMY_DATABASE_URI’] = ‘sqlite:///:memory:’
app.config[‘SQLALCHEMY_TRACK_MODIFICATIONS’] = False
app.config[‘TESTING’] = True # テストモード有効化
db = SQLAlchemy(app)
# --- モデル定義 --- (テスト対象のモデルをここに配置)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self): return f"<User(id={self.id}, username='{self.username}')>"
# --------------------
# アプリケーションと db オブジェクトを返す
return app, db, User
テストクラス
class DatabaseTestCase(unittest.TestCase):
def setUp(self):
# 各テストメソッドの前に実行
self.app, self.db, self.User = create_test_app()
self.app_context = self.app.app_context()
self.app_context.push() # アプリケーションコンテキストをプッシュ
# インメモリDBにテーブルを作成
self.db.create_all()
def tearDown(self):
# 各テストメソッドの後に実行
# インメモリDBは自動で消えるが、明示的に削除する方が安全な場合も
self.db.session.remove() # 現在のセッションをクリーンアップ
self.db.drop_all() # 全テーブルを削除
self.app_context.pop() # アプリケーションコンテキストをポップ
def test_create_user(self):
# テストケース
new_user = self.User(username='testuser', email='[email protected]')
self.db.session.add(new_user)
self.db.session.commit() # commit が成功するかテスト
# データベースから取得して確認
user = self.User.query.filter_by(username='testuser').first()
self.assertIsNotNone(user)
self.assertEqual(user.email, '[email protected]')
def test_duplicate_username(self):
# 重複ユーザーのテスト
user1 = self.User(username='sameuser', email='[email protected]')
user2 = self.User(username='sameuser', email='[email protected]')
self.db.session.add(user1)
self.db.session.commit() # 最初のユーザーは成功
self.db.session.add(user2)
# 2番目のユーザーはユニーク制約違反で例外が発生するはず
with self.assertRaises(Exception): # 実際には IntegrityError など特定の例外を捕捉すべき
self.db.session.commit()
# 例外発生後、セッションは無効になるのでロールバックが必要
self.db.session.rollback()
テスト実行
if name == ‘main‘:
unittest.main()
“`
setUp
メソッドでテスト用アプリケーションとデータベースを準備し、tearDown
メソッドでクリーンアップします。インメモリSQLiteを使用することで、各テストが完全に独立した環境で実行されることを保証できます。
パフォーマンスの考慮事項 (クエリの最適化、Eager Loading)
大規模なデータセットや高負荷なアプリケーションでは、データベースクエリのパフォーマンスが重要になります。SQLAlchemy(ORM)は便利な反面、非効率なクエリを生成してしまう可能性もあります。
主なパフォーマンス最適化のテクニック:
-
N+1問題の回避 (Eager Loading):
- リレーションシップを使って関連オブジェクトをループ処理する際に、N+1問題が発生しやすいです。例えば、ユーザーのリストを取得し、それぞれのユーザーに対して個別に投稿リストを取得するような場合、ユーザー数(N)+ 1(ユーザーリスト取得)のクエリが実行されてしまいます。
- これを避けるために、
options(joinedload('posts'))
やoptions(subqueryload('posts'))
のようなEager Loadingのテクニックを使用します。これにより、最初のクエリで関連オブジェクトもまとめて取得し、クエリ数を削減できます。 joinedload
: JOINを使って関連データを取得します。subqueryload
: サブクエリを使って関連データを取得します。- Flask-SQLAlchemyでは、
User.query.options(db.joinedload('posts')).all()
のように記述します。
-
不要なカラムのロードを避ける (Deferred Loading, Only Loaded Columns):
- 大きなテキストやバイナリデータなど、特定のカラムが頻繁に使用されない場合は、デフォルトでのロードを遅延させることができます(Deferred Loading)。
- 特定のクエリで必要なカラムだけを明示的に指定して取得することもできます (
query(User.id, User.username).all()
)。
-
クエリの調査:
- SQLAlchemyが実際にどのようなSQLクエリを生成しているかを確認することが重要です。
echo=True
を指定してエンジンを作成すると、実行されるSQLクエリがコンソールに出力されます。create_engine('sqlite:///site.db', echo=True)
- または、SQLAlchemy Loggerを設定して、クエリをログに出力させることも可能です。
- 生成されたSQLクエリをデータベースのExplain Planツールなどで分析し、インデックスの不足や非効率な結合などがないかを確認します。
- SQLAlchemyが実際にどのようなSQLクエリを生成しているかを確認することが重要です。
-
インデックスの活用:
Column
定義時にindex=True
オプションを指定することで、そのカラムにデータベースインデックスを作成できます。db.Column(db.String(120), unique=True, nullable=False, index=True)
- 外部キーカラムや
WHERE
句、ORDER BY
句でよく使用されるカラムにはインデックスを作成することを検討してください。
-
Core機能やRaw SQLの利用:
- ORMでは効率的に表現するのが難しい複雑なクエリや、特定のデータベース機能を利用したい場合は、SQLAlchemy Coreを使ってSQL文を組み立てるか、
db.session.execute()
を使ってRaw SQLを実行することも可能です。
- ORMでは効率的に表現するのが難しい複雑なクエリや、特定のデータベース機能を利用したい場合は、SQLAlchemy Coreを使ってSQL文を組み立てるか、
パフォーマンスチューニングは応用的なトピックですが、アプリケーションがスケールするにつれて避けて通れないため、基本的な考え方を理解しておくことは重要です。
エラーハンドリング
トランザクションとロールバック
前述の通り、Flask-SQLAlchemyはリクエスト終了時に自動でコミットまたはロールバックを行ってくれますが、ビュー関数内の特定の処理ブロックでエラーが発生した場合に、そのブロック内の変更だけを取り消したいといった、より細かいトランザクション制御が必要になることもあります。
このような場合は、手動で db.session.commit()
, db.session.rollback()
を呼び出すことができます。通常は try...except...finally
ブロックと組み合わせます。
“`python
@app.route(‘/update_user/
def update_user(user_id):
user = User.query.get_or_404(user_id)
try:
new_email = request.form.get(‘email’)
if new_email:
user.email = new_email
# ここで他のテーブルへの変更など複数のDB操作を行う可能性
db.session.commit() # ここで明示的にコミット
return "User updated successfully!"
except Exception as e:
db.session.rollback() # エラーが発生したらロールバック
print(f"Error updating user: {e}")
return "An error occurred during update.", 500
# finally:
# Flask-SQLAlchemy がリクエスト終了時に session.close() するので、
# 通常はここで手動で close() する必要はない
# db.session.close() # 手動で close() する場合は、必ず finally で行う
“`
Flask-SQLAlchemyの自動コミット/ロールバックは、リクエスト全体を一つの大きなトランザクションとして扱います。これは多くのWebアプリケーションのユースケースに適していますが、より粒度の細かいトランザクション制御が必要な場合は、明示的な commit()
と rollback()
を使用します。
一般的なエラーと対処法
- データ型エラー、制約違反 (IntegrityError, OperationalErrorなど): モデル定義と実際に挿入/更新しようとするデータの不一致や、NOT NULL制約、UNIQUE制約、外部キー制約違反などで発生します。
- 対処法: データのバリデーションをしっかり行う、適切なデータ型を使用する、データベースのエラーメッセージを確認して原因を特定する。制約違反の場合は
db.session.rollback()
が必要です。
- 対処法: データのバリデーションをしっかり行う、適切なデータ型を使用する、データベースのエラーメッセージを確認して原因を特定する。制約違反の場合は
- セッションの状態に関するエラー (PendingDeprecationWarning, InvalidRequestErrorなど):
commit()
やrollback()
の後にセッションを使おうとしたり、既にクローズされたセッションを使おうとしたりした場合に発生します。- 対処法: Flask-SQLAlchemyを使用している場合は、リクエストコンテキスト外でのセッション利用に注意が必要です。また、手動でセッションを扱っている場合は、ライフサイクル管理(生成、クローズ、ロールバック)を正しく行う必要があります。
db.session
はリクエストが終了するとクリーンアップされるため、長時間生き続けるバックグラウンドタスクなどでデータベースを操作する場合は、別途セッションを管理する必要があります。
- 対処法: Flask-SQLAlchemyを使用している場合は、リクエストコンテキスト外でのセッション利用に注意が必要です。また、手動でセッションを扱っている場合は、ライフサイクル管理(生成、クローズ、ロールバック)を正しく行う必要があります。
- 同時実行によるエラー (Deadlock, Stale Object Errorなど): 複数のクライアントやプロセスが同時に同じデータを変更しようとした場合に発生する可能性があります。
- 対処法: トランザクションの分離レベルを調整する、適切にインデックスを使用する、競合が発生しやすい処理をリトライ可能にする、楽観的ロックや悲観的ロックといった手法を検討する。これはより高度なトピックです。
エラー発生時には、必ず db.session.rollback()
を呼び出してセッションをクリーンな状態に戻すことが重要です。Flask-SQLAlchemyはリクエスト終了時に自動で行いますが、手動で commit()
や rollback()
を行う場合は注意が必要です。
発展的なトピック (簡潔に)
- Raw SQLの実行:
db.session.execute(text("SELECT * FROM users WHERE username = :name"), {'name': 'alice'})
のように、SQLAlchemy Coreのtext()
関数や直接文字列としてSQLクエリを実行できます。ORMでは表現しにくい複雑なクエリや、パフォーマンスが求められる場合に有効です。 - カスタム型: 特定のデータ型(例えばJSONカラム、UUIDなど)をPythonオブジェクトとして扱いたい場合に、SQLAlchemyのカスタム型システムを拡張して対応させることができます。
- イベント: セッションの状態変化(オブジェクトがロードされた、変更されたなど)や、SQLの実行前後にフックを仕掛けるイベントシステムを利用できます。例えば、データの保存前処理やログ記録などに利用できます。
これらのトピックは入門からは一歩進んだ内容ですが、SQLAlchemyが非常に柔軟で拡張性の高いライブラリであることを示しています。
まとめ
この記事では、Pythonにおける強力なデータベースツールであるSQLAlchemyと、軽量WebフレームワークであるFlaskとの連携について詳細に解説しました。
まず、リレーショナルデータベースの基本概念とSQLの役割を確認しました。次に、SQLAlchemyのCoreとORMという二つの側面を紹介し、特にORMにおける宣言的スタイルによるモデル定義、セッションの概念、そしてCRUD操作の基本をコード例と共に学びました。
後半では、Webアプリケーション開発におけるデータベース連携の課題を手動管理の例で示し、Flask-SQLAlchemyという拡張機能がどのようにそれらの課題(特にリクエストごとのセッション管理)を自動化し、開発を容易にするかを詳しく解説しました。Flask-SQLAlchemyを使ったモデル定義、データベースの初期設定、そしてビュー関数内での実際のデータベース操作の例を示しました。
さらに、リレーションシップの定義と利用方法(特に一対多、多対多)、開発・運用で不可欠なマイグレーション(Alembic/Flask-Migrateの紹介)、テストにおけるデータベースリセットの考え方、そしてパフォーマンスチューニングの基本的なアプローチについても触れました。
SQLAlchemyは非常に多機能で奥深いライブラリです。この記事で紹介した内容は、その機能のほんの一部に過ぎません。しかし、Flaskと組み合わせてデータベースを利用する上での核となる概念と実践的な手法は網羅できたかと思います。
次のステップ:
- 実際に手を動かして、この記事のコード例を試してみてください。
- ご自身のFlaskプロジェクトにFlask-SQLAlchemyを導入し、モデルを定義してCRUD機能を実装してみてください。
- Flask-Migrateを使ってマイグレーションを導入してみてください。
- 公式ドキュメント(SQLAlchemyおよびFlask-SQLAlchemy)を参照し、さらに詳細な機能や高度な使い方(例えば、複雑なクエリ、トランザクション分離レベル、イベントなど)について学んでみてください。
- 実際にWebアプリケーションを開発する中で発生する具体的な課題(パフォーマンスボトルネック、複雑なデータ構造など)に対して、SQLAlchemyの様々な機能をどのように活用できるかを調べてみてください。
SQLAlchemyとFlaskの組み合わせは、多くのPython Webアプリケーションで採用されており、強力なデータベースアプリケーションを効率的に開発するための基盤となります。この記事が、皆さんのデータベース連携学習の一助となれば幸いです。
これで、SQLAlchemyとFlaskを使ったデータベース開発の旅を始める準備が整いました。Happy Coding!