PythonによるWebアプリケーション開発(Flask編)

いよいよ今日から、プログラミング言語Pythonと、その強力な相棒である「Flask(フラスク)」を使って、本格的なショッピングサイト作りに挑戦していきます!

「プログラミングでWebサイトを作るって、何だか難しそう…」

「ショッピングサイトなんて、大企業じゃないと作れないんじゃ?」

そんな風に思っている方も、心配ご無用です。

この研修テキストは、プログラミング初心者の方を対象に、一つひとつのステップを丁寧に解説していきます。一緒に学んでいきましょう!

第1章 はじめに:FlaskとEコマースの世界へようこそ

1-1. こんにちは、Flask! なぜ今Flaskを選ぶのか?

まずは、今回私たちが使うメインツール、「Flask」について紹介させてください。

Flaskとは、PythonでWebアプリケーション(WebサイトやWebサービスのことです)を作るための「フレームワーク」の一種です。

ここで早速、「フレームワーク」という専門用語が出てきましたね。

大丈夫、難しくありませんよ。

フレームワークを例えるなら、「料理キット」のようなものです。

カレーを作るとき、スパイスを一から調合し、野菜をすべて切り、ルーも自分で作るのは大変ですよね?

「料理キット」には、必要な材料やルー、そして「作り方の手順書」がセットになっています。私たちは手順書に従って調理するだけで、美味しいカレーが作れます。

Web開発におけるフレームワークも同じです。

ログイン機能、データベースとの連携、URLの管理など、Webサイトに必要な基本的な部品や骨組みをあらかじめ提供してくれます。私たちは、それらを組み合わせて、本当に作りたい「独自の部分」(例えば、商品のデザインやカートの仕組み)に集中できるのです。

Flaskは、そのフレームワークの中でも「マイクロフレームワーク」と呼ばれています。

「マイクロ」と聞くと、機能が少ないように感じるかもしれません。

これはデメリットではなく、実は強力なメリットなのです!

Flaskは、「最小限の必須機能」だけを提供します。

例えるなら、「最高品質の塩とコショウだけがセットになった料理キット」です。

「カレーが食べたい日もあれば、パスタが食べたい日もある」というシェフ(開発者)にとって、最初からカレールーが固定されているキットは不便ですよね。

Flaskは基本がシンプルな分、とても軽量で、動作が速いのが特徴です。

そして、「こういう機能が欲しい」と思ったら、「拡張機能」と呼ばれる追加パーツを自分で選んで導入できます。例えば、「ログイン機能専門のパーツ」や「データベース操作専門のパーツ」といった具合です。

この「自分で選んで組み立てる自由度の高さ」と「学習のしやすさ」こそが、私たちが今回Flaskを選ぶ最大の理由です!

1-2. Eコマース(ショッピングサイト)ってどう動いてるの?

次に、私たちが作る「ショッピングサイト(Eコマースサイト)」が、裏側でどう動いているのか、その全体像を掴んでおきましょう。

皆さんが普段ネットショッピングをするとき、何気なく見ている画面には、大きく分けて2つの側面があります。

  1. フロントエンド(お店の表側:お客様が見る場所)
  2. バックエンド(お店の裏側:店員さんやシステムが動く場所)

「フロントエンド」は、ブラウザに表示されるデザインやボタン、商品の写真など、お客様の目に見える部分です。例えるなら、お店の「陳列棚」や「内装」ですね。

一方、「バックエンド」は、お客様の目には見えない裏側の処理を担当します。

Flaskは、主にこのバックエンドを作るためのフレームワークです。

お店の裏側では、たくさんの仕事がありますよね。

  • データベース(在庫台帳)
    • 商品情報(名前、価格、在庫数)や、お客様の情報(名前、住所、パスワード)が記録されている「巨大な台帳」です。
  • ユーザー管理(会員証の発行・確認)
    • お客様が「会員登録」したり、「ログイン」したりする処理です。「この人は本当に会員様か?」を確認します。
  • 商品カタログ(商品の陳列)
    • 「データベース」から商品情報を取り出して、見やすい形に整え、「フロントエンド」(陳列棚)に並べる仕事です。
  • カート(買い物かご)
    • お客様が「これを買う!」と選んだ商品を、一時的に覚えておく機能です。
  • 決済(レジ対応)
    • お客様が「購入確定」ボタンを押したとき、クレジットカード会社と通信して「ちゃんとお支払いできるか」を確認し、売上を記録する、最も重要な処理です。

Flaskの役割は、これらバックエンドの複雑な仕事を、Pythonのプログラムで正確に実行することなのです。

1-3. 研修のゴール:私たちがこれから作るサイトの完成図

この研修では、最終的に以下の機能を持ったショッピングサイトをゼロから構築することを目指します。

  • 商品の一覧表示と詳細表示
  • ユーザーの新規登録とログイン、ログアウト機能
  • 商品をカートに追加・削除する機能
  • (簡単な)決済機能と注文の確定
  • 管理者用の商品登録・編集ページ

もちろん、最初からすべてを作るわけではありません。

一つひとつの機能を、章ごとに順番に組み立てていきますので、安心してくださいね。

1-4. 開発環境を整えよう

さあ、いよいよ手を動かす準備です!

料理を始める前に、まずはキッチン(開発環境)を整えましょう。

必要なものは以下の通りです。

  1. Python(パイソン)
    • 私たちのメイン言語です。まだインストールしていない方は、公式サイトから最新版をダウンロードしておきましょう。
  2. テキストエディタ
    • プログラムを書くためのメモ帳です。VSCode (Visual Studio Code) など、プログラミング用のものがおすすめです。
  3. ターミナル(またはコマンドプロンプト)
    • コンピュータに命令を出すための黒い画面です。

準備ができたら、まず「プロジェクト用のフォルダ」を好きな場所に作りましょう。

ここでは my_shop という名前にします。

次に、ターミナルを開いて、そのフォルダに移動してください。

そして、大事な作業、「仮想環境」の準備をします。

「仮想環境(venv)」とは、専門用語ですが、これも例えれば簡単です。

「プロジェクト専用の、汚してもいい作業部屋」だと思ってください。

もし、家のキッチン(PC本体)で、ある料理(プロジェクトA)のために特殊なスパイス(ライブラリA)を使い、別の料理(プロジェクトB)のために別のスパイス(ライブラリB)を使ったとします。

スパイスが混ざってしまうと、どちらの料理も味がおかしくなってしまいますよね?

「仮想環境」を作ると、my_shop プロジェクト専用のキッチンが用意され、そこで使った道具(Flaskなど)は、他のプロジェクトに一切影響を与えなくなります。

これは、プロの開発現場では必須のテクニックですよ!

ターミナルで以下のコマンドを実行します。

($マークは入力不要です)

# my_shop フォルダの中で実行します

# 1. 'venv' という名前の仮想環境(作業部屋)を作る
python -m venv venv

# 2. 作った仮想環境(作業部屋)に入る
# (Windowsの場合)
.\venv\Scripts\activate
# (Mac/Linuxの場合)
source venv/bin/activate

ターミナルの表示の先頭に (venv) と付けば、専用の作業部屋に入れた合図です。

それでは、この作業部屋にFlaskをインストールしましょう。

インストールには「pip(ピップ)」という、Pythonの便利な道具(パッケージ管理ツール)を使います。

# (venv) が表示されている状態で実行します
pip install Flask

これで、料理の道具(Flask)がキッチンに運ばれました!

1-5. 最初の第一歩:Hello, Flask! とプロジェクトの骨組み作り

環境が整ったので、最小限のプログラムでFlaskを動かしてみましょう。

my_shop フォルダの中に、 app.py という名前で新しいファイルを作ってください。

そして、以下のコードをそのまま書き写してみましょう。

from flask import Flask

# Flaskアプリのおまじない
app = Flask(__name__)

# サイトのトップページ('/')にアクセスが来たら
@app.route('/')
def hello():
    # 'Hello, Flask!' という文字を画面に表示する
    return 'Hello, Flask!'

# このファイルが直接実行されたらサーバーを起動する
if __name__ == '__main__':
    app.run(debug=True)

コードが書けたら、ターミナルで((venv) が付いていることを確認して)以下のコマンドを実行します。

python app.py

ターミナルに Running on http://127.0.0.1:5000/ のような文字が表示されたら成功です!

このURLをコピーして、Webブラウザ(ChromeやSafariなど)のアドレスバーに貼り付けてみてください。

画面に「Hello, Flask!」と表示されましたか?

おめでとうございます! これが、あなたがFlaskで作り上げた、世界で最初のWebページです!

ここで使った @app.route('/') という記述は、「ルーティング」と呼ばれるものです。

Webサイトの住所(URL)と、実行するPythonの関数(hello())を結びつける、「受付係」のような役割だと覚えておいてください。

最後に、今後のためにプロジェクトの「骨組み」(フォルダ構成)を整えておきましょう。

現時点では、my_shop フォルダの中はこうなっています。

my_shop/
├── venv/       (仮想環境の作業部屋)
└── app.py    (さっき作ったプログラム)

これを、今後の拡張に備えて、以下のように変更します。

(templates と static というフォルダを新しく作ります)

my_shop/
├── venv/
├── app.py
├── templates/  (HTMLファイル置き場)
└── static/     (画像やCSSファイル置き場)

templates は、Webページのデザイン図(HTML)を入れておく箱です。

static は、デザインを装飾するCSSファイルや、商品画像などを入れておく箱です。

第1章はここまでです。

まずはFlaskに触れ、Webサイトが動く仕組みの第一歩を踏み出しましたね。

次の第2章では、いよいよショッピングサイトの「商品」を扱うための、「データベース」という技術を学んでいきます。お楽しみに!

第2章 商品カタログの構築:データベースとの対話

2-1. データを保存する「データベース」とは?

皆さんは、ExcelやGoogleスプレッドシートを使ったことがありますか?

