11-Flask

Flask(Part.11)| 【ふたつのテーブルの利用(1)実装と実行】

python| まとめ | 現役エンジニア&プログラミングスクール講師「python」のまとめページです。pythonに関して抑えておきたい知識や文法やにについて記事をまとめています。まとめページの下部には「おすすめの学習書籍」「おすすめのITスクール情報」「おすすめ求人サイト」について情報を掲載中...

目標

  • リレーショナルデータベースでふたつのテーブルを利用する方法を理解する。

Flaskでデータベースを利用する方法

アプリケーションの修正ポイントを確認

Part.9までで商品テーブルを利用するアプリケーションを作成してきましたが、商品を追加し続けると、管理が煩雑になり、必要な商品をすぐに見つけにくくなる問題が発生します。
例えば、メニューの確認時に『りんごジュース』などの飲み物と『パスタ』などの食べ物が順不同に並び、商品リストの中から目的のものを探すのが大変になります。

これを回避するために、「ソフトドリンク」や「アルコール」、「サラダ」、「ピザ」、「パスタ」などのカテゴリーを格納する カテゴリーテーブル を作成し、商品の追加時にこのテーブルを参照してカテゴリーを選択できるように修正します。

商品テーブル内に直接カテゴリーを手入力する方法は採用しません。 これは、データの重複や管理の手間を減らすために、データベースの 正規化 を行う必要があるためです(例:カテゴリー名を統一し、一元管理することで、誤入力や冗長なデータを防ぐ)。

データベースや正規化については、こちらの記事を参考にしてください。

データベース(Part.5)| 正規化 | 現役エンジニア&プログラミングスクール講師「正規化」について記事にしています。正規化の概要、正規形の種類、重要語、第1正規形、第2正規形、第3正規形、の作業手順について解説しています。...

本来であれば、アプリケーションを作成する前に データベース設計やプログラム設計 を行うのが理想ですが、本記事では学習のため、開発しながら必要な修正を加えていきます。

アプリケーションのプログラム追加と修正

カテゴリーモデルクラスの定義と商品モデルクラスの修正(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ファイルを削除してサーバーを再起動します。

サーバーを起動している場合はターミナルで「Ctrl + C」を押下し、サーバーを停止してから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」を選択すると削除されます。

削除後。

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

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

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

今回は以上になります。

「python」おすすめ書籍 ベスト3 | 現役エンジニア&プログラミングスクール講師「python」の学習でお勧めしたい書籍をご紹介しています。お勧めする理由としては、考え方、イメージなどを適切に捉えていること、「生のpython」に焦点をあてて解説をしている書籍であることなどが理由です。勿論、この他にも良い書籍はありますが、特に質の高かったものを選んで記事にしています。ページの下部には「おすすめのITスクール情報」「おすすめ求人サイト」について情報を掲載中。...

ブックマークのすすめ

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

「お気に入り」の登録・削除方法【Google Chrome / Microsoft Edge】「お気に入り」の登録・削除方法【Google Chrome / Microsoft Edge】について解説している記事です。削除方法も掲載しています。...
【パソコン選び】失敗しないための重要ポイント | 現役エンジニア&プログラミングスクール講師【パソコン選び】失敗しないための重要ポイントについての記事です。パソコンのタイプと購入時に検討すべき点・家電量販店で見かけるCPUの見方・購入者が必要とするメモリ容量・HDDとSSDについて・ディスプレイの種類・バッテリーの持ち時間や保証・Officeソフト・ウィルス対策ソフトについて書いています。...
RELATED POST
11-Flask

Flask(Part.2)| 【MVTの概要とアプリケーションの起動方法 】

2025年2月13日
プログラミング学習 おすすめ書籍情報発信 パソコン初心者 エンジニア希望者 新人エンジニア IT業界への就職・転職希望者 サポートサイト Programming learning Recommended schools Recommended books Information dissemination Computer beginners Prospective engineers New engineers Prospective job seekers in the IT industry Support site
11-Flask

Flask(Part.9)| 【Flaskでデータベースを利用する方法(2)解説】

2025年3月2日
プログラミング学習 おすすめ書籍情報発信 パソコン初心者 エンジニア希望者 新人エンジニア IT業界への就職・転職希望者 サポートサイト Programming learning Recommended schools Recommended books Information dissemination Computer beginners Prospective engineers New engineers Prospective job seekers in the IT industry Support site