Archive

中古オークションアプリの取引設計をどう整理したか

items・orders・messages を中心に、favorites・addresses・role まで含めて、実際の利用フローに沿う DB 設計をどう考えたかを整理した記事です。

このアプリを作る中で、特に時間をかけて考えたのが、取引まわりのデータベース設計でした。 画面づくりを進めていると、 一覧を出す、 詳細を出す、 ボタンを押したら次の画面へ進む、 といった流れは比較的早く形になります。 ただ、中古オークションや中古取引のようなアプリでは、 見た目ができても、裏側の構造が整理されていないとすぐに苦しくなります。 たとえば、 誰が出品したのか、 誰が購入したのか、 取引は今どの状態なのか、 会話はどこに紐づくのか、 配送先はどう持つのか、 管理者はどこまで触れるのか。 こうしたことを後から足そうとすると、 最初の設計が曖昧なままでは全体が崩れやすくなります。 そのため今回は、 「画面に必要な情報」から考えるのではなく、 「実際の利用フローを無理なく表現できるか」を基準に DB 設計を考えました。 この設計の中心にあるのは items と orders です。 そして今回一番意識したのは、 この二つを同じものとして扱わないことでした。 items は、出品されている商品そのものを表します。 タイトル、説明、価格、カテゴリー、商品の状態、現在の出品状況など、 商品一覧や詳細画面に必要な情報を持つテーブルです。 つまり items は、「何が出品されているか」を表す役割を持っています。 一方で orders は、「その商品に対してどんな取引が始まっているか」を表します。 buyer_id、seller_id、status、shipping_address などを持つことで、 誰と誰の間で取引が進んでいるのか、 今どの段階にあるのかを表現できるようにしています。 この二つを分けた理由はシンプルです。 商品の情報と、取引の情報は、似ているようで責務が違うからです。 もし items の中に購入者情報や配送先、取引状況まで全部入れてしまうと、 「出品中の商品の情報」と 「進行中の取引の情報」が同じ場所に混ざります。 そうなると、 一覧画面で必要な情報と、 購入後の進行管理に必要な情報と、 管理画面で確認したい情報が一つのテーブルに集中してしまい、 後から扱いづらくなります。 今回はそこを避けるために、 items は出品の単位、 orders は取引の単位、 という考え方で分けました。 この分離をしておくと、 商品を見せるための設計と、 取引を進めるための設計を分けて考えられます。 実装中もこの違いはかなり大きく、 どの情報が商品に属するのか、 どの情報が取引に属するのかを整理しやすくなりました。 messages を orders に紐づけたのも、この考え方の延長です。 メッセージ機能を考えるとき、 最初は item に紐づける形も考えられます。 ただ実際には、会話は「商品そのもの」に対して発生するというより、 「進行している取引」の中で発生することのほうが自然です。 たとえば購入後のやり取りでは、 発送時期、 配送先の確認、 受け取りに関する連絡など、 取引の文脈が前提になります。 そのため今回は、 messages は item ではなく order に紐づけました。 これによって、 どの取引に関する会話なのかが明確になり、 会話履歴もかなり自然に追えるようになります。 さらに user_id も持たせることで、 誰の発言なのかも明確にできます。 この設計の良かった点は、 「会話」という機能を独立した便利機能として置くのではなく、 取引フローの一部として扱えることでした。 中古取引のアプリでは、 メッセージは単なるチャットではなく、 取引進行に密接に関わる要素です。 だからこそ、注文に紐づける形のほうが構造として自然だと考えました。 favorites を別テーブルにしたのも、 役割を混ぜないためです。 お気に入り機能は一見小さく見えますが、 実際にはユーザーが「気になった商品を一時的に残す」という行動を表しています。 これは商品そのものの属性ではなく、 ユーザーと商品の関係です。 そのため、 favorites は user_id と item_id を持つ独立したテーブルにしました。 こうしておくことで、 商品の基本情報と、 ユーザーの関心情報を分けて扱えます。 この分離には実装面でも利点がありました。 お気に入り済み判定、 件数の集計、 一覧での表示切り替えなどを考えたとき、 商品本体のデータへ無理に混ぜ込まずに済むからです。 今はシンプルな使い方でも、 将来的に行動分析やレコメンドに広げたい場合にも、 独立しているほうが扱いやすくなります。 addresses を users から分けたのも、 実際の利用を意識した判断でした。 単純に作るなら、users テーブルに住所を直接持たせることもできます。 ただ、それだと一人のユーザーが一つの住所しか持てません。 実際には、 自宅、 実家、 職場など、 複数の配送先を使い分けたいことがあります。 そこで今回は addresses を独立したテーブルにし、 user_id に紐づける形にしました。 この形なら、 住所を追加する、 既存の住所を編集する、 複数の中から選ぶ、 といった機能にも素直につなげられます。 こうした部分は、初期段階では省略したくなりやすいです。 ただ、後から実際の使い方に合わせて広げようとすると、 最初から分けておいたほうが結果的に楽だと感じました。 認証と権限については、 users に role を持たせています。 ここで意識したのは、 「ログインできること」と 「何ができるか」は別の話だということです。 一般ユーザーと管理者では、 見られる画面も、 操作できる対象も変わります。 商品を管理する、 ユーザーを確認する、 記事を管理するなど、 運用側の機能を考えると、 認証だけでは足りません。 そのため、 早い段階で role を持たせておくことで、 後から管理機能を足しやすい土台を作りました。 これは c群の中でもかなり重要なポイントでした。 なぜなら、取引設計は利用者向けの流れだけで完結しないからです。 実際には、運用する側の視点も必要になります。 つまり今回の DB 設計では、 「ユーザーがどう使うか」と 「管理側がどう扱うか」の両方を意識していました。 ここまでの設計をまとめると、 このアプリのスキーマでは次のように役割を分けています。 items は出品物そのもの。 orders は取引の進行。 messages は取引内の会話。 favorites はユーザーの関心。 addresses は配送先。 users は認証と権限の起点。 このように整理しておくことで、 どの情報をどこが持つべきかがかなり明確になります。 そして、機能が増えたときも、 まずどの責務の話なのかを考えやすくなります。 実際、開発を進める中で感じたのは、 DB 設計が整理されていると、 その後の API 設計や画面設計もかなり楽になるということでした。 逆に、最初に責務を混ぜてしまうと、 画面が増えるたびにモデルの扱いが苦しくなります。 個人開発では、 まず動くことが大事です。 ただ、中古取引のように複数の行動が連続するアプリでは、 何がどこに紐づくのかを最初に整理しておくことが本当に重要だと感じました。 今回の設計で一番大事にしたのは、 テーブル数を減らすことではありません。 それよりも、 責務を混ぜないこと、 実際の利用フローに沿うこと、 後から機能を足しても破綻しにくいことを優先しました。 このアプリの取引設計を通して学んだのは、 DB は単なる保存場所ではなく、 アプリの考え方そのものを表す土台だということです。 どの情報をどこへ置くかには、 そのまま設計の姿勢が出ます。 今回は、 items と orders を分けることを軸にしながら、 messages、favorites、addresses、role まで含めて、 実際の取引フローを自然に支えられる形を目指しました。 見えにくい部分ではありますが、 この DB 設計こそが、アプリ全体の一貫性を支える重要な基盤になっていると考えています。