行と列がある表に、データを整理して入力していきますよね。

「データベース」(通称DB)は、そのスプレッドシートを、何百万倍も強力かつ高速にした「データ管理の専門家」だと思ってください。

ショッピングサイトでは、何千、何万という商品データや、すべてのお客様の会員情報、過去の注文履歴などを保存しておく必要があります。

これらをもしExcelファイルで管理していたら…。

「新しい注文が入るたびにファイルを開いて手入力」「在庫が減ったら手動で更新」…考えただけで大変ですし、同時にたくさんの人がアクセスしたらファイルが壊れてしまいそうですよね。

データベースは、大量のデータを安全に保管し、複数の人(やプログラム)が同時にアクセスしても、高速に「データを追加して!」「このデータを探して!」という要求に応えてくれる、非常に頼もしいシステムなのです。

今回私たちが使うのは、その中でも「リレーショナルデータベース(RDB)」という主流のタイプです。これは、Excelのように「テーブル(表)」を使ってデータを管理するのが特徴です。

2-2. Flask-SQLAlchemy入門:Pythonでデータベースを操る

データベースは非常に高性能ですが、その反面、操作するには「SQL(エスキューエル)」という専門の言語で命令を送る必要があります。

「えっ、Pythonの他に新しい言語も覚えるの?」

と心配になった方、ご安心ください。

ここで登場するのが、Flaskの強力な拡張機能、「Flask-SQLAlchemy(フラスク・エスキューエルアルケミー)」です。

これは「ORM(オーアールエム:Object-Relational Mapper)」と呼ばれる技術の一つです。

はい、また専門用語ですね!

ORMを例えるなら、「Python語しか話せない私たち」と「SQL語しか話せないデータベースさん」の間に立つ、「超優秀な通訳さん」です。

私たちがPythonのコードで「この商品を登録したい」と(オブジェクト操作で)書くと、ORMがそれをSQL語に翻訳して、データベースに伝えてくれます。

逆に、データベースから取り出したデータ(SQL語の結果)を、私たちが使いやすいPythonの形(オブジェクト)に翻訳し直して渡してくれるのです。

これのおかげで、私たちはSQL言語を(ほとんど)知らなくても、慣れ親しんだPythonの書き方だけでデータベースを操作できてしまいます!

さあ、まずはこの優秀な通訳さん(Flask-SQLAlchemy)を私たちのプロジェクトに招き入れましょう。

ターミナルで、仮想環境((venv) が表示されている状態)にして、以下のコマンドを実行します。

pip install Flask-SQLAlchemy

インストールが完了したら、app.py を開き、この通訳さんに働いてもらうための「設定」を書き加えます。

app.py を以下のように修正してください。

from flask import Flask
# 1. Flask-SQLAlchemyを読み込む
from flask_sqlalchemy import SQLAlchemy
import os

app = Flask(__name__)

# --- データベース設定 ---
# 実行中のファイルの絶対パスを取得
basedir = os.path.abspath(os.path.dirname(__file__))
# データベースファイルのパスを指定(my_shopフォルダにsite.dbという名前で作成)
app.config['SQLALCHEMY_DATABASE_URI'] = \
    'sqlite:///' + os.path.join(basedir, 'site.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# 2. Flaskアプリと連携させる
db = SQLAlchemy(app)
# ---------------------

@app.route('/')
def hello():
    return 'Hello, Flask!'

if __name__ == '__main__':
    app.run(debug=True)

少しコードが増えましたね。

ポイントは、app.config['SQLALCHEMY_DATABASE_URI'] の部分です。

ここでは、「データベースをどこに保存するか」を指定しています。

今回は練習用として、sqlite:/// という設定を使いました。

これは、「SQLite(エスキューエルライト)」という簡易的なデータベースを使う設定です。SQLiteは、Excelファイルのように「一つのファイル」としてデータベースを扱えるため、設定がとても簡単で、学習用に最適なんですよ。

2-3. 商品情報を定義する「モデル」の設計

通訳さんの準備ができたので、次はデータベースに「どんなデータを保存したいか」の設計図を渡しましょう。

この「データベースの設計図」のことを、Flask-SQLAlchemyでは「モデル」と呼びます。

私たちは「商品」を扱いたいので、「商品モデル(Product Model)」を作ります。

app.py の db = SQLAlchemy(app) のすぐ下に、以下のコードを追加してください。

# ... (db = SQLAlchemy(app) の下) ...

# 【モデルの定義】
# 商品情報を表す「Product」クラスを作成
class Product(db.Model):
    # 商品ID (整数型、これが主キー=重複しない番号)
    id = db.Column(db.Integer, primary_key=True)
    
    # 商品名 (文字列型、最大100文字、重複を許さない)
    name = db.Column(db.String(100), nullable=False, unique=True)
    
    # 価格 (整数型、必須)
    price = db.Column(db.Integer, nullable=False)
    
    # 商品説明 (テキスト型、空でもOK)
    description = db.Column(db.Text, nullable=True)

    # (おまけ) 管理画面などで見やすいように、商品名を表示する設定
    def __repr__(self):
        return f'<Product {self.name}>'

これは、Pythonの「クラス」という機能を使って、「Product(商品)というデータは、こういう部品(カラム)で構成されますよ」と定義しています。

  • db.Model を「継承(けいしょう)」する(設計図の書き方のお作法です)
  • id:商品を識別するためのユニークな番号(会員番号のようなもの)
  • name:商品名(文字列)
  • price:価格(整数)
  • description:商品説明(長い文章)

これが、データベース内に作られる「商品テーブル(Excelのシート)」の設計図になります。

さて、設計図ができました。

この設計図を元に、データベースさん(site.db ファイル)に「この設計図通りにテーブル(表)を作ってください!」とお願いする必要があります。

ターミナルで(python app.py が動いていたら Ctrl + C で一度停止し)、以下のコマンドを順番に実行してください。

# (venv) が表示されている状態で

# 1. Flaskの対話モードを開始
flask shell

>>> という記号が表示されたら、Pythonの対話モードに入った合図です。

続けて以下を入力します。

# >>> の後に続けて入力

# 2. app.py から db (通訳さん) と Product (設計図) を読み込む
from app import app, db, Product

# 3. アプリケーションのコンテキスト内で実行 (おまじない)
app.app_context().push()

# 4. 設計図(モデル)を元に、データベースファイルとテーブルを作成
db.create_all()

これを実行すると、my_shop フォルダの中に site.db というファイルが新しく作られたはずです!

これで、商品データを入れるための「空の表」がデータベース内に準備できました。

ついでに、テスト用の商品をいくつか登録しておきましょう。

続けて >>> の後に、以下を入力してみてください。

# テスト用商品データを作成
p1 = Product(name='すごいTシャツ', price=3000, description='これを着れば無敵です。')
p2 = Product(name='ふつうのキャップ', price=2500, description='日差しを遮ります。')
p3 = Product(name='丈夫なソックス', price=1200)

# データベースの「セッション(作業台)」に追加
db.session.add(p1)
db.session.add(p2)
db.session.add(p3)

# 変更をデータベースに「コミット(確定)」
db.session.commit()

これで、データベースに3つの商品が登録されました!

exit() と入力して、flask shell を終了しておきましょう。

2-4. 商品一覧ページを作ってみよう

データベースに商品が入りましたね!

いよいよ、この商品をWebページに表示させましょう。

まずは、第1章で作った templates フォルダが重要になります。

ここに、products.html という名前で、新しいHTMLファイルを作成してください。

templates/products.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>商品一覧</title>
</head>
<body>
    <h1>ようこそ!商品一覧ページへ</h1>

    </body>
</html>

次に、app.py を編集して、「/products というURLにアクセスが来たら、データベースから全商品を取り出して、products.html を表示する」という命令(ルーティング)を追加します。

app.pyhello() 関数の下(if __name__ == '__main__': の上)に、以下を追加してください。

# ... hello() 関数の下 ...

# 1. render_template を import リストに追加
from flask import Flask, render_template
# ... (他の import 文) ...

# ... (db, Product クラス定義) ...

@app.route('/')
def hello():
    return 'Hello, Flask!'

# ▼▼▼ ここから追加 ▼▼▼

# 商品一覧ページ
@app.route('/products')
def product_list():
    # 1. データベースから全ての商品を取得
    products = Product.query.all()
    
    # 2. products.html を表示し、商品データを渡す
    return render_template('products.html', products=products)

# ▲▲▲ ここまで追加 ▲▲▲


if __name__ == '__main__':
    # ...

重要なのは Product.query.all() の部分です。

これが、先ほどの「通訳さん(ORM)」の力です!

「Productモデル(商品テーブル)から、query(問い合わせて)、all(全部)持ってきて!」というPythonの命令が、裏側でSQL語に翻訳されてデータベースに伝達されます。

結果は products というリスト(Pythonのデータ形式)で返ってきます。

そして render_template('products.html', products=products) で、products.html を表示する際に、products という名前で、今取得した商品リストのデータをHTML側に渡しています。

さあ、これだけではまだ商品は表示されません。

HTML側でデータを受け取る準備をしましょう。

templates/products.html を以下のように修正します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>商品一覧</title>
</head>
<body>
    <h1>ようこそ!商品一覧ページへ</h1>

    <ul>
        {% for product in products %}
            <li>
                {{ product.name }} ({{ product.price }}円)
            </li>
        {% else %}
            <li>商品はまだありません。</li>
        {% endfor %}
    </ul>
    
</body>
</html>

{% ... %} や {{ ... }} という見慣れない記号が出てきましたね。

これが、Flaskが使っている「Jinja2(ジンジャツー)」というテンプレートエンジンの記法です。

テンプレートエンジンとは、「HTMLのひな形(テンプレート)に、後からデータを流し込むための仕組み」です。

  • {% for product in products %}
    • Pythonの for 文とそっくりですね! Python側から渡された products リストから、商品を1個ずつ取り出して product という変数に入れます。
  • {{ product.name }}
    • {{ ... }} は、変数の「中身を表示して」という意味です。productname 属性(商品名)を表示します。
  • {% endfor %}
    • for 文の終わりを示します。

ターミナルで python app.py を実行(または再起動)して、ブラウザで http://127.0.0.1:5000/products にアクセスしてみてください!

先ほど登録した「すごいTシャツ」たちがリストで表示されたら大成功です!

2-5. 商品詳細ページの作成

一覧が表示できたら、次は個別の商品ページです。

「すごいTシャツ」をクリックしたら、Tシャツの詳細説明ページに飛ぶようにしたいですよね。

ここで学ぶのが「動的ルーティング」です。

app.py に、さらにルーティングを追加します。

app.pyproduct_list() 関数の下に追加してください。

# ... product_list() 関数の下 ...

# 商品詳細ページ
# <int:product_id> の部分が「動的ルーティング」
@app.route('/product/<int:product_id>')
def product_detail(product_id):
    # 1. URLから受け取ったIDを使って、商品をデータベースから検索
    #    get_or_404 は、IDが見つかれば商品データを返し、
    #    見つからなければ自動で 404 Not Found ページを表示する便利な機能
    product = Product.query.get_or_404(product_id)
    
    # 2. product_detail.html を表示し、特定の商品データを渡す
    return render_template('product_detail.html', product=product)

ポイントは @app.route('/product/<int:product_id>') です。

<int:product_id> の部分は、「ここには、product_id という名前で、int(整数)が入ってきますよ」という宣言です。

例えば、/product/1 にアクセスすれば product_id = 1 として、/product/2 にアクセスすれば product_id = 2 として、product_detail 関数が実行されます。

これが動的ルーティングです!

関数の中では Product.query.get_or_404(product_id) を使っています。

all() が「全部」だったのに対し、get_or_404(ID) は「指定されたID(主キー)の商品を1件だけ取ってきて! もし無かったら404エラーにしてね」という命令です。

では、表示用のHTMLファイル templates/product_detail.html を新しく作りましょう。

templates/product_detail.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>{{ product.name }} - 商品詳細</title>
</head>
<body>
    <h1>{{ product.name }}</h1>
    
    <p>価格: {{ product.price }}円</p>
    
    <p>
        {{ product.description }}
    </p>

    <a href="/products">商品一覧に戻る</a>
</body>
</html>

最後に、一覧ページ (products.html) から詳細ページへ飛べるように「リンク」を設置しましょう。

templates/products.html を修正します。

<ul>
        {% for product in products %}
            <li>
                <a href="{{ url_for('product_detail', product_id=product.id) }}">
                    {{ product.name }}
                </a>
                ({{ product.price }}円)
                </li>
        {% else %}
            <li>商品はまだありません。</li>
        {% endfor %}
    </ul>

ここでは <a href="..."> というHTMLのリンクタグを使いました。

href の中身が {{ url_for('product_detail', product_id=product.id) }} となっていますね。

url_for() もJinja2の便利な機能です。

url_for('関数名', 引数=値) と書くと、Flaskが自動的に正しいURL(この場合は /product/1 や /product/2 など)を生成してくれます。

URLを直接 /product/{{ product.id }} と書くよりも、将来的にURLの構造を変更したときに自動で追従してくれるため、url_for() を使うのが推奨されています。

さあ、python app.py を再起動して、http://127.0.0.1:5000/products にアクセスしてみてください。

商品名がリンクになっているはずです。

クリックして、商品の詳細ページが表示されることを確認しましょう!


第2章はここまでです。

データベースという「データを保存する箱」と、ORMという「通訳さん」を使って、商品のカタログ機能を作ることができましたね。

でも、今のままでは誰でも商品を見られるだけです。

次の第3章では、いよいよショッピングサイトの核心部、「お客様」を管理する機能、すなわち「ユーザー登録」と「ログイン」機能の実装に挑戦します!

第3章 お客様をお迎えする準備:ユーザー認証機能

3-1. 「ログイン」の仕組みを知ろう

私たちが目指すのは、「一度ログインしたら、サイト内のどのページに行っても『あなたは〇〇さんですね』とサーバーが覚えてくれている」状態です。

でも、Webの仕組み(HTTPという通信ルール)は、実はとても「忘れっぽい」のです。

ページを移動するたびに、サーバーは「はじめまして、どなたですか?」と、前のページの記憶を忘れてしまいます。

そこで、「あなたはログイン済みですよ」という証拠を、ユーザーとサーバーで共有し続ける必要があります。

そのために使われる技術が「セッション」と「クッキー(Cookie)」です。

  • クッキー (Cookie)
    • 例えるなら、「サーバーが発行する会員カード」や「ロッカーの鍵」です。
    • ユーザーが正しくログインできた証拠として、サーバーがこの小さなデータ(鍵)を作り、ユーザーのPC(ブラウザ)に「これ、持っておいてください」と預けます。
    • ユーザーは、次にそのサイトにアクセスするとき、必ずこの「鍵(クッキー)」をサーバーに見せます。
  • セッション (Session)
    • 例えるなら、「サーバー側にある会員名簿」や「ロッカーそのもの」です。
    • サーバーは、発行した「鍵(クッキー)」とペアになる「名簿(セッション)」を自分側で管理しています。
    • この名簿には「鍵番号123番の人は、ゆうせいさん。ログイン中。」といった情報が書かれています。

ユーザーが鍵(クッキー)を見せると、サーバーは名簿(セッション)と照合し、「ああ、123番のゆうせいさんですね。どうぞ!」とログイン状態を認識できるわけです。

Flaskでは、この仕組みを安全に実現するために「シークレットキー(SECRET_KEY)」という秘密の合言葉が必要になります。

この合言葉を使って「鍵(クッキー)」が偽造されていないかをチェックするのです。

さあ、app.pyに、この大事な合言葉を設定しましょう。

# app.py の上部 (app = Flask(__name__) のすぐ下あたり)
import os

app = Flask(__name__)

# ★★★ ここから追加 ★★★
# クッキーやセッションを安全に保つための秘密鍵(合言葉)
# 必ず、誰にも知られない複雑な文字列に変更してください
app.config['SECRET_KEY'] = 'my_secret_key_12345'
# ★★★ ここまで追加 ★★★

# --- データベース設定 ---
# (前回のコード)

3-2. ユーザー登録機能の実装

まずは、お客様に会員登録してもらうための機能が必要です。

これには、Flaskの世界で非常に人気のある拡張機能「Flask-WTF(フラスク・ダブルティーエフ)」を使います。

これは、Webフォーム(お名前やパスワードを入力する欄)を、驚くほど簡単に、そして安全に作るための道具です。

「入力必須ですよ」「これはメールアドレスの形じゃないですよ」といった面倒なチェック(バリデーション)も自動でやってくれます。

まずはインストールしましょう。

pip install Flask-WTF

次に、商品モデル(Product)を作った時のように、「ユーザーモデル(User)」をapp.pyに定義します。

どんな会員情報が必要か、設計図を書きましょう。

# app.py の Product モデル定義の上 (db = SQLAlchemy(app) の下あたり)

# ... (db = SQLAlchemy(app) の下) ...

# 【モデルの定義】

# ★★★ ここから追加 ★★★
# ユーザー情報を表す「User」クラスを作成
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(128), nullable=False) # ★パスワードそのものではなく、ハッシュ値を保存

    def __repr__(self):
        return f'<User {self.username}>'
# ★★★ ここまで追加 ★★★


class Product(db.Model):
    # ... (前回のコード) ...

注目は password_hash です。

「パスワード(password)」ではなく、「パスワードのハッシュ(password_hash)」という名前にしていますね。

これについては、次のセクションで詳しく説明します!

モデル(設計図)を変更したので、データベースに「この設計図を反映してね!」とお願いするのを忘れずに。

ターミナルで flask shell を起動し、db.create_all() を(再度)実行して、Userテーブルを作成しておいてください。(既存のProductテーブルは消えないのでご安心を)

3-3. パスワードは安全に! Flask-Bcryptでハッシュ化

さて、先ほどの password_hash です。

ショッピングサイトを作る上で、絶対に守らなければならない鉄則があります。

それは、「お客様のパスワードを、そのままデータベースに保存してはいけない!」ということです。

もし、データベース(site.dbファイル)が何らかの理由で盗まれたら…?

パスワードがそのまま保存されていたら、すべてのお客様のアカウントが乗っ取られてしまいます。

そこで登場するのが「ハッシュ化」という技術です。

「ハッシュ化」とは、専門用語ですが、例えるなら「一方通行の魔法のミキサー」です。

  1. あなたが「password123」という元のパスワード(イチゴ🍓)をミキサーに入れます。
  2. ミキサーが動くと、「$2b$12$E...」(イチゴスムージー🍹)という、まったく別の、ランダムに見える文字列に変換されます。
  3. ここが最重要ポイント! この「スムージー」から、元の「イチゴ」に戻すことは(ほぼ)不可能なのです。

私たちは、データベースにはこの「スムージー(ハッシュ化されたパスワード)」だけを保存します。

もし盗まれても、元のパスワードは誰にもわかりません。

では、どうやってログインをチェックするのでしょう?

それは、「お客様が入力したパスワード(新しいイチゴ🍓)を、その都度ミキサーにかけて、保存されているスムージー🍹と味が同じか?」を比べるのです。

この「魔法のミキサー」の役割を担うのが、「Flask-Bcrypt(フラスク・ビークリプト)」です。

インストールしましょう。

pip install Flask-Bcrypt

そして、app.pyで使えるように設定します。

# app.py の上の方
from flask_bcrypt import Bcrypt
# ...

app = Flask(__name__)
# ... (SECRET_KEY 設定) ...
db = SQLAlchemy(app)

# ★★★ ここから追加 ★★★
bcrypt = Bcrypt(app)
# ★★★ ここまで追加 ★★★

# ... (モデル定義) ...

これでミキサーが準備できました!

3-4. ログインとログアウトの実装 (Flask-Loginの活用)

いよいよ、ログイン・ログアウト機能の本体を作ります。

「セッション」や「クッキー」の管理を、すべて手動でやるのは本当に大変です。

そこで、3つ目の強力な拡張機能、「Flask-Login」の登場です!

Flask-Loginは、例えるなら「サイトの優秀な受付係」です。

この受付係が、ログイン・ログアウト処理、セッション管理、クッキーの発行、「あなたはログイン済みですか?」のチェックなど、面倒な作業をすべて引き受けてくれます。

私たちは、この受付係に「お客様の探し方」を一度だけ教えてあげればOKです。

まずはインストール。

pip install Flask-Login

次に、app.pyで受付係(Flask-Login)に働いてもらう準備をします。

# app.py の上の方
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
# ...

app = Flask(__name__)
# ... (bcrypt 設定) ...

# ★★★ ここから追加 ★★★
# ログインマネージャー(受付係)の初期化
login_manager = LoginManager(app)
login_manager.login_view = 'login' # 未ログイン時にリダイレクトする先('login'は後で作るログイン関数の名前)
login_manager.login_message_category = 'info' # flashメッセージのカテゴリ
# ★★★ ここまで追加 ★★★


# --- モデル定義 ---

# ★★★ Userモデルを修正 ★★★
# UserMixin を「継承」リストに追加
class User(db.Model, UserMixin):
    # ... (中身は同じ) ...
    
# ★★★ ここまで修正 ★★★

# ... (Product モデル) ...

# ★★★ ここから追加 ★★★
# 受付係(Flask-Login)に「お客様の探し方」を教える
# ユーザーID(クッキーに保存される)を元に、対応するユーザーオブジェクトを返す関数
@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))
# ★★★ ここまで追加 ★★★

UserMixin を継承リストに追加したのは、「受付係がお客様を管理するのに必要な標準機能(『あなたはログイン中ですか?』に答える機能など)をUserモデルに自動で追加しますよ」というおまじないです。

load_user 関数は、受付係への指示書です。「セッションから『IDが1番の人』という情報が出てきたら、User.query.get(1) を使ってデータベースから探してきてね」と教えてあげています。

3-5. 登録・ログインページと、「ログイン必須」ページの作り方

準備が整いました!

最後に、実際にユーザーが操作するページ(ルーティング)と、HTML(テンプレート)を作ります。

(※コードが長くなるため、ここではFlask-WTFのフォーム定義は省略し、ルーティングの主要部分に焦点を当てます。完全なコードは別途参照してください)

まず、ログインしていないと見られない「マイページ」を作ってみましょう。

app.py に以下を追加します。

# app.py の下の方 (ルーティングが並んでいるところ)

# ... (product_detail 関数の下) ...

# ★★★ ここから追加 ★★★

# マイページ(ログイン必須)
@app.route('/mypage')
@login_required  # ★これが「ログイン必須」の魔法のサイン!
def mypage():
    # current_user は、Flask-Loginが管理している「今ログイン中のユーザー」情報
    return f'こんにちは、{current_user.username}さん!'


# ログインページ
@app.route('/login', methods=['GET', 'POST'])
def login():
    # ... (ここにWTFormsを使ったフォーム処理が入る) ...
    
    # 仮のロジック (フォームが送信されたら)
    if request.method == 'POST':
        # 1. フォームからEmailとPWを受け取る
        email = request.form['email']
        password = request.form['password']
        
        # 2. EmailでユーザーをDBから探す
        user = User.query.filter_by(email=email).first()
        
        # 3. ユーザーが実在し、かつパスワードが正しいかチェック
        #    bcrypt.check_password_hash で「スムージー」の味を比べる
        if user and bcrypt.check_password_hash(user.password_hash, password):
            # 4. 正しければ「受付係」にログイン処理を依頼
            login_user(user, remember=True) # remember=True でクッキーを保持
            return redirect(url_for('mypage')) # マイページへ飛ばす
        else:
            # 5. 失敗したらエラーメッセージ
            return 'ログイン失敗'
            
    # GETリクエストならログインページを表示
    return render_template('login.html') # (login.htmlを別途作成)


# ログアウト処理
@app.route('/logout')
def logout():
    # 「受付係」にログアウト処理を依頼
    logout_user()
    return redirect(url_for('hello')) # トップページに戻る


# ユーザー登録ページ
@app.route('/register', methods=['GET', 'POST'])
def register():
    # ... (ここにWTFormsを使ったフォーム処理が入る) ...
    
    # 仮のロジック (フォームが送信されたら)
    if request.method == 'POST':
        # 1. フォームから情報を受け取る
        username = request.form['username']
        email = request.form['email']
        password = request.form['password']
        
        # 2. パスワードを「ミキサー(ハッシュ化)」にかける
        hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
        
        # 3. 新しいユーザーを作成
        new_user = User(username=username, email=email, password_hash=hashed_password)
        
        # 4. データベースに追加・保存
        db.session.add(new_user)
        db.session.commit()
        
        return redirect(url_for('login')) # ログインページへ
        
    # GETリクエストなら登録ページを表示
    return render_template('register.html') # (register.htmlを別途作成)

# ★★★ ここまで追加 ★★★

(※上記のコードは、Flask-WTFを使わない簡易版のため、セキュリティ(CSRF対策など)が不十分です。実際の研修ではWTFormsを使った形に置き換えてください。)

一番の注目ポイントは、マイページのルーティングです。

@app.route('/mypage')
@login_required
def mypage():
    # ...

@login_required という「デコレータ(装飾)」を一行追加するだけで、Flask-Login(受付係)が自動的に「このページはログイン必須」と判断してくれます。

もしログインしていない人が /mypage にアクセスしようとしたら、login_manager.login_view = 'login' の設定に基づき、自動的に /login ページに追い返してくれるのです。

なんて便利なんでしょう!


Flaskとセッションで作る!ショッピングカート機能の仕組み【第4章】

こんにちは。ゆうせいです。

第3章では、Flask-LoginやBcryptといった強力な助っ人を使って、安全な「ログイン機能」を実装しましたね。これでお店に「会員カウンター」ができました!

さて、お客様がログインできるようになったら、次はもちろん「お買い物」です。

商品を選んでカゴに入れる…この、ネットショッピングでおなじみの「ショッピングカート」機能を作っていきましょう。

これはEコマースのまさに心臓部とも言える機能です。

「どの人が、どの商品を、何個カゴに入れているか」を、サーバーはどうやって覚えているのでしょうか?

その秘密は、第3章でも少し登場した「セッション」にあります。

第4章 お買い物の心臓部:ショッピングカート機能

4-1. カートのデータをどこに保存する?

まず、一番の疑問です。

お客様がカゴに入れた商品の情報は、どこに保存すればよいでしょう?

第2章で学んだ「データベース」でしょうか?

もちろん、それでも実現は可能です。しかし、データベースは「確定した大事な情報(会員情報や注文履歴)」を永続的に保存する場所です。

考えてみてください。

お客様が、商品をカゴに入れたり、やっぱりやめて削除したり、数を変えたり…といった操作は、購入を確定するまで非常に「一時的」で「変わりやすい」データですよね。

それに、まだログインしていない「ゲスト」のお客様だって、カゴを使いたいはずです。

こうした「一時的」で「その人(のブラウザ)専用」のデータを保存するのに、データベースは少し重厚すぎます。

そこで大活躍するのが、再び登場「セッション (Session)」です! 🧺

第3章では、セッションを「ログイン状態を管理する会員名簿」のように例えました。

今回は、セッションを「サーバーが、お客様(のブラウザ)ごとに一時的に貸し出す、専用の買い物カゴ」として使います。

セッションは、データベースと違って、その人のブラウザ(のクッキー)に紐付いています。

だから、ログインしていなくても使えるのです!

具体的には、Flaskの session という、Pythonの「辞書(dictionary)」によく似た特別な入れ物を使います。

私たちは、この session の中に 'cart' という名前のキーで、さらに辞書を保存することにします。

こんなイメージです。

session['cart'] = { 商品ID : 個数, 商品ID : 個数, ... }

例えば、IDが1のTシャツを2枚、IDが5のキャップを1個カゴに入れたら、セッションの中身はこうなります。

session['cart'] = { 1: 2, 5: 1 }

これなら、どの商品を何個選んだか、一目でわかりますね!

4-2. カートに商品を追加する機能

理屈がわかったら、早速実装してみましょう!

まずは、商品詳細ページ (product_detail.html) に「カートに入れる」ボタンを設置します。

templates/product_detail.html を開いて、商品説明の下あたりに、form タグを追加します。

<form action="{{ url_for('add_to_cart', product_id=product.id) }}" method="POST">
        <div>
            <label for="quantity">数量:</label>
            <input type="number" id="quantity" name="quantity" value="1" min="1">
        </div>
        <button type="submit">カートに入れる</button>
    </form>
    <hr>
    <a href="{{ url_for('product_list') }}">商品一覧に戻る</a>
</body>
</html>

このフォームは、method="POST" と指定されているので、ボタンが押されると「POSTリクエスト」という形式でデータを送信します。

送信先は action に指定された url_for('add_to_cart', product_id=product.id) 、つまり /add_to_cart/1 のようなURLです。

では、このURLを受け取る app.py 側の関数(ルーティング)を作りましょう。

app.py の下の方に、以下のコードを追加します。

# app.py の上の方に、session, redirect, request を import します
from flask import Flask, render_template, session, redirect, url_for, request
# ...

# ... (他のルーティング) ...

# ★★★ ここから追加 ★★★
@app.route('/add_to_cart/<int:product_id>', methods=['POST'])
def add_to_cart(product_id):
    # 1. フォームから送信された「数量(quantity)」を受け取る
    #    request.form は、POST送信されたフォームデータを保持しています
    quantity_str = request.form.get('quantity', '1')
    
    # 念のため、数値に変換できるかチェック
    try:
        quantity = int(quantity_str)
        if quantity < 1:
            quantity = 1
    except ValueError:
        quantity = 1

    # 2. セッションから、現在のカート情報を取得する
    #    session.get('cart', {}) は、「'cart'キーがあればその値を、無ければ空の辞書{}をください」
    #    という安全な取り出し方です
    cart = session.get('cart', {})

    # 3. カート(辞書)を更新する
    #    product_idを「文字列」としてキーにすることをお勧めします(JSONとの互換性のため)
    product_id_str = str(product_id)
    
    #    すでに入っていれば数量を加算、初めてなら新しいキーとして追加
    cart[product_id_str] = cart.get(product_id_str, 0) + quantity

    # 4. 更新したカート情報を、再びセッションに保存する
    session['cart'] = cart
    
    # (おまけ) session['cart'] の中身がどうなったかターミナルで確認してみましょう
    print(f"カートの中身: {session['cart']}")

    # 5. カートページ(後で作る)にリダイレクト(転送)する
    return redirect(url_for('cart_page'))

# ★★★ ここまで追加 ★★★

やっていることはシンプルですね!

session からカート(辞書)を取り出し、Pythonの辞書を操作するのと同じように数量を更新し、最後に session に戻す。

たったこれだけです!

session['cart'] = cart のように、session に辞書を「再代入」するのがポイントです。

(Flaskは、辞書の中身だけが変わったことに気づかない場合があるため、明示的に「これが新しいカートだよ!」と教えます)

4-3. カートの中身を表示・更新するページ

さて、add_to_cart の最後で redirect(url_for('cart_page')) というコードを書きました。

まだ cart_page という関数は存在しないので、次は「カートの中身一覧ページ」を作りましょう。

app.py に、以下のルーティングを追加します。

# ... (add_to_cart の下) ...

@app.route('/cart')
def cart_page():
    # 1. セッションからカート情報を取得
    cart_data = session.get('cart', {})

    # 2. カートが空っぽの場合
    if not cart_data:
        return render_template('cart.html', cart_items=[], total_price=0)

    # 3. カートに入っている商品のIDリストを取得
    #    cart_data.keys() は ['1', '5'] のようなリストです
    product_ids = [int(pid) for pid in cart_data.keys()]

    # 4. データベースから、IDリストに一致する商品情報をまとめて取得
    #    Product.id.in_(...) は、SQLのIN句を賢く作ってくれます
    products = Product.query.filter(Product.id.in_(product_ids)).all()

    # 5. テンプレート(HTML)に渡すための整形済みリストを作成
    cart_items = []
    total_price = 0

    for product in products:
        # セッションから、この商品の数量を取り出す
        quantity = cart_data[str(product.id)]
        
        # 小計(価格 * 数量)を計算
        subtotal = product.price * quantity
        
        # 合計金額に加算
        total_price += subtotal
        
        # テンプレートで使いやすいよう、情報を辞書にまとめる
        cart_items.append({
            'product': product,
            'quantity': quantity,
            'subtotal': subtotal
        })

    # 6. HTMLテンプレートを呼び出し、必要なデータを渡す
    return render_template('cart.html', cart_items=cart_items, total_price=total_price)

この関数は、今までの関数より少し複雑に見えますね。

なぜなら、セッションには { '1': 2 } (IDと数量)しか入っていないからです!

これだけでは、「商品名」や「価格」がわかりません。

そこで、cart_data.keys() で商品IDのリストを取り出し、Product.query.filter(Product.id.in_(product_ids)) を使って、データベースから該当する商品情報を「まとめて」取得しています。

そして、cart_items というリストに、商品情報と数量、小計をセットにして格納し、HTMLに渡しているのです。

では、表示用の templates/cart.html を新しく作りましょう。

templates/cart.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ショッピングカート</title>
</head>
<body>
    <h1>あなたのカート</h1>

    {% if cart_items %}
        <table>
            <thead>
                <tr>
                    <th>商品名</th>
                    <th>価格</th>
                    <th>数量</th>
                    <th>小計</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
                {% for item in cart_items %}
                <tr>
                    <td>{{ item.product.name }}</td>
                    <td>{{ item.product.price }}円</td>
                    <td>
                        {{ item.quantity }}
                    </td>
                    <td>{{ item.subtotal }}円</td>
                    <td>
                        <form action="{{ url_for('remove_from_cart', product_id=item.product.id) }}" method="POST">
                            <button type="submit">削除</button>
                        </form>
                    </td>
                </tr>
                {% endfor %}
            </tbody>
        </table>

        <h2>合計金額: {{ total_price }}円</h2>

        <a href="#">レジに進む(第5章)</a>
    
    {% else %}
        <p>カートは空です。</p>
    {% endif %}

    <hr>
    <a href="{{ url_for('product_list') }}">お買い物を続ける</a>

</body>
</html>

python app.py を再起動し、商品詳細ページから「カートに入れる」ボタンを押してみてください。

cart_page 関数が実行され、カートの中身が一覧表示されるはずです!

4-4. カートから商品を削除する機能

最後に、カートから商品を削除する機能を実装しましょう。

cart.html には、すでに「削除」ボタンのフォームを設置しましたね。

action="{{ url_for('remove_from_cart', product_id=item.product.id) }}" を指しています。

このルーティングを app.py に追加します。

# ... (cart_page の下) ...

@app.route('/remove_from_cart/<int:product_id>', methods=['POST'])
def remove_from_cart(product_id):
    # 1. セッションからカート情報を取得
    cart = session.get('cart', {})
    
    product_id_str = str(product_id)

    # 2. カート(辞書)から、指定された商品IDのキーを削除
    #    pop() は、キーがあれば削除し、無くてもエラーにならない安全な方法
    cart.pop(product_id_str, None)

    # 3. 更新したカートをセッションに保存
    session['cart'] = cart

    # 4. カートページにリダイレクトして、最新の状態を表示
    return redirect(url_for('cart_page'))

add_to_cart よりもさらに簡単ですね!

セッションからカート(辞書)を取り出し、Pythonの pop() メソッドで指定されたキー(商品ID)を削除し、セッションに戻す。

たったこれだけです。

(応用)

cart.html の「数量」の部分も、add_to_cart と同じようなロジックで「数量更新フォーム」を作ることができますよ。

ぜひチャレンジしてみてください!(ヒント:数量を cart.get(product_id_str, 0) + quantity で上書きするのではなく、cart[product_id_str] = new_quantity のように「丸ごと上書き」する新しい関数 update_cart を作ります)


第4章はここまでです!

「セッション」という「一時的なカゴ」を使うことで、データベースを汚さずに、ログイン状態に関わらず使えるショッピングカートの基本機能が完成しました!

お客様は商品を選び、カゴに入れ、削除できるようになりました。

次はいよいよ、Eコマースのゴールです。

カゴに入った商品を持って「レジに進む」処理、すなわち「注文の確定」と「決済」の仕組みについて学んでいきましょう!

第5章 注文と決済:Eコマースのゴール

5-1. 注文情報を保存する「注文モデル」の設計

まず、大事なことを確認します。

今、カートの情報は「セッション」にありますよね。でも、セッションは一時的なものです。ブラウザを閉じたり、時間が経つと消えてしまいます。

お客様が「買った!」という事実は、消えてもらっては困ります。

「誰が」「いつ」「何を」「いくつ」「いくらで」買ったのか。

この情報は、第2章で学んだ「データベース」に、永久的な記録として保存しなければなりません。

そのために、新しい「設計図(モデル)」が2つ必要になります。

  1. Order (注文) モデル
    • 「1回のお買い物」そのものを記録する台帳です。
    • 「誰が(User ID)」、「いつ(購入日時)」、「合計金額はいくら」といった情報を保存します。
  2. OrderItem (注文明細) モデル
    • 「そのお買い物で、具体的に何を買ったか」を記録する明細書です。
    • 「どの注文(Order ID)に紐づくか」、「どの商品(Product ID)を」、「何個」、「買った時点での価格はいくら」といった情報を保存します。

なぜ「買った時点での価格」をわざわざ保存するのか、わかりますか?

もし、Tシャツを3000円で買った後、お店がそのTシャツを「セールで2500円」に値上げ(値下げ)したとします。

注文履歴を見たときに、後から変わった2500円で表示されてしまったら、おかしいですよね?

だから、購入が確定した「その瞬間」の価格をスナップショットとして保存しておく必要があるのです。

さあ、app.py に、この2つのモデルを追加しましょう。

User モデルや Product モデルが定義されている場所に追加してください。

# app.py のモデル定義エリア

# ... (Productモデルの下あたり) ...

import datetime # 日時を扱うために import

# 1. 注文(Order)モデル
# 1回の注文(レシートのヘッダー)
class Order(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    
    # 注文日時(デフォルトで、作成された瞬間を記録)
    date_created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
    
    # 合計金額
    total_price = db.Column(db.Integer, nullable=False)

    # 配送先住所(簡易的に文字列で保存)
    shipping_address = db.Column(db.String(200), nullable=True)

    # ★リレーションシップ(関連付け)★
    # 'User'モデルと関連付ける (誰の注文か)
    # db.ForeignKey は「外部キー」と読み、別のテーブルのIDを指し示す目印です
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    
    # 'OrderItem'モデルと関連付ける (この注文の明細は何か)
    # 'order'は、OrderItem側からOrderを参照するときの名前
    # lazy=True は、必要になるまで関連データを読み込まない設定(効率化)
    items = db.relationship('OrderItem', backref='order', lazy=True)

    def __repr__(self):
        return f'<Order {self.id}>'


# 2. 注文明細(OrderItem)モデル
# 注文に含まれる各商品(レシートの明細行)
class OrderItem(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    
    # 数量
    quantity = db.Column(db.Integer, nullable=False)
    
    # 購入時点での価格(スナップショット)
    price_at_purchase = db.Column(db.Integer, nullable=False)

    # ★リレーションシップ(関連付け)★
    # 'Order'モデルと関連付ける (どの注文の明細か)
    order_id = db.Column(db.Integer, db.ForeignKey('order.id'), nullable=False)

    # 'Product'モデルと関連付ける (どの商品か)
    product_id = db.Column(db.Integer, db.ForeignKey('product.id'), nullable=False)

ここで db.ForeignKeydb.relationship という新しい専門用語が出てきました。

db.ForeignKey('user.id') とは、「ここに書くIDは、user テーブルの id 列と繋がっていますよ」という「関連付けの目印」です。

例えるなら、注文台帳(Order)に、会員証(User)のID番号を書き込む欄を作るイメージです。

db.relationship(...) は、ORM(通訳さん)への「便利なショートカットの指示」です。

Order モデルに items = db.relationship(...) と書くことで、order.items と書くだけで、その注文に紐づく OrderItem のリストを自動で取ってきてくれるようになります。

逆も然りで、OrderItem モデルの backref='order' の設定により、item.order でその明細が属する Order の情報を取れます。

これがORMの強力な機能、「リレーショナル(関連)」マッピングです!

モデルを変更したので、flask shelldb.create_all() を実行し、データベースにテーブルを作成するのを忘れないでくださいね。

5-2. 配送先住所の入力フォーム

カートページ (cart.html) に、「レジに進む」ボタンを追加しましょう。

templates/cart.html

<h2>合計金額: {{ total_price }}円</h2>

    {% if current_user.is_authenticated %} <a href="{{ url_for('checkout') }}">レジに進む</a>
    {% else %}
        <p><a href="{{ url_for('login') }}">ログイン</a> してお会計に進んでください。</p>
    {% endif %}
    <hr>

current_user.is_authenticated は、第3章で導入したFlask-Login(受付係)が提供してくれる機能で、「今、ログイン中のユーザーですか?」を True か False で返してくれます。

注文は必ずユーザーと紐付ける必要があるため、ログイン必須とするのが一般的です。

このボタンが押されると、/checkout ( checkout 関数) に移動します。

ここで、配送先住所などを入力するフォームを表示します。

app.py

# app.py

# ★login_required を import しておく
from flask_login import login_required, current_user
# ★flash メッセージ機能も import しておくと便利
from flask import Flask, render_template, session, redirect, url_for, request, flash

# ... (他のルーティング) ...

# ★★★ ここから追加 ★★★
@app.route('/checkout', methods=['GET', 'POST'])
@login_required # ログイン必須!
def checkout():
    # カートページ(4-3)と同じロジックで、カートの中身を取得
    cart_data = session.get('cart', {})
    if not cart_data:
        flash('カートが空です。', 'danger')
        return redirect(url_for('product_list'))

    product_ids = [int(pid) for pid in cart_data.keys()]
    products = Product.query.filter(Product.id.in_(product_ids)).all()
    
    cart_items = []
    total_price = 0
    for product in products:
        quantity = cart_data[str(product.id)]
        subtotal = product.price * quantity
        total_price += subtotal
        cart_items.append({
            'product': product,
            'quantity': quantity,
            'subtotal': subtotal
        })
    
    # ここからが checkout 固有の処理
    
    # POSTリクエスト(=フォームが送信された)の場合
    if request.method == 'POST':
        # 1. フォームから配送先住所を受け取る
        shipping_address = request.form.get('address')
        if not shipping_address:
            flash('配送先住所を入力してください。', 'warning')
            # 住所が空なら、もう一度 checkout ページを表示
            return render_template('checkout.html', cart_items=cart_items, total_price=total_price)

        # 2. ★決済処理(次のセクション)★
        # ... ここでStripeなどの決済APIを呼ぶ ...
        payment_success = True # ★今は「必ず成功」と仮定します

        # 3. 決済が成功したら、データベースに注文を記録
        if payment_success:
            
            # (3-a) Orderテーブルに記録
            new_order = Order(
                total_price=total_price,
                shipping_address=shipping_address,
                user_id=current_user.id # ログイン中のユーザーID
            )
            db.session.add(new_order)
            db.session.commit() # ★ここでコミットして new_order.id を確定させる

            # (3-b) OrderItemテーブルに記録
            for item in cart_items:
                order_item = OrderItem(
                    quantity=item['quantity'],
                    price_at_purchase=item['product'].price,
                    order_id=new_order.id, # 今作ったばかりのOrderのID
                    product_id=item['product'].id
                )
                db.session.add(order_item)
            
            db.session.commit() # 明細もすべて保存

            # (3-c) カート(セッション)を空にする
            session.pop('cart', None)
            
            flash('ご注文ありがとうございました!', 'success')
            return redirect(url_for('thank_you_page', order_id=new_order.id))

        else:
            flash('決済に失敗しました。', 'danger')

    # GETリクエスト(=初めてページを開いた)の場合
    return render_template('checkout.html', cart_items=cart_items, total_price=total_price)


# サンキューページ
@app.route('/thank_you/<int:order_id>')
@login_required
def thank_you_page(order_id):
    # 自分の注文かどうかを(念のため)確認
    order = Order.query.get_or_404(order_id)
    if order.user_id != current_user.id:
        flash('不正なアクセスです。', 'danger')
        return redirect(url_for('product_list'))
        
    return render_template('thank_you.html', order=order)

# ★★★ ここまで追加 ★★★

そして、templates/checkout.htmltemplates/thank_you.html を新しく作ります。

templates/checkout.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>注文確認</title>
</head>
<body>
    <h1>注文確認</h1>

    {% with messages = get_flashed_messages(with_categories=true) %}
      {% if messages %}
        <ul class=flashes>
        {% for category, message in messages %}
          <li class="{{ category }}">{{ message }}</li>
        {% endfor %}
        </ul>
      {% endif %}
    {% endwith %}

    <h2>ご注文内容</h2>
    <ul>
        {% for item in cart_items %}
            <li>{{ item.product.name }} ({{ item.product.price }}円) x {{ item.quantity }} = {{ item.subtotal }}円</li>
        {% endfor %}
    </ul>
    <h3>合計金額: {{ total_price }}円</h3>

    <hr>

    <form method="POST">
        <div>
            <label for="address">配送先住所:</label>
            <input type="text" id="address" name="address" style="width: 300px;">
        </div>
        
        <button type="submit">購入を確定する</button>
    </form>

</body>
</html>

templates/thank_you.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ご注文ありがとうございました</title>
</head>
<body>
    <h1>ご注文ありがとうございました!</h1>
    <p>注文番号: {{ order.id }}</p>
    <p>ご注文内容の確認は、注文履歴ページから行えます。</p>
    <a href="{{ url_for('product_list') }}">トップページに戻る</a>
</body>
</html>

checkout 関数のロジックが、この研修で最も重要です。

「フォーム送信(POST)だったら」という if 文の中で、決済(今はシミュレート)、Order の作成、OrderItem の作成、そして最後に session.pop('cart') でカートを空にする、という一連の流れ(これを「トランザクション」と呼びます)を実行しています。

5-3. 決済機能の導入 (外部サービス「Stripe」API連携の基礎)

さて、先ほどの checkout 関数で payment_success = True と仮定した部分がありました。

現実の世界では、ここでクレジットカード会社などと通信し、「本当にお金が払えるか」を確認しなければなりません。

これをゼロから作るのは、セキュリティの観点からも非常に困難です。

そこで私たちは、「決済代行サービス」の「API」を利用します。

「API (Application Programming Interface)」とは、専門用語ですが、例えるなら「外部サービスの専門窓口」です。

私たちがクレジットカード会社と直接、暗号化通信や法律の心配をする代わりに、Stripe(ストライプ)のような決済代行サービスが提供する「API」という窓口に、決まった形式で「この人に5000円請求して」とお願いするだけです。

すると、Stripeが面倒な処理をすべて裏側で実行し、「OK!」か「NG!」だけを私たちに返してくれます。

今回は、Stripe APIを使った「雰囲気」だけを体験してみましょう。

(実際に動かすには、Stripeへの登録と秘密鍵の設定が必要です)

まずはライブラリをインストールします。

pip install stripe

そして、checkout 関数の「購入を確定する」ボタンを、Stripeが提供する決済ボタン(Checkout)に置き換えるのが一般的な流れです。

(Stripeの決済フローは、Stripeのサーバーに一度リダイレクトし、決済が完了したら私たちのサイトの thank_you_page に戻ってくる、という動きをします)

checkout 関数のロジックは、実際にはStripeからの「決済完了通知(Webhook)」を受け取る別の関数として実装することが多いですが、ここでは先ほどの POST 処理の中で「Stripeに問い合わせたフリ」をします。

5-4. 注文確定とサンキューページの作成

これは、checkout 関数の実装(5-2)で既に行いましたね!

重要な流れをもう一度おさらいします。

  1. @login_required で、ログイン中のユーザーのみアクセスを許可する。
  2. GET リクエスト(初回アクセス)では、カートの中身を確認し、checkout.html(住所入力フォーム)を表示する。
  3. POST リクエスト(フォーム送信)では、
    1. 住所情報を受け取る。
    2. (ここでStripe APIを呼び出し、決済を実行する)
    3. 決済が成功したら、Order モデルと OrderItem モデルにデータを書き込む。
    4. db.session.commit() でデータベースに「確定」する。
    5. session.pop('cart') でカートを「空」にする。(重要!)
    6. redirectthank_you_page に飛ばす。

この流れがEコマースの心臓部です!

5-5. 注文履歴の表示

最後のおまけです。

データベースに保存した Order は、「マイページ」などで一覧表示できるようにしましょう。

app.py に、注文履歴ページのルーティングを追加します。

# app.py

@app.route('/order_history')
@login_required
def order_history():
    # ログイン中のユーザーの注文(Order)を、
    # 注文日時の「降順(desc)」=新しい順で、
    # すべて(all)取得する
    orders = Order.query.filter_by(user_id=current_user.id).order_by(Order.date_created.desc()).all()
    
    return render_template('order_history.html', orders=orders)

Order.query.filter_by(user_id=current_user.id) が「自分の注文だけを探す」処理です。

order_by(Order.date_created.desc()) が「新しい順に並べ替える」処理です。

そして、templates/order_history.html を新しく作ります。

templates/order_history.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>注文履歴</title>
</head>
<body>
    <h1>注文履歴</h1>

    {% if orders %}
        {% for order in orders %}
            <div style="border: 1px solid #ccc; margin-bottom: 15px; padding: 10px;">
                <h3>注文日: {{ order.date_created.strftime('%Y-%m-%d') }} (注文番号: {{ order.id }})</h3>
                <p>配送先: {{ order.shipping_address }}</p>
                
                <h4>注文明細:</h4>
                <ul>
                    {% for item in order.items %}
                        <li>商品ID: {{ item.product_id }} (数量: {{ item.quantity }} x {{ item.price_at_purchase }}円)</li>
                    {% endfor %}
                </ul>
                <p><b>合計金額: {{ order.total_price }}円</b></p>
            </div>
        {% endfor %}
    {% else %}
        <p>注文履歴はまだありません。</p>
    {% endif %}

    <a href="{{ url_for('product_list') }}">お買い物を続ける</a>
</body>
</html>

order_history.html で商品名を表示するには、OrderItemproduct_name を保存するようモデルを修正するか、Jinja2テンプレート内で item.product のように(リレーションを貼って)アクセスする必要があります。ここでは後者のほうがFlaskらしいので、OrderItem モデルに Product への db.relationship を追加するほうが良いかもしれませんね)


第5章はここまでです!

ついに、お客様が商品を選び、カゴに入れ、決済し、その履歴がデータベースに永久に記録される、というEコマースの「メインストリート」が完成しました!

しかし、今のままでは、お店側(私たち)が新しい商品を登録したり、注文を確認したりするのが大変です。flask shell を毎回使うわけにもいきませんよね?

次の最終章、第6章では、お店の「裏側」、すなわち「管理者用ダッシュボード」の作り方を学んでいきましょう!

第6章 お店の裏側:管理者用ダッシュボード

6-1. 管理者だけがアクセスできる画面を作る

管理画面は、一般のお客様に見られてはいけません。

第3章で学んだ @login_required は、「ログインさえしていれば誰でも見られる」機能でした。

今回は、「ログインしていて、かつ、管理者権限を持つユーザー」だけが見られるようにする必要があります。

どうすれば良いでしょう?

まずは、User モデル(設計図)を修正し、「管理者かどうか」を判別する「印」を追加しましょう。

app.pyUser モデルに、is_admin という列(カラム)を追加します。

# app.py の User モデル
class User(db.Model, UserMixin):
    # ... (id, username, email, password_hash) ...
    
    # ★★★ ここから追加 ★★★
    # 管理者フラグ(デフォルトはFalse=一般ユーザー)
    is_admin = db.Column(db.Boolean, nullable=False, default=False)
    # ★★★ ここまで追加 ★★★

モデルを変更したので、データベースに反映させる必要があります。

(flask shell で db.create_all() を実行するか、本格的な運用では Flask-Migrate という拡張機能を使ってマイグレーションを行います。今回は db.create_all() で大丈夫です)

次に、flask shell を使って、あなた自身(特定のユーザー)を「管理者」に昇格させておきましょう。

$ flask shell
>>> from app import app, db, User
>>> app.app_context().push()
# 自分のユーザー(例:IDが1番)を探す
>>> u = User.query.get(1)
# 管理者フラグを True にする
>>> u.is_admin = True
# データベースに保存
>>> db.session.commit()
>>> exit()

これで準備完了です。

次に、「管理者専用」のページを作るための「魔法のサイン」を自作してみましょう。

@login_required のような「デコレータ」を自作します。

app.py の上部(import 文が並ぶあたり)に、以下のコードを追加します。

# app.py
from functools import wraps # ★デコレータ自作のために import
from flask_login import current_user
from flask import abort # ★403エラー(権限なし)を返すために import

# ... (他の import) ...

# ★★★ 管理者専用デコレータの定義 ★★★
def admin_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        # ログインしていないか、ログインしていても管理者でなければ
        if not current_user.is_authenticated or not current_user.is_admin:
            # 403 Forbidden(権限がありません)エラーを返す
            abort(403)
        return f(*args, **kwargs)
    return decorated_function

functools.wraps は、デコレータを自作するときのお作法だと思ってください。

重要なのは if not current_user.is_authenticated or not current_user.is_admin: の部分です。

Flask-Login が提供する current_user(今ログイン中の人)が、「管理者フラグ(is_admin)」を持っていなければ、abort(403) でアクセスを拒否します。

さあ、使ってみましょう!

管理ダッシュボードのトップページを作ります。

app.py に以下のルーティングを追加します。

# app.py

@app.route('/admin')
@login_required  # まずログイン必須
@admin_required  # 次に管理者必須!
def admin_dashboard():
    return render_template('admin/dashboard.html')

デコレータは上から順番にチェックされます。

@login_required でログインチェック、@admin_required で管理者チェック。

これで、一般ユーザーが /admin にアクセスしても、403エラー(またはログインページ)に飛ばされるようになり、安全です!

templates フォルダの中に admin というフォルダを作り、そこに dashboard.html を作成しておきましょう)

6-2. 商品の管理機能:CRUD

管理画面のメイン機能は、データの管理ですよね。

プログラミングの世界では、データ管理の基本操作を「CRUD(クラッド)」と呼びます。

  • Create: 作成 (新しい商品の登録)
  • Read: 読み取り (商品の一覧表示)
  • Update: 更新 (既存商品の編集)
  • Delete: 削除 (商品の削除)

このCRUD操作を、商品(Product)モデルに対して実装してみましょう。

(※第3章で使った Flask-WTF を使ってフォームを作成すると、より安全で完璧になりますが、ここではロジックの骨組みを中心に解説します)

app.py に、以下の4つのルーティングを追加します。

(すべて @admin_required を付けるのを忘れずに!)

# app.py

# --- 管理者向け:商品CRUD ---

# R: 商品一覧 (Read)
@app.route('/admin/products')
@login_required
@admin_required
def admin_products():
    products = Product.query.all()
    return render_template('admin/product_list.html', products=products)

# C: 商品追加 (Create)
@app.route('/admin/product/add', methods=['GET', 'POST'])
@login_required
@admin_required
def add_product():
    if request.method == 'POST':
        # フォームからデータを受け取る
        name = request.form['name']
        price = int(request.form['price'])
        description = request.form.get('description', '')
        
        # 新しい商品オブジェクトを作成
        new_product = Product(name=name, price=price, description=description)
        db.session.add(new_product)
        db.session.commit()
        
        flash('商品を追加しました', 'success')
        return redirect(url_for('admin_products'))
        
    return render_template('admin/product_form.html') # 追加用フォームを表示

# U: 商品編集 (Update)
@app.route('/admin/product/edit/<int:product_id>', methods=['GET', 'POST'])
@login_required
@admin_required
def edit_product(product_id):
    # 編集対象の商品をDBから取得
    product = Product.query.get_or_404(product_id)
    
    if request.method == 'POST':
        # フォームからのデータで上書き
        product.name = request.form['name']
        product.price = int(request.form['price'])
        product.description = request.form.get('description', '')
        
        db.session.commit() # 変更を保存
        
        flash('商品を更新しました', 'success')
        return redirect(url_for('admin_products'))

    # GETリクエストなら、現在のデータが入ったフォームを表示
    return render_template('admin/product_form.html', product=product)

# D: 商品削除 (Delete)
@app.route('/admin/product/delete/<int:product_id>', methods=['POST']) # 安全のためPOSTのみ
@login_required
@admin_required
def delete_product(product_id):
    product = Product.query.get_or_404(product_id)
    
    db.session.delete(product)
    db.session.commit()
    
    flash('商品を削除しました', 'success')
    return redirect(url_for('admin_products'))

templates/admin/ 内に、product_list.html(一覧)と product_form.html(追加・編集兼用フォーム)を作成してくださいね)

product_form.html は、product 変数が渡されてきたか(=編集か)どうかで、action 先や初期値を変えると、1つのファイルで追加と編集を兼用できますよ。

6-3. 顧客からの注文一覧を確認する

商品管理ができたら、次は「注文管理」です。

これは、CRUDのうち「R (Read)」だけを実装してみましょう。

app.py

# app.py

# --- 管理者向け:注文管理 ---

@app.route('/admin/orders')
@login_required
@admin_required
def admin_orders():
    # 注文を新しい順にすべて取得
    orders = Order.query.order_by(Order.date_created.desc()).all()
    
    # ★ N+1問題の対策(おまけ)
    # このままだと、一覧で注文者の名前を表示するたびにDBアクセスが発生します。
    # .options(db.joinedload(Order.user)) を使うと、
    # 最初にまとめてユーザー情報も取得してくれるので高速になります。
    # orders = Order.query.options(db.joinedload(Order.user)).order_by(Order.date_created.desc()).all()
    
    return render_template('admin/order_list.html', orders=orders)

@app.route('/admin/order/<int:order_id>')
@login_required
@admin_required
def admin_order_detail(order_id):
    order = Order.query.get_or_404(order_id)
    
    # 第5章で設定した order.items (リレーションシップ) がここで活躍!
    # これで、この注文に紐づく明細(OrderItem)がすべて取れます。
    
    return render_template('admin/order_detail.html', order=order)

