はじめに
前回の記事モデルの設計編では、
- 決済情報を保存する
Checkoutモデル - クレジットカード情報を保存する
Paymentモデル - 「どの商品を何個買ったか」を保存する
LineItemモデル - それらに対応する
OrderForm(フォーム)
を作り、テーブル設計とフォームのバリデーションを用意しました。
この記事では、その続きとして、
FormView を使って実際に DB に保存する処理(決済処理の本体)を実装していきます。
やりたいことの全体像
決済ボタンが押されたときに、次の流れで処理を行うよう実装しました。
- フォームから送信された入力値(名前・住所・カード情報など)を受け取る
- セッションIDから現在のカートを取得する
- カートから合計金額・合計個数・カートアイテム一覧を取得する
-
Checkoutに1回の注文情報を保存する -
Paymentにその注文のカード情報を保存する -
LineItemにそのときのカートの中身を明細として保存する - カートを空にして再度買い物をできるようにする
この一連の処理を、Order という FormView を継承したクラスにまとめて書きました。
FormViewを継承して決済処理を書いていく
まずは、完成形の Order ビューを見てみます。
from django.views.generic import FormView
from django.urls import reverse_lazy
from django.db import transaction
from .models import Checkout, Payment, LineItem
from checkout.models import Cart
from .forms import OrderForm
from checkout.utils import _ensure_cart_session
# DBに請求情報とクレジットカード情報を保存するView
class Order(FormView):
template_name = "checkout/checkout.html"
form_class = OrderForm
success_url = reverse_lazy("product:product_list")
# トランザクション処理にする
@transaction.atomic
def form_valid(self, form):
# form_validは引数にrequestを取らないので、selfから取得する
session_key = _ensure_cart_session(self.request)
cart_obj = Cart.objects.get(session_id=session_key)
# 合計金額・合計数量・カート内の商品一覧を取得する
total_amount = cart_obj.calculate_total_price()
total_quantity = cart_obj.calculate_total_quantity()
cart_items = cart_obj.get_items()
# formsで定義したバリデーションを通過したデータを取得する
data = form.cleaned_data
# 1. フォームから請求情報を取得してCheckoutに保存
checkout = Checkout.objects.create(
last_name=data["last_name"],
first_name=data["first_name"],
user_name=data["user_name"],
email=data["email"],
zip_code=data["zip_code"],
prefecture=data["prefecture"],
city=data["city"],
street_address=data["street_address"],
building_name=data["building_name"],
total_amount=total_amount,
total_quantity=total_quantity,
)
# 2. 決済情報をPaymentモデルに保存
Payment.objects.create(
checkout=checkout,
card_holder=data["card_holder"],
card_number=data["card_number"], # 本来はDBに保存しない
expiration_date=data["expiration_date"],
cvv=data["cvv"], # 本来はDBに保存しない
)
# 3. カート内の商品情報をLineItemに明細として保存
for item in cart_items:
LineItem.objects.create(
checkout=checkout,
name=item.product.name,
price=item.product.price,
quantity=item.quantity,
subtotal_amount=item.product.price * item.quantity,
)
# 4. カートを削除(中身を空にする)
cart_obj.clear()
# 最後に親クラスのform_validを呼び出す(success_urlへリダイレクト)
return super().form_valid(form)
ここから、FormViewの基本部分 → トランザクション → カート処理の順番で解説していきます。
FormViewの基本設定
まずはクラスの上の方から見ていきます。
class Order(FormView):
template_name = "checkout/checkout.html"
form_class = OrderForm
success_url = reverse_lazy("product:product_list")
template_name = "checkout/checkout.html"
- 決済ページで使うテンプレートファイルを指定します。
- 今回は
checkout/templates/checkout/checkout.htmlを使う想定です。
form_class = OrderForm
- どのフォームクラスを使うかを指定します。
- 設計編で作った
OrderForm(名前・住所・カード情報などが入っているやつ)をここで紐づけています。
success_url = reverse_lazy("product:product_list")
- フォームの処理(
form_valid)が正常に終わったあとにリダイレクトするURLを指定します。 -
reverse_lazy("product:product_list")は
productアプリのurls.pyでname="product_list"とつけたURLに変換してくれます。
return super().form_valid(form)
form_valid の最後で呼んでいるこの一文は、
return super().form_valid(form)
- 「フォームの処理が正常に終わりました」と親クラス(FormView)に教え、その結果として、
success_urlで指定したURLにリダイレクトされる
という役割を持っています。
トランザクション処理とは?
@transaction.atomic
def form_valid(self, form):
...
この @transaction.atomic が、いわゆるトランザクション処理です。
何のために使うのか?
今回の決済処理では、1回のフォーム送信の中で、
-
Checkoutを作成(1回の注文) -
Paymentを作成(カード情報) -
LineItemを複数作成(注文の中身) - 最後にカートを削除
という、複数のDB操作を行っています。
ここで問題なのは、
途中までDBに保存されたのに、途中でエラーになって処理が止まると中途半端な状態になってしまう
ということです。
たとえば:
-
Checkoutまでは保存された - でも
LineItemの保存中にエラーが出て止まった - カートだけ消えて、注文の中身が一部しか保存されていない
みたいな状態は絶対に避けたいです。
transaction.atomic の動き
@transaction.atomic をつけると、
- 中の処理が全部成功したときだけ まとめてDBに反映されるようになり、途中で例外(エラー)が発生した場合は、その処理の中で行ったDB操作をなかったことにして元に戻すロールバックという処理が行われます。
つまり、
「全部ちゃんとできた」か「何もやっていない」のどちらかに必ずなる
ようにしてくれる仕組みです。
そのおかげで、ユーザーの側から見ると、
- ちゃんと注文できた状態
- 何も注文されていない状態
のどちらかしか存在しなくなり、
半端なDB状態が残らずに安全な決済処理 が実現できます。
form_valid の中身を順番に追う
1. カート情報を取得する
session_key = _ensure_cart_session(self.request)
cart_obj = Cart.objects.get(session_id=session_key)
-
form_validはrequestを引数に取りませんが、
クラスベースビューではself.requestにリクエストが入っています。 -
_ensure_cart_session(self.request)は
「セッションIDがなければ作る/あればそれを返す」というヘルパー関数です。 - そのセッションIDを使って、
Cartモデルからカートを1件取得しています。
2. 合計金額・合計個数・カートアイテムを取得する
total_amount = cart_obj.calculate_total_price()
total_quantity = cart_obj.calculate_total_quantity()
cart_items = cart_obj.get_items()
これらは、Cart モデルに定義したモデルメソッドを呼び出しています。
後で、Cart モデル側のコードも載せます。
ここでやっていることはシンプルで、
-
get_items()でカートに入っているCartItem一覧を取ってくる - その結果を使って合計金額・合計数量を計算している
という流れです。
3. フォームのバリデーション済みデータを取得
data = form.cleaned_data
-
OrderFormに定義したバリデーション(必須チェック・文字数チェックなど)を通過したデータだけがcleaned_dataに入ります。 -
data["last_name"]のように、辞書のような形で取り出して使えます。
4. Checkout(1回分の注文)を保存
checkout = Checkout.objects.create(
last_name=data["last_name"],
first_name=data["first_name"],
user_name=data["user_name"],
email=data["email"],
zip_code=data["zip_code"],
prefecture=data["prefecture"],
city=data["city"],
street_address=data["street_address"],
building_name=data["building_name"],
total_amount=total_amount,
total_quantity=total_quantity,
)
ここでは、フォームから受け取った名前・住所と、
カートから計算した合計金額・合計数量をまとめて Checkout に保存しています。
1件の Checkout が、「1回分の注文のまとまり」を表すイメージです。
5. Payment(カード情報)を保存
Payment.objects.create(
checkout=checkout,
card_holder=data["card_holder"],
card_number=data["card_number"], # 本来はDBに保存しない
expiration_date=data["expiration_date"],
cvv=data["cvv"], # 本来はDBに保存しない
)
-
checkout=checkoutで、「どの注文のカード情報か」を紐づけています。 - 繰り返しになりますが、実際のサービスではカード番号やCVVを平文でDBに保存するのはNG です。
(Stripeなどの外部決済サービスを使うべき部分)
ここではあくまで、「Djangoでどうやって紐づいたレコードを保存するか」を学ぶためのサンプルとして入れています。
6. LineItem(注文の明細)をカートから作成
for item in cart_items:
LineItem.objects.create(
checkout=checkout,
name=item.product.name,
price=item.product.price,
quantity=item.quantity,
subtotal_amount=item.product.price * item.quantity,
)
- カート内の
cart_itemsをループしながら、
各商品ごとにLineItemを1レコードずつ作成しています。ここでは、nameやpriceをProductからコピーしていきます。
これは、設計編で説明した通り、
「あとで商品名や価格を変更しても、注文したときの情報は変わらないようにしたい」
からです。
LineItem はあくまで「注文時点のスナップショット」という役割になります。
7. カートを空にする
cart_obj.clear()
カートモデルの clear メソッドを呼び出して、カートを削除しています。
これにより、同じセッションで再度商品を追加したときは新しいカートとして扱われます。
Cartモデルのメソッド
最後に、この記事で使っている Cart モデルのメソッドも載せておきます。
class Cart(models.Model):
class Meta:
db_table = "cart"
session_id = models.CharField(max_length=40, null=True, blank=True, unique=True)
# このCartに紐づいているCartItemを、Product情報も一緒に全部取ってくる
def get_items(self):
return self.cartitem_set.select_related("product").all()
# カート内の合計数量を計算するメソッド
def calculate_total_quantity(self):
total_quantity = sum(item.quantity for item in self.get_items())
return total_quantity if total_quantity is not None else 0
# カートの合計金額を計算するメソッド
def calculate_total_price(self):
total_amount = sum(
item.quantity * item.product.price for item in self.get_items()
)
return total_amount if total_amount is not None else 0
# カート自体を削除するメソッド
def clear(self):
self.delete()
-
get_items()
このカートに紐づくCartItemを、productも一緒に取得します。 -
calculate_total_quantity()
すべてのCartItemのquantityを足し合わせます。 -
calculate_total_price()
quantity × product.priceの合計を計算します。 -
clear()
このCartレコードを削除(ForeignKeyで紐づいているCartItemも一緒に消える想定)します。
こうしておくことで、Order View からは
cart_obj.calculate_total_price()
cart_obj.calculate_total_quantity()
cart_obj.get_items()
cart_obj.clear()
のように、「カートに関する処理」を全部 Cart モデルに任せて書けるようになっています。
さいごに
今回は、
- FormView を使って決済処理用の
Orderビューを作る方法 -
form_validの中で Checkout / Payment / LineItem を保存する流れ - その際にトランザクション(
@transaction.atomic)で「全部成功 or 全部なかったこと」にする考え方 - Cart モデルのメソッドを使って、合計金額・合計数量・カートアイテムを取得する方法
をまとめました。
これで、
「フォーム送信 → 決済情報をDBに保存 → カートを空にする」
という決済機能の核の処理を完成させることができました。
次の発展編では、
- 購入完了メールを送る処理
- Djangoの
messagesフレームワークを使って「購入ありがとうございます」を画面に表示する処理
などを追加して、より実践的な決済機能に強化していく予定です。