
目標
- Part.11で入力したロジック部分のプログラムを理解できる。
Flaskでデータベースを利用する方法
アプリケーションの修正ポイントを確認
前回は、ふたつのテーブルを利用するための注意点を記事の冒頭で確認しました。テーブルは次のように「カテゴリーテーブル」を作成し、「商品テーブル」の商品カテゴリーはこのカテゴリーテーブルを参照するというものでした。

アプリケーションで追加・修正したプログラムの解説
カテゴリーモデルクラスと商品モデルクラスの修正部分(models.py)
models.pyファイル
models.pyファイルではカテゴリーモデルクラスを作成し、商品モデルクラスにカテゴリーモデルクラスのIDを外部キーとして参照していました。
from flask_sqlalchemy import SQLAlchemy
# SQLAlchemyインスタンスを共通で使えるように
db = SQLAlchemy()
# カテゴリーモデル
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False, unique=True)
products = db.relationship(
"Product", backref="category", lazy=True
) # 商品とのリレーション
# 商品モデル(テーブル)の定義
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
price = db.Column(db.Integer, nullable=False)
category_id = db.Column(
db.Integer, db.ForeignKey("category.id"), nullable=True
) # カテゴリID
def __repr__(self):
return f"<Product {self.id} - {self.name} - {self.price}>"
カテゴリーモデルクラス
db.Modelの継承
class Category(db.Model):
Categoryクラスは、db.Modelを継承しています。これにより、Productクラス同様CategoryクラスはSQLAlchemyのモデルとして機能し、データベースのテーブルと関連づけられます。
idカラム
id = db.Column(db.Integer, primary_key=True)
idはデータベースのINTEGER型のカラムで、カテゴリーごとの一意な識別子(主キー)として機能します。primary_key=Trueによって、このカラムが主キーであることが示されます。主キーは各レコードを一意に識別するため、重複しない値である必要があります。
nameカラム
name = db.Column(db.String(50), nullable=False, unique=True)
nameはデータベースのSTRING型のカラムで、カテゴリーの名前を格納します。この名前は最大文字数50文字となっています。
nullable=Falseは、このカラムにNULLが許可されないことを意味します。つまり、カテゴリー名は必須項目であり、空の値は保存できません。
unique=Trueは、このカラムで入力されるカテゴリー名の重複を防ぎます。
productsカラム
products = db.relationship("Product", backref="category", lazy=True)
Product クラス(商品テーブル)との リレーション(関連付け) を定義しています。
db.relationship(“Product”) により、Product テーブルとの 1対多の関係を構築します。
1つのカテゴリーに複数の商品が属する構造になります(例:「ソフトドリンク」カテゴリに「コーラ」「オレンジジュース」が含まれる)。
backref=”category” によって、Productクラスから category属性を使って対応するカテゴリーにアクセスできます。
例: product.category.name → 商品に関連付いたカテゴリ名を取得可能
lazy=True は、遅延ロードを意味し、関連データを必要になった時に取得する設定です(パフォーマンスの最適化)。
商品モデルクラスの修正部分の解説
追加されたプログラム
# カテゴリID
category_id = db.Column(db.Integer, db.ForeignKey("category.id"), nullable=True)
category_id = db.Column(…)
データベースのカラム(列)を定義しています。このカラムは category_id という名前の整数型のカラムになります。Product モデル(商品テーブル)に、このカラムが追加されることを想定しています。
db.Integer
category_id は、整数型(Integer)で保存されます。カテゴリーのID(id)を参照するためのカラムなので、整数型になっています。
db.ForeignKey(“category.id”)
外部キー(ForeignKey)を設定しています。category.id は、Category モデル(カテゴリーテーブル)の idカラムを指します。つまり、この category_id は Category テーブルの id を参照するという意味です。この設定により、Product テーブルは Category テーブルとリレーション(関連付け)ができるようになります。
nullable=True
nullable=True は、このカラムが NULL(空)でもOK という意味です。商品にカテゴリーが必ず必要とは限らない場合、この設定が使われます。カテゴリーが未設定の商品があってもエラーにならないようにできます。もし nullable=False にすると、category_id に必ず値を入れないといけなくなります。
カテゴリーフォームクラスの定義と商品フォームクラスの修正(forms.py)
forms.pyの編集後の内容です。
from flask_wtf import FlaskForm
from wtforms import IntegerField, SelectField, StringField, SubmitField
from wtforms.validators import DataRequired, NumberRange
from apps.productsapp.models import Category
class CategoryForm(FlaskForm):
name = StringField("カテゴリー名", validators=[DataRequired()])
submit = SubmitField("カテゴリーを追加する")
class AddProductForm(FlaskForm):
product_name = StringField("商品名", validators=[DataRequired()])
product_price = IntegerField(
"価格",
validators=[
DataRequired(),
NumberRange(min=1, message="価格は1以上である必要があります"),
],
)
category = SelectField("カテゴリー", coerce=int) # カテゴリー選択フィールド
def set_category_choices(self):
self.category.choices = [
(c.id, c.name) for c in Category.query.all()
] # DBから取得
インポートされたライブラリ等の確認
FlaskForm:Flask-WTF の基本フォームクラス。これを継承してフォームを作成する。
IntegerField:数値(整数)を入力するフィールド。
SelectField:プルダウン(セレクトボックス)のフィールド。
StringField:テキスト入力フィールド。
SubmitField:送信ボタン。
DataRequired():必須入力のバリデーション(入力が空の場合エラー)。
NumberRange(min=1):数値の範囲を制限するバリデーション(最低 1 以上)。
Category:Category モデルをデータベースから取得するためにインポート。
追加されたインポート部分
from wtforms import IntegerField, SelectField, StringField, SubmitFieldの行でSubmitFieldが追加されています。更に、カテゴリーモデルクラスをインポートしています。
from wtforms import IntegerField, SelectField, StringField, SubmitField
from apps.productsapp.models import Category
CategoryFormクラスの作成
class CategoryForm(FlaskForm):
name = StringField("カテゴリー名", validators=[DataRequired()])
SubmitField("カテゴリーを追加する")
StringField()やSubmitField()の第一引数ではテンプレートに表示する文字列を指定しています。SubmitField(“カテゴリーを追加する”)ではテンプレートに表示されるボタン上に「カテゴリーを追加する」と表示することになります。
AddProductFormクラスの修正部分(1)
category = SelectField("カテゴリー", coerce=int) # カテゴリー選択フィールド
SelectField()を利用してプルダウンを利用するカテゴリー選択をテンプレートで行うことができます。
AddProductFormクラスの修正部分(2)
def set_category_choices(self):
self.category.choices = [(c.id, c.name) for c in Category.query.all()] # DBから取得
Category.query.all() を使ってデータベースのカテゴリー一覧を取得します。
self.category.choices に (カテゴリーID, カテゴリー名) のリストを設定しています。
※category属性にchoicesを利用してリストをセットすると SelectField() でプルダウンメニューが表示されることになります。
app.pyの修正
app.pyの編集後の内容です。
from itertools import groupby
from flask import Flask, flash, redirect, render_template, url_for
from apps.productsapp.forms import AddProductForm, CategoryForm
from apps.productsapp.models import Category, Product, db # models.pyからインポート
# アプリケーションの初期化
app = Flask(__name__)
app.secret_key = "your_secret_key"
# SQLiteの設定
app.config["SQLALCHEMY_DATABASE_URI"] = (
"sqlite:///products.db" # SQLiteデータベースファイル
)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False # 不要な変更追跡を無効化
# SQLAlchemyのインスタンスを初期化
db.init_app(app)
# アプリケーションコンテキスト内でテーブル作成
with app.app_context():
db.create_all()
# ルート: トップページ
@app.route("/")
def index():
return "Top page"
# ルート: カテゴリー一覧を表示
@app.route("/categories")
def category_list():
categories = Category.query.all()
return render_template("categories.html", categories=categories)
# ルート: カテゴリーを追加
@app.route("/category/add", methods=["GET", "POST"])
def add_category():
form = CategoryForm()
if form.validate_on_submit():
new_category = Category(name=form.name.data)
db.session.add(new_category)
db.session.commit()
flash("カテゴリーを追加しました", "success")
return redirect(url_for("category_list"))
return render_template(
"category_form.html", form=form, title="カテゴリー追加フォーム"
)
# ルート: カテゴリーを編集
@app.route("/category/edit/<int:category_id>", methods=["GET", "POST"])
def edit_category(category_id):
category = Category.query.get_or_404(category_id)
form = CategoryForm(obj=category)
if form.validate_on_submit():
category.name = form.name.data
db.session.commit()
flash("カテゴリーを更新しました", "success")
return redirect(url_for("category_list"))
return render_template("category_form.html", form=form, title="カテゴリー編集")
# ルート: カテゴリーを削除
@app.route("/category/delete/<int:category_id>", methods=["POST"])
def delete_category(category_id):
category = Category.query.get_or_404(category_id)
# もしカテゴリーに紐づいた商品があればエラーを出す
if category.products:
flash("このカテゴリーには商品が登録されているため削除できません", "danger")
return redirect(url_for("category_list"))
db.session.delete(category)
db.session.commit()
flash("カテゴリーを削除しました", "success")
return redirect(url_for("category_list"))
# ルート: 商品一覧ページ
@app.route("/products")
def product_list():
products = Product.query.all() # SQLiteから商品データを取得
# 商品をカテゴリーごとにグループ化
grouped_products = {}
for key, group in groupby(
sorted(products, key=lambda p: p.category.name if p.category else "なし"),
key=lambda p: p.category.name if p.category else "なし",
):
grouped_products[key] = list(group)
return render_template("products.html", grouped_products=grouped_products)
# ルート: 商品追加ページ
@app.route("/add_product", methods=["GET", "POST"])
def add_product():
form = AddProductForm()
form.set_category_choices() # カテゴリーリストを設定
if form.validate_on_submit():
# フォームが送信され、バリデーションが成功した場合
name = form.product_name.data
price = form.product_price.data
category_id = form.category.data
# 新しい商品をデータベースに追加
new_product = Product(name=name, price=price, category_id=category_id)
db.session.add(new_product) # セッションに追加
db.session.commit() # データベースにコミット
# 商品一覧ページにリダイレクト
return redirect(url_for("product_list"))
return render_template("add_product.html", form=form)
# ルート: 商品管理ページ
@app.route("/manage_products")
def manage_products():
return render_template("manage_products.html")
if __name__ == "__main__":
app.run(debug=True)
修正内容の解説
groupbyのインポート
from itertools import groupby
groupby は Python の itertools モジュールからインポートされる関数で、データをキーごとにグループ化するために使用されます。例えば、商品をカテゴリーごとにグループ化するときに利用できます。
flashのインポート
from flask import Flask, flash, redirect, render_template, url_for
Flask:アプリケーションのインスタンスを作成する際に使用します。
flash:ユーザーにフォーム送信成功などメッセージを一時的に表示する仕組み。
redirect:特定のURLへリダイレクトします。
render_template:HTMLテンプレートを表示します。
url_for:ルート名を元にURLを生成します。
CategoryFormのインポート/Categoryモデルの作成
from apps.productsapp.forms import AddProductForm, CategoryForm
from apps.productsapp.models import Category, Product, db # models.pyからインポート
forms.pyから AddProductFormクラス と CategoryFormクラス、models.pyから Productクラス と Categoryクラス、dbインスタンス をインポートしています。
カテゴリー一覧を表示するルートを作成
# ルート: カテゴリー一覧を表示
@app.route("/categories")
def category_list():
categories = Category.query.all()
return render_template("categories.html", categories=categories)
/categories にアクセスすると、データベースからカテゴリー一覧を取得します。
categories.html にカテゴリーのリストを渡し、HTMLに表示します。
カテゴリーの追加ページを表示するルートを作成
# ルート: カテゴリーを追加
@app.route("/category/add", methods=["GET", "POST"])
def add_category():
form = CategoryForm()
if form.validate_on_submit():
new_category = Category(name=form.name.data)
db.session.add(new_category)
db.session.commit()
flash("カテゴリーを追加しました", "success")
return redirect(url_for("category_list"))
return render_template(
"category_form.html", form=form, title="カテゴリー追加フォーム"
)
form = CategoryForm()
- CategoryForm(Flask-WTFのフォームクラス)を作成します。
- CategoryForm には name フィールド(カテゴリー名)と submit ボタンがあります。
if form.validate_on_submit():
- validate_on_submit() は、フォームが送信され、かつバリデーション(必須入力など)が通ったときに True になります。
- POST メソッドでフォームが送信された場合のみ、以下の処理を実行します。
new_category = Category(name=form.name.data)
- フォームの nameフィールドからデータを取得し、新しくCategoryインスタンスを作成します。
db.session.add(new_category)
db.session.commit()
- db.session.add(new_category) でデータベースに追加します。
- db.session.commit() で変更を確定(コミット)します。
flash(“カテゴリーを追加しました”, “success”)
- flash() は、一時的なメッセージを表示するためのFlaskの機能です。
- “カテゴリーを追加しました” というメッセージを “success” タイプ(成功メッセージ)として保存します。
return redirect(url_for(“category_list”))
- 新しいカテゴリーを追加した後、category_list ルート(/categories)へリダイレクトします。
return render_template(“category_form.html”, form=form, title=”カテゴリー追加フォーム”)
- GET メソッドのとき(初回アクセス時)や、バリデーションに失敗したときに フォームを表示します。
- “category_form.html” というテンプレートに form と title を渡します。
カテゴリーの編集ページを表示するルートを作成
# ルート: カテゴリーを編集
@app.route("/category/edit/<int:category_id>", methods=["GET", "POST"])
def edit_category(category_id):
category = Category.query.get_or_404(category_id)
form = CategoryForm(obj=category)
if form.validate_on_submit():
category.name = form.name.data
db.session.commit()
flash("カテゴリーを更新しました", "success")
return redirect(url_for("category_list"))
return render_template("category_form.html", form=form, title="カテゴリー編集")
@app.route(“/category/edit/”, methods=[“GET”, “POST”])
- “/category/edit/” というURLでアクセスできます。
- により、編集するカテゴリーのIDをURLの一部として受け取ります。
- methods=[“GET”, “POST”] により、フォームの表示(GET)と送信(POST) の両方を処理します。
Category.query.get_or_404(category_id)
- IDが category_id のカテゴリーをデータベースから取得します。
- 見つからない場合は 404エラーを返します(ページが見つかりません)。
form = CategoryForm(obj=category)
- CategoryForm のインスタンスを作成します。
- obj=category を指定することで、フォームの初期値に取得した category のデータをセットします。
return render_template(“category_form.html”, form=form, title=”カテゴリー編集”)
テンプレートで{{title}}を利用してブラウザのタグ名やテンプレート内の表示に利用できます。
カテゴリーの削除を行うルートを作成
# ルート: カテゴリーを削除
@app.route("/category/delete/<int:category_id>", methods=["POST"])
def delete_category(category_id):
category = Category.query.get_or_404(category_id)
# もしカテゴリーに紐づいた商品があればエラーを出す
if category.products:
flash("このカテゴリーには商品が登録されているため削除できません", "danger")
return redirect(url_for("category_list"))
db.session.delete(category)
db.session.commit()
flash("カテゴリーを削除しました", "success")
return redirect(url_for("category_list"))
商品一覧ページの修正
# ルート: 商品一覧ページ
@app.route("/products")
def product_list():
products = Product.query.all() # SQLiteから商品データを取得
# 商品をカテゴリーごとにグループ化
grouped_products = {}
for key, group in groupby(
sorted(products, key=lambda p: p.category.name if p.category else "なし"),
key=lambda p: p.category.name if p.category else "なし",
):
grouped_products[key] = list(group)
return render_template("products.html", grouped_products=grouped_products)
groupby と sorted を使って、商品をカテゴリーごとにグループ化します。
sorted(products, key=lambda p: p.category.name if p.category else “なし”)
products を カテゴリー名(p.category.name)でソートします。category が None(カテゴリなし)場合には、 “なし” をカテゴリー名として扱います。
groupby(…, key=lambda p: p.category.name if p.category else “なし”)
ソートされた商品を、カテゴリー名でグループ化します。key引数により、グループ化の基準が指定されます。ここではカテゴリー名または “なし” です。
grouped_products[key] = list(group)
グループ化された商品を grouped_products 辞書に追加します。key はカテゴリー名、group はそのカテゴリーに所属する商品のリストです。結果として、grouped_products はカテゴリーごとに商品が整理された辞書になります。
商品追加ページの修正
# ルート: 商品追加ページ
@app.route("/add_product", methods=["GET", "POST"])
def add_product():
form = AddProductForm()
form.set_category_choices() # カテゴリーリストを設定
if form.validate_on_submit():
# フォームが送信され、バリデーションが成功した場合
name = form.product_name.data
price = form.product_price.data
category_id = form.category.data
# 新しい商品をデータベースに追加
new_product = Product(name=name, price=price, category_id=category_id)
db.session.add(new_product) # セッションに追加
db.session.commit() # データベースにコミット
# 商品一覧ページにリダイレクト
return redirect(url_for("product_list"))
return render_template("add_product.html", form=form)
カテゴリー属性部分の追加を行ってます。(下3つ)
form.set_category_choices() # カテゴリーリストを設定
category_id = form.category.data
new_product = Product(name=name, price=price, category_id=category_id)
今回は以上になります。

ブックマークのすすめ
「ほわほわぶろぐ」を常に検索するのが面倒だという方はブックマークをお勧めします。ブックマークの設定は別記事にて掲載しています。

