
目標
- リレーショナルデータベースでふたつのテーブルを利用する方法を理解する。
Flaskでデータベースを利用する方法
アプリケーションの修正ポイントを確認
Part.9までで商品テーブルを利用するアプリケーションを作成してきましたが、商品を追加し続けると、管理が煩雑になり、必要な商品をすぐに見つけにくくなる問題が発生します。
例えば、メニューの確認時に『りんごジュース』などの飲み物と『パスタ』などの食べ物が順不同に並び、商品リストの中から目的のものを探すのが大変になります。
これを回避するために、「ソフトドリンク」や「アルコール」、「サラダ」、「ピザ」、「パスタ」などのカテゴリーを格納する カテゴリーテーブル を作成し、商品の追加時にこのテーブルを参照してカテゴリーを選択できるように修正します。
データベースや正規化については、こちらの記事を参考にしてください。


アプリケーションのプログラム追加と修正
カテゴリーモデルクラスの定義と商品モデルクラスの修正(models.py)
models.pyファイルの編集
models.pyファイルにカテゴリーモデルクラスを作成して、商品モデルクラスにカテゴリーモデルクラスのIDを外部キーとして参照します。
models.pyファイル に次のプログラムを入力します。
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}>"
カテゴリーフォームクラスの定義と商品フォームクラスの修正(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から取得
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)
categorys.html(カテゴリー一覧ページ)の作成
categorys.htmlの内容。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>カテゴリー一覧</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<h1>カテゴリー一覧ページ</h1>
<ul>
<li><a href="{{ url_for('manage_products') }}" class="button-link">商品管理ページ</a></li>
<li><a href="{{ url_for('add_category') }}" class="button-link">カテゴリーを追加</a></li>
</ul>
<h1>カテゴリー一覧</h1>
<ul>
{% for category in categories %}
<li class="category-item">
<span class="category-name">{{ category.name }}</span>
<div class="button-group">
<form action="{{ url_for('edit_category', category_id=category.id) }}" method="GET">
<button type="submit" class="edit-button">編集</button>
</form>
<form action="{{ url_for('delete_category', category_id=category.id) }}" method="POST">
<button type="submit" class="delete-button"
onclick="return confirm('本当に削除しますか?※既に商品が存在する場合は削除できません。')">削除</button>
</form>
</div>
</li>
{% endfor %}
</ul>
</body>
</html>
category_form.html(カテゴリー追加・編集ページ)の作成
category_form.htmlの内容。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<h1>カテゴリー追加ページ</h1>
<ul>
<li><a href="{{ url_for('manage_products') }}" class="button-link">商品管理ページ</a></li>
<li><a href="{{ url_for('category_list') }}" class="button-link">カテゴリー一覧ページ</a></li>
</ul>
<h1>{{ title }}</h1>
<form method="POST">
{{ form.hidden_tag() }}
<label for="name">カテゴリー名</label>
{{ form.name() }}
{% for error in form.name.errors %}
<p style="color: red;">{{ error }}</p>
{% endfor %}
{{ form.submit(class="button-link") }}
</form>
</body>
</html>
add_product.htmlの修正
add_product.htmlの修正後の内容。フォーム内でカテゴリー項目の追加を行っています。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品追加</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<h1>商品追加ページ</h1>
<ul>
<li><a href="{{ url_for('product_list') }}" class="button-link">商品一覧に戻る</a></li>
<li><a href="{{ url_for('manage_products') }}" class="button-link">商品管理ページ</a></li>
</ul>
<h1>商品追加フォーム</h1>
<form method="POST" action="{{ url_for('add_product') }}">
{{ form.hidden_tag() }} <!-- CSRFトークンを含めるため -->
<label for="product_name">商品名</label>
{{ form.product_name() }} <!-- 商品名入力フィールド -->
{% if form.product_name.errors %}
<ul>
{% for error in form.product_name.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<label for="product_price">価格</label>
{{ form.product_price() }} <!-- 価格入力フィールド -->
{% if form.product_price.errors %}
<ul>
{% for error in form.product_price.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<label for="category">カテゴリー</label>
{{ form.category() }}
{% if form.category.errors %}
<ul>
{% for error in form.category.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<input type="submit" value="商品を追加する">
</form>
</body>
</html>
products.htmlの修正
products.htmlの修正後の内容。商品をカテゴリーごとに表示するための修正を行っています。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品一覧</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<h1>商品一覧ページ</h1>
<ul>
<li><a href="{{ url_for('add_product') }}" class="button-link">商品を追加する</a></li>
<li><a href="{{ url_for('manage_products') }}" class="button-link">商品管理ページ</a></li>
</ul>
<h1>商品一覧</h1>
{% for category, products in grouped_products.items() %}
<h2>カテゴリー: {{ category }}</h2>
<ul class="product-list">
{% for product in products %}
<li>{{ product.name }} : ¥{{ "{:,}".format(product.price) }}</li>
{% endfor %}
</ul>
{% endfor %}
</body>
</html>
manage_products.htmlの修正
manage_products.htmlの修正後の内容。カテゴリー一覧とカテゴリー追加のページリンクを追加しています。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品管理</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<h1>商品管理ページ</h1>
<ul>
<li><a href="{{ url_for('product_list') }}" class="button-link">商品一覧</a></li>
<li><a href="{{ url_for('add_product') }}" class="button-link">商品追加</a></li>
<li><a href="{{ url_for('category_list') }}" class="button-link">カテゴリ一覧</a></li>
<li><a href="{{ url_for('add_category') }}" class="button-link">カテゴリー追加</a></li>
</ul>
</body>
</html>
スタイルシートの修正と追加
style.css
body {
font-family: Arial, sans-serif;
margin: 20px;
padding: 20px;
background-color: #f8f8f8;
}
h1 {
color: #333;
}
/* ボタン部分のリストを横並びにする */
ul {
padding: 0;
}
ul:first-of-type {
display: flex;
/* 横並び */
justify-content: flex-start;
/* 左寄せ */
}
ul:first-of-type li {
margin-right: 10px;
/* ボタン間に余白 */
}
/* リストアイテムの基本スタイル */
li {
list-style-type: none;
/* デフォルトのリストスタイルを削除 */
}
/* カテゴリーごとの商品リストのスタイル */
ul.product-list {
list-style-type: none;
padding: 0;
}
ul.product-list li {
background-color: #fff;
padding: 10px;
margin: 5px 0;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* ボタンリンクのスタイル */
.button-link {
display: block;
padding: 10px 20px;
background-color: #4CAF50;
color: white;
text-decoration: none;
border-radius: 5px;
font-size: 16px;
transition: background-color 0.3s ease;
}
/* ボタンにホバーしたときのスタイル */
.button-link:hover {
background-color: #45a049;
}
/* 商品リストのスタイル */
h2 {
margin-top: 30px;
/* 商品リストの見出しに余白 */
}
ul:nth-of-type(2) {
list-style-type: none;
/* 商品リストのマーカーを削除 */
padding: 0;
}
ul:nth-of-type(2) li {
background-color: #fff;
padding: 10px;
margin: 5px 0;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* フォーム全体のスタイル */
form {
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-top: 30px;
}
/* フォーム内のラベルのスタイル */
form label {
font-size: 16px;
color: #333;
display: block;
margin-bottom: 8px;
}
/* フォーム内の入力フィールドのスタイル */
form input[type="text"],
form input[type="number"],
form input[type="submit"] {
width: 100%;
padding: 10px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
box-sizing: border-box;
/* パディングを含めてサイズ調整 */
}
/* フォーム内のボタン(送信ボタン) */
form input[type="submit"] {
background-color: #4CAF50;
color: white;
cursor: pointer;
transition: background-color 0.3s ease;
}
/* 送信ボタンにホバーしたときのスタイル */
form input[type="submit"]:hover,
form button:hover {
background-color: #45a049;
}
/* カテゴリーの選択ボックス(select) */
form select {
width: 100%;
padding: 10px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
box-sizing: border-box;
transition: border-color 0.3s ease;
}
/* フォーカス時のスタイル */
form select:focus {
border-color: #4CAF50;
outline: none;
box-shadow: 0 0 5px rgba(76, 175, 80, 0.5);
}
/* リストアイテムを横並びにする */
.category-item {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
padding: 10px;
margin: 5px 0;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* カテゴリー名のスタイル */
.category-name {
font-size: 16px;
flex-grow: 1;
/* 左側に余白を作り、ボタンを右寄せ */
}
/* ボタンを右側に配置 */
.button-group {
display: flex;
gap: 10px;
/* ボタン間のスペース */
}
/* ボタンを囲む form のスタイルをリセット */
.button-group form {
display: inline-block;
margin: 0;
padding: 0;
}
/* 編集・削除ボタンの共通スタイル */
.button-group button {
display: inline-block;
padding: 8px 16px;
font-size: 14px;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
color: white;
width: 80px;
/* 幅を統一 */
text-align: center;
}
/* 編集ボタンのスタイル */
.button-group .edit-button {
background-color: #2196F3;
/* ブルー */
}
.button-group .edit-button:hover {
background-color: #1976D2;
/* ホバー時の色 */
}
/* 削除ボタンのスタイル */
.button-group .delete-button {
background-color: #F44336;
/* レッド */
}
.button-group .delete-button:hover {
background-color: #D32F2F;
/* ホバー時の色 */
}
古いデータベースの削除とアプリケーションの稼働確認
instanceディレクトリの中に、products.dbファイルを削除してサーバーを再起動します。
instanceディレクトリの中に、products.dbファイルを右クリックして、「Delete」をクリックします。

ブラウザで http://127.0.0.1:5000/manage_productsにアクセスします。
商品管理ページが表示されます。「カテゴリー一覧」と「カテゴリー追加」ボタンが追加されています。

カテゴリー一覧ボタンを押下すると、カテゴリー一覧ページが表示されます。始めて開いた状態ではカテゴリー一覧には何もありません。

カテゴリーを追加するボタンを押下すると、カテゴリー追加ページが表示されます。

カテゴリー名に「アルコール」と入力して「カテゴリーを追加する」ボタンを押下します。

http://127.0.0.1:5000/categories にリダイレクトされます。

編集ボタンをクリックするとhttp://127.0.0.1:5000/category/edit/1?に移動します。
入力フォームはカテゴリー追加のページと同じです。

「アルコール」を「ソフトドリンク」に修正して「カテゴリーを追加する」をクリックします。

http://127.0.0.1:5000/categories にリダイレクトされ、「アルコール」が「ソフトドリンク」に変更されているのが確認できます。

削除ボタンをクリックすると、警告が表示され「OK」を選択すると削除されます。

削除後。

適当にカテゴリーを追加しておきます。

「商品管理ページ」をクリックし、商品管理ページに戻り「商品追加」をクリックします。

「ソフトドリンク」のカテゴリーに「りんごジュース」や「オレンジジュース」、「グレープジュース」を追加します。その他、「アルコール」のカテゴリーに「ビール」、「酎ハイ」を追加し、「パスタ」のカテゴリーに「カルボナーラ」「ナポリタン」「明太子パスタ」を追加し、「ピザ」のカテゴリーに「マルゲリータ」、「デザート」のカテゴリーに「チーズケーキ」「プリン」を追加します。

今回は以上になります。

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

