はじめに
ECサイトのカート機能では、
- Aさんのカート
- Bさんのカート
をきちんと分けて管理する必要があります。
そのときに鍵になるのが 「セッション(session)」と「session_id」「Cookie」 です。
ここでは、まず前提となる HTTP / REST / セッション の概念から整理していこうと思います。
セッションの仕組みと概念
まず前提として、WebアプリはHTTPというプロコトルの上で動いています。そして、Webアプリではフロントエンド・バックエンとのやり取りをREST APIという設計スタイルに沿って実装することが多いです。
このREST APIというルールでは
- 1回のリクエストはそれだけで完結した情報を持つ
- サーバー側は目のリクエストの状態を前提としない
つまり、HTTP/REST API そのものは
このリクエストがさっきアクセスしてきたあのユーザーが同じ人かどうか、このユーザーが前回どんな操作をしたのか
といった状態(state)を覚えておく仕組みがありません。
REST API についての詳しい内容は、こちらで解説しています。
そのため、何かしらの工夫をしないと、
Aさんが通販サイトでカートに商品を入れても、次のリクエストではまた何も入っていないカートから処理が始まってしまいます。
というように、毎回まっさらな状態から処理されてしまうのです。
これがステートレスな通信です。
しかし、通販サイトでは
ユーザーごとにカートの中身を保持したり、画面遷移をしても多少時間が空いてもカートの中身を復元したいということが多々あります。
このようにユーザーごとに状態を持たせる仕組みがsessionです。
状態をもつ通信のことをステートフルな通信と呼びます。
セッションID・Cookie と REST API の関係
セッションでは、ユーザーごとのアクセスを識別するためにsession_idを使います。REST API自体はステートレスですが、セッションIDを使い状態をサーバ側に置いておくことでユーザーごとの状態を扱えるようにすることができます。
流れのイメージ
1.ユーザーが初回アクセスをする
- サーバー側で新しいsesson_idを発行する
- session_idをキーとしてサーバー側のDBなどにユーザーの状態:カートの情報を保存する準備をする
- 同時に、このsesson_idをCookieとしてブラウザに送る
2.2回目のアクセス
- ブラウザは、自動的にCookieに保存されたsession_idをリクエストにもたせて送信する
- サーバーはそのsesson_idを使って、DBから対応するカート情報を取り出す
- カート情報があればそれを画面に表示する
こういった流れによって本来は、ステートレスなHTTP/REST APIの世界でも、
このリクエストはsesson_id=aeta の人の続きだな、じゃあこの人のカートに入っている情報を表示しよう
というように、ユーザーごとに状態を持った処理(ステートフルな動き)を実現することができます。
カート機能とセッションの関係
カート機能では、この session_id を使って
-
Cartモデルにsession_idを保存しておく - リクエストから取り出した
session_idと一致するCartを取得する - 見つかった
Cartに紐づいているCartItemを使って、カート画面を表示する
という形で、
「ユーザーごとに別々のカートを持たせる」
ことができます。
Django では、request.session.session_key と sessionid Cookie を使って、
この仕組みをかなりいい感じにラップしてくれているので、
実際の実装では 「どうセッションIDを発行して、Cart と紐づけるか」 を押さえておくのがポイントになります。
モデルを作成
class CartItem(models.Model):
class Meta:
db_table = "cart_items"
constraints = [
models.UniqueConstraint(
fields=["cart", "product"],
name="uniq_cart_product",
)
]
cart = models.ForeignKey(Cart, verbose_name="カート", on_delete=models.CASCADE)
product = models.ForeignKey(Product, verbose_name="商品", on_delete=models.CASCADE)
quantity = models.IntegerField("個数", default=0)
UniqueConstraint で cart + product をユニークにしてる理由
結論:1つのカートに同じ商品業を1レコードにしたいから。
UniqueConstraint(fields=["cart", "product"])をつける理由は、1つの中に同じ商品が複数行で重複して存在しないようにするため
です
例えば、UniqueConstraintをつけないと同じカートに同じ商品が下記のように入る可能性があります
| id | cart_id | product_id | quantity |
|---|---|---|---|
| 1 | 1 | 5 | 1 |
| 2 | 1 | 5 | 2 |
このようになると、特定のアイテムの個数を出すときにquantityを合計しないといけなくなったり、商品がカートに入っているかを調べる際もfilter(cart=cart, product=product)で複数返る可能性があります
となるとバグが発生しやすくなります。
UniqueConstraintをつけることで
cart + product をユニークにしておくと、
(この cart, この product) の組み合わせは1レコードしか持てない
という制約になるため、
| id | cart_id | product_id | quantity |
|---|---|---|---|
| 1 | 1 | 5 | 3 |
この1行にまとめる前提の設計にすることができます。
これによって
「カートに同じ商品を追加する」処理では quantity を更新するだけ
「商品がカートに入っているか」を get_or_create などでシンプルに扱える
といったメリットが得られます。
セッションを使ってカートを区別する処理
先ほど書いた通り、session_id はユーザーごとのリクエストを区別するためのIDです。
Django では、この情報は request オブジェクトの中に入っています。
requestとは
request は、Django がクライアント(ブラウザなど)から受け取った情報をまとめたオブジェクトです。
言い換えると、「ユーザーのリクエスト内容」がすべて詰まっている箱のようなものです。
よく使う属性としては例えば次のようなものがあります。
-
request.method: リクエストの種類("GET" / "POST" など) -
request.GET: URLクエリのデータ(例: /product?id=3 の id=3) -
request.POST: フォーム送信(POST)のデータ -
request.session: セッション情報(ユーザーごとの一時データ) -
request.user: ログイン中のユーザー情報 -
request.FILES: ファイルアップロードのデータ
request.session の中に session_key が入っているので、これを元に
「カートごとに個別のセッションIDを作成・取得する」 処理を書いていきます。
ただし、毎回すべての処理の中に「セッションIDがなければ作る」というコードを書くのは大変なので、
utils.py を作成し、その中にセッションIDを用意する関数を定義します。
# セッションIDを使ってカートを管理する関数
def _ensure_cart_session(request):
if request.session.session_key is None:
request.session.create()
return request.session.session_key
意味としては、
もし request の中に session_key がなければ、新しく session_key を作成する
そのうえで、現在有効な session_key を返す
という関数です。
さいごに
ここまでで、
- HTTP / REST API がステートレスであること
- セッションと session_id / Cookie の役割
- Cart / CartItem モデルと UniqueConstraint の設計意図
- request.session.session_key を使ってセッションIDを扱う方法
といった、カート機能の土台となる仕組みを整理することができました。
これで、セッション機能を使う準備ができたので
次回は、この session_key を使って
- カートに商品を追加する処理
- 既存の商品の数量を更新する処理
- カートから商品を削除する処理
など、実際のカート更新処理についてまとめていきたいと思います。
↓まとめました。