templates/admin/order_list.html では、orders をループ処理します。

order.user.username のようにして、注文者の名前にもアクセスできます(リレーションシップのおかげです!)。

templates/admin/order_detail.html では、order.items をさらにループ処理して、購入された商品、数量、購入時価格などを表示します。

これで、お店側が注文を確認できるようになりました!

6-4. (発展)Flask-Adminを使った簡単な管理画面

さて、ここまでCRUD操作を「手作り」してきました。

大変でしたよね?

実は、Flaskには「Flask-Admin」という、非常に強力な拡張機能があります。

これは、例えるなら「管理画面の自動生成マシン」です。

私たちがやることは、app.py で「User モデルと Product モデルを管理したいです」と宣言するだけ。

すると、Flask-Adminが、先ほど私たちが手作りしたCRUD(商品追加・一覧・編集・削除)のページを、すべて自動で生成してくれるのです!

インストール

pip install Flask-Admin

app.py での設定

# app.py
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView

# ... (db = SQLAlchemy(app) の下あたり) ...

# ★★★ Flask-Admin の設定 ★★★

# ModelViewをカスタマイズ(管理者のみアクセス可能にする)
class MyModelView(ModelView):
    def is_accessible(self):
        return current_user.is_authenticated and current_user.is_admin
    
    def inaccessible_callback(self, name, **kwargs):
        # アクセス拒否されたらトップページへ
        return redirect(url_for('hello'))

# 管理画面の初期化
admin = Admin(app, name='MyShop Admin', template_mode='bootstrap4')

# 管理画面に、モデル(設計図)を追加
# これだけで User と Product のCRUDが自動生成される!
admin.add_view(MyModelView(User, db.session))
admin.add_view(MyModelView(Product, db.session))
admin.add_view(MyModelView(Order, db.session))
admin.add_view(MyModelView(OrderItem, db.session))
# ★★★ ここまで ★★★

たったこれだけです!

(先ほど自作した @admin_required のルーティングは不要になります)

python app.py を実行して、/admin にアクセスしてみてください。(admin = Admin(app, ...) と設定した時点で、/admin が自動的に作られます)

あなたが管理者としてログインしていれば、驚くほど高機能な管理画面が自動で表示されるはずです!

なぜこれを最初から使わなかったのでしょう?

それは、CRUDが裏側で「どう動いているか」を知らずに自動ツールを使うと、問題が起きたときに応用が効かないからです。

手動でCRUDを作った経験があるからこそ、Flask-Adminのありがたみも、カスタマイズの方法も理解できるのです。


第6章はここまでです。

お店の「表側」と「裏側」が、これで両方とも完成しました!

あなたはもう、立派なWebアプリケーション開発者です。

さて、この素晴らしいショッピングサイト、今はまだあなたのパソコンの中でしか動いていませんよね?

研修の最後を飾る第7章では、このサイトをインターネット上に公開し、世界中の誰もがアクセスできるようにする「デプロイ」という作業と、今後の学習についてお話しします!

この研修を終えたあなたが次に何を学ぶべきか、その「未来への地図」をお渡しします!

第7章 サイト公開と次のステップ

7-1. いよいよ公開! 本番環境へのデプロイ準備

私たちはこれまで、python app.py というコマンドでFlaskを動かしてきました。

このとき、app.run(debug=True) という設定を使っていたのを覚えていますか?

debug=True は「開発モード」を意味します。

これは、コードを変更したら自動でサーバーが再起動したり、エラーが起きたらブラウザに詳しい原因を表示してくれたりする、開発者のための便利なモードです。

しかし、このモードは、例えるなら「キッチンのドアを開けっ放しにして、調理の様子もレシピも全部お客さんに見せている」ような状態です。

非常に便利ですが、セキュリティはガバガバですし、一人のお客様(開発者)の対応しか想定されていません。

これをそのままインターネットに公開するのは、絶対にダメです!

本番環境(=お店)では、debug=TrueFalse に切り替え、もっと屈強な「店員さん」にサーバーの運営を任せる必要があります。

7-2. なぜGunicorn? 本番用サーバー(WSGI)とは

app.run() は、Flaskに組み込まれた「開発用サーバー」です。

これは「店員さんが一人しかいないお店」のようなものです。

あなたがアクセスしている間は良いですが、もし同時に100人のお客様が来店したら、レジは長蛇の列になり、お店はパンクしてしまいます。

そこで登場するのが、「WSGI(ウィズギー)サーバー」と呼ばれる、本番環境のプロフェッショナルたちです。

有名なものに、「Gunicorn(ジーユニコーン)」があります。

Gunicornを例えるなら、「優秀なフロアマネージャー」です。

Gunicornは、「店員さん(=ワーカと呼ばれる処理部隊)を何人待機させておくか」「お客様(=リクエスト)が来たら、ど手の空いている店員さんに割り振るか」といった交通整理を、超高速で実行してくれます。

これにより、Flaskアプリケーション(=お店のルールブック)自体は変えずに、何百、何千という同時アクセスを効率よくさばける、頑丈なお店(サーバー)を建てることができるのです。

本番環境へのデプロイでは、私たちが python app.py を直接実行するのではなく、Gunicornが app.py を読み込んで起動する、という形に変わります。

7-3. クラウドプラットフォームへのデプロイ

サーバーを頑丈にできても、それを置く「土地」が必要です。

昔は、自分で物理的なサーバー(コンピュータ)を買ってきて、24時間365日、電源とインターネットを確保し続ける必要がありました。これは大変ですよね。

今は、「クラウドプラットフォーム」という便利なサービスがあります。

これは、インターネット上に「Webサーバーを動かすための土地と設備一式」を貸し出してくれるサービスです。

代表的なものに、「Render (レンダー)」や「Heroku (ヘロク)」(現在は有料化が進みました)、「AWS (Amazon Web Services)」などがあります。

特にRenderのような「PaaS (Platform as a Service)」と呼ばれるサービスは、初心者にも優しいです。

PaaSは、例えるなら「厨房設備、電気、水道、警備がすべて整った店舗スペースを月額で借りる」ようなものです。

私たちは、自分たちのプログラム(app.py や site.db など)を持ち込むだけで、すぐにお店をオープンできます。

デプロイに必要な準備は、主に以下の2つです。

  1. requirements.txt の作成
    • これは「お店のオープンに必要な食材リスト」です。
    • クラウドプラットフォームに、「Flaskと、Flask-SQLAlchemyと、Gunicornをインストールしておいてね!」と教えるためのファイルです。
    • ターミナルで以下のコマンドを実行すると、今あなたの環境(venv)に入っているライブラリの一覧が自動で作成されます。
    pip freeze > requirements.txt
  2. 起動コマンドの指定
    • プラットフォームの管理画面で、「お店を開くときの合図」を教えます。
    • gunicorn app:app のように、「Gunicornを使って、app.py の中にある app という名前のFlaskアプリを起動してね」と指定します。

この2つを準備して、GitHub(プログラムの置き場)とRenderを連携させれば、あなたのショッピングサイトは、ついに世界中の誰もがアクセスできる「本物のWebサイト」としてインターネットに公開されます!

7-4. これからの学習指針

この研修で、あなたはWebアプリケーション開発の「全体像」を掴みました。

データベースから始まり、認証、セッション、API連携、そして管理画面まで。

これはEコマースに限らず、あらゆるWebサービスの「土台」となる知識です。

もし、あなたがさらにレベルアップしたいなら、次に学ぶべき道筋をいくつか紹介しますね。

  • 1. テストを書く (Pytest)
    • プロの開発現場では、「テストコード」が必須です。
    • 「カートに商品を追加する」機能を修正したとき、「ログイン機能が壊れていないか」を毎回手で確認するのは大変ですよね?
    • pytest などのツールを使い、プログラムが正しく動くかを自動でチェックする仕組みを学びましょう。
  • 2. フロントエンドを極める (JavaScript, React)
    • 今回の研修では、HTML(Jinja2)だけでシンプルな画面を作りました。
    • 「ボタンを押したら、ページを移動せずにカートの中身が更新される」(非同期通信)ような、リッチで滑らかな動き(UI/UX)を実現するには、「JavaScript」や、そのライブラリである「React」「Vue.js」の学習が不可欠です。
  • 3. データベースを深掘りする (SQL, パフォーマンス)
    • 今回はSQLiteという簡易的なDBを使いましたが、本番環境では「PostgreSQL」や「MySQL」といった、より強力なデータベースが使われます。
    • また、ORM(SQLAlchemy)に頼るだけでなく、裏側で動いている「SQL」言語そのものを理解すると、より複雑なデータ操作や、サイトが重くなったときの原因調査(パフォーマンスチューニング)ができるようになります。
  • 4. 応用機能に挑戦する
    • 今回学んだ知識を応用して、新しい機能を追加してみましょう!
    • 例えば、「商品レビュー(口コミ)機能」「商品の検索機能」「画像のアップロード機能」「メールマガジン配信機能」など、アイデアは無限にあります。

結びに

全7章、本当にお疲れ様でした!

あなたはもう、「Webサイトってどうやって動いてるの?」と質問する側から、「Webサイトはね、サーバーサイドのロジックとデータベースが連携して…」と説明できる側になりました。

プログラミングの学習は、この研修が終わってからが本番です。

たくさんのエラーに悩み、動かないコードに頭を抱える日もあるでしょう。

でも、忘れないでください。あなたは「ゼロから一つのサービスを完成させた」という、揺るぎない自信と経験を手に入れたのです。

この研修が、あなたの「作りたいものを作る」力になることを、心から願っています。

最後までお付き合いいただき、ありがとうございました!

またどこかでお会いしましょう。


セイ・コンサルティング・グループの新人エンジニア研修のメニューへのリンク

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。