11-Flask

Flask(Part.12)| 【ふたつのテーブルの利用(2)ロジック部分の解説】

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

目標

  • 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)

今回は以上になります。

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

ブックマークのすすめ

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

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

Flask(Part.18)| 【画像のアップロードと利用(実装編)】

2025年3月11日
プログラミング学習 おすすめ書籍情報発信 パソコン初心者 エンジニア希望者 新人エンジニア 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.38)| 【商品管理アプリケーションの修正(3)】

2025年3月31日
プログラミング学習 おすすめ書籍情報発信 パソコン初心者 エンジニア希望者 新人エンジニア 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.45)| 【経費管理アプリケーションのプログラミング(1)】

2025年4月7日
プログラミング学習 おすすめ書籍情報発信 パソコン初心者 エンジニア希望者 新人エンジニア 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.48)| 【経費管理アプリケーションへの追加プログラミング】

2025年4月10日
プログラミング学習 おすすめ書籍情報発信 パソコン初心者 エンジニア希望者 新人エンジニア 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.41)| 【レジ管理アプリケーションのプログラミング(3)】

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