読者です 読者をやめる 読者になる 読者になる

Rails を使った EC アプリケーション開発について学べる本 "Take My Money" を読んだ

読みました。

pragprog.com

どんな本か

副題が “Accepting Payments on the Web” となっているように、決済 (payment) システムをもつ Web アプリケーションを作る方法について説明しています。『達人プログラマー』などでおなじみの The Pragmatic Bookshelf シリーズの本です。

チケット販売システムの開発を通して、次のような具体的な話題に触れています。基本的には Rails 5 を使ってロジックからビューまでを開発していきます*1

  • 決済システムの実装
    • ショッピングカート
    • 外部決済サービスとの連携
    • サブスクリプション機能
    • エラーケースとその対策
  • 管理画面の実装
    • 返金など注文の操作
    • 認証/認可
  • その他実務上必要な項目
    • 監査用ログの保存
    • PCI DSS への準拠

なぜ読んだか

この本の発売当時(2017-01 ぐらい)に社の Slack で紹介されていたのを見かけて、その存在を知りました。半年ぐらい前から EC 系サービスの開発に携わっていて、その分野に関して一般的な知見をあらためて得たいなと思い、読むことにしました。

全体を読むのにかけた時間は 2 週間ぐらいです。pragprg.com のサポートページに置いてあるサンプルコードを見つつ、実際に動かしたりしながら読んでいました。

どうだったか

おさえている話題の幅が広かったです。EC 系のアプリケーションに必要なデータのモデリングや Rails で開発するうえでの実装の工夫から各種定番の gem についてまで、ひととおり説明がありました。自分が携わっているサービスとの共通点/相違点を認識しながら、こういうやりかたもあるのかという発見や知識の整理ができました。

紹介されている外部決済サービスや gem の使いかたの説明は風化が速い部分になってしまうとは思いますが、データモデリングの部分などはそれなりに長く通用する有用な具体例を示してくれているように感じます。

個人的に有用だと思った点をまとめておきます。

データのモデリング

商品在庫

この本では商品在庫をカウンタカラムで管理するのではなく、1 件ずつレコードを作るという方法がよいと述べていました。つまり、次のテーブル定義のような在庫用カウンタ tickets_count をもつものではなく、

# 採用しない例
create_table "tickets" do |t|
  # ...
  t.integer "tickets_count"
  t.integer "price_cents", default: 0, null: false
end

在庫ごとにレコードを作り、status カラムに enum で unsold, sold といったデータを持たせる次のような定義を採用していました。

# 採用する例
create_table "tickets" do |t|
  # ...
  t.integer "status"
  t.integer "price_cents", default: 0, null: false
end

これについては、開発当初はカウンタを持たせるのが簡単だが、だんだんと

  • 在庫数
  • カートに入っている商品数
  • 売上数

…のようにカウンタの種類が増えていって管理がつらくなる、という理由があるようです。代わりに、在庫ごとにレコードを作るのが商品グループを操作するうえではより簡単な方法だと述べています。

返金

返金のモデリングについては、「返金のおかげでこの本を書こうと思ったぐらい返金はだるい」(意訳)と述べていることから、重要かつ工夫のいる部分であることがわかります。まず、この本で開発するアプリケーションでは、決済したときに次のようなテーブルのレコードを作成しています。

create_table "payments" do |t|
  # ...
  t.integer "user_id"
  t.integer "price_cents", default: 0, null: false
  t.integer "status"
  t.string  "payment_method"
  t.json    "full_response"
end

返金のモデリングでは、次の 2 とおりの方法

  • payments テーブルに返金額を保存するカラムを追加する方法
  • 返金データとして price_cents が負の値の payments レコードを作成する方法

を選ぶ余地があります。

  • これまでに作成したレコードは不変のものとしたい
  • 決済処理時のペイメントゲートウェイからの JSON レスポンスを各レコードに保存しておきたい

と言った理由から、この本では返金データも 1 件の payment レコードとする方法をとっています。また、返金を表すレコードのために、返金の対象となった元の決済レコードへの参照用カラムを持たせています。

change_table "payments" do |t|
  t.references :original_payment, index: true
end

これで、複数回の一部返金にも対応できるようになります。これは、ER 図を描くと自分自身へのループ参照で表現されるようなイメージです。

実装上の手法

workflow による薄いコントローラ

コントローラにロジックを書かないために、workflow と称したクラスを app/workflows に切り出してロジックを分離する、という方法が紹介されています。

# app/controllers/shopping_carts_controller.rb
class ShppingCartsController < ApplicationController
  def update
    # performance はある映画のある時間における上映
    workflow = AddsToCart.new(user: current_user, performance: performance, count: params[:ticket_count])
    workflow.run # workflow に実際のロジックを委譲する
    # ...
  end
end

# app/workflows/adds_to_cart.rb
class AddsToCart
  # initialize など...
  def run
    # 実際にカートへ商品を追加するロジックを書く
  end
end

調べてみると Trailblazer の Operation も同じような発想に基づいているようです。

複数の決済方法

EC サービスを開発していると、決済方法は徐々に増えるものです。この本では、最初は Stripe だけを決済サービスとして利用していますが、途中で PayPal を追加します。このように決済方法が増えてきたときの対処として、上で説明した workflow を用いて解決しています。具体的には、抽象的な決済用 workflow を用意し、テンプレートメソッドでそれぞれの決済サービス用の workflow を実装しています。

# app/workflows/purchases_cart.rb
class PurchasesCart
  # ...
  def run
    update_tichets
    create_payment
    purchase
    calculate_success
  end
end

# app/workflows/purchases_cart_via_stripe.rb
class PurchasesCartViaStripe < PurchasesCart
  def purchase
    # Stripe の Web API クライアントを使って決済
  end
end

# app/workflows/purchases_cart_via_pay_pal.rb
class PurchasesCartViaPayPal < PurchasesCart
  def purchase
    # PayPal の Web API クライアントを使って決済
  end
end

そして、これらの workflow を作成するファクトリはメソッド create_workflow に切り出していました。次のような感じで使います。

class PaymentsController < ApplicationController
  # ...
  def create
    workflow = create_workflow(params[:payment_type]) # ビューからの payment_type で具象 workflow 作成
    workflow.run
    # ...
  end

  private

  def create_workflow(payment_type)
    case payment_type
    when "paypal"
      PurchasesCartViaPayPal.new( # 引数
      )
    else
      PurchasesCartViaStripe.new( # 引数
      )
    end
  end
end

定番の gem

アプリケーションを作るうえで必要になる定番の gem がいろいろと紹介されていました。次の gem は知らなかったので参考になりました。

  • money-rails
    • 金額計算や通貨変換に関する API を提供する money という gem を Rails へ統合する
  • Administrate
    • Thoughtbot 謹製の管理画面作成フレームワーク
    • 本の中では ActiveAdmin が使われているものの、こちらについても軽く言及があった
  • Pundit
    • Policy クラスでコントローラアクションに認可機構をかけられる
  • PaperTrail
    • ActiveRecord モデルのデータの変更を追跡してバージョン管理する
  • bundler-audit
    • アプリケーションに導入している gem のうち Gemfile.lock に書いているバージョンのものに脆弱性が報告されているかチェックする

おわりに

EC アプリケーションというドメインに絞って具体的な開発方法が説明されているニッチな本だと思いますが、個人的には参考になる部分がそれなりにあってよかったです。上に述べたような話が気になる人は読んでみてください。

*1:「クライアントサイド、サーバサイド両方 Rails 5 を使って開発しています」と書いていましたが、表現がよくないので修正しました

RFC 5988 "Web Linking" を読んだ

JSON を返す API サーバでページネーションを実装したいと思っていて、前/後ページや最初/最後のページなどといった他のリソースとの関係を表すメタ情報をどこに格納すべきかなと考えていました。解決方法として少なくとも次の二つがありそうです。

  • JSON の中(レスポンスのボディ)にメタ情報も入れる
  • レスポンスヘッダにメタ情報を入れる

レスポンスヘッダに当該メタ情報を入れる後者の方法について提案している文書としては、RFC 5988 “Web Linking” があります。この方法は大きなサイズのコレクションをページネーションで返す API のレスポンスを設計するときにも使えるという話を見つけたので、メタ情報はボディよりヘッダに入れたほうが場所としては適切だろうと思い、ページネーションを実装する前に、この RFC を読んでみました。

RFC 5988 の要点をハイパーざっくりまとめておきます。


イントロ

  • なにをやるのか
    • 特定の形式や応用例に縛られない型付きリンクの表現を定義する
  • なぜやるのか
    • リソース間の関係とその型を示す方法は HTML と Atom で別々に定義しているけど一般化できる
    • HTTP ヘッダでリンクを定義する方法が RFC 2068 で定義されてたけど RFC 2616 で削除されてしまった
    • 放っておくと各アプリケーション特有のやり方が生まれちゃってつらそうなので、この RFC ではそれを解決するよ

リンクとは

  • リンクは「IRI*1 で識別されるリソース間の型付き接続」として定義する
    • 基本的に IRI は URI と読み替えて OK
  • リンクは次の要素からなる
    • 一つのコンテキスト IRI
    • 一つのリンク関連型 (link relation type)
    • 一つのターゲット IRI
    • 任意個数のターゲット属性
  • <コンテキスト IRI> は <リンク関連型> のリソースを <ターゲット IRI> で持ち、そのリソースは <ターゲット属性> を持つ という文章として見ることができる
  • どんな型のリンクもどの IRI から何本出ていてもいいし、順番も気にしない
  • ターゲット属性は key-value ペア

リンク関連型

  • リンク関連型はリンクの意味を決める
    • copyright という関連型のリンクは、ターゲット IRI が指すリソースはコンテキスト IRI へ適用する著作権規定の文章であることを示している
  • リンク関連型はターゲットリソースが特定の属性を持つことも示す
    • service というリンクは、「サービスの説明」のようにリソースが何か定義されたプロトコルの一部であることを示している
  • メディアタイプではないので、リンクの参照先の表現形式は問わない
  • 他のリンク関連型の存在や出現数に依存して意味を付け加わるような関連型は望ましくない
    • alternatestylesheet は歴史的事情により例外

関連型の種類

  • 登録関連型 (Registered Relation Types)
    • IANA レジストリに登録済みの再利用できる関連型
    • 名前は reg-rel-type rule にしたがう
      • reg-rel-type = LOALPHA *( LOALPHA | DIGIT | "." | "-" ) と定義されている
      • つまり ASCII 小文字 "a".."z" 1 文字の後に任意個数の ASCII 小文字、数字、".", "-" が続く形式
  • 拡張関連型 (Extension Relation Types)
    • 関連型を一意に識別できる URI を使うこともできる
    • URIは関連型を定義しているものを指し示す必要があるけど、サーバに負荷がかかるのでクライアントは勝手にそのリソースへアクセスすべきではない

レジストリに最初に登録された関連は該当 RFC 6.2.2 節に載っている。

現在のレジストリ登録済み関連は IANA のページに載っている。結構増えてますね。

Link ヘッダフィールド

  • エンティティのヘッダ上で 1 本以上のリンクを表現する
  • HTML の <LINK> 要素とか Atom の atom:link 要素と等価
  • ターゲット IRI
    • ブラケットで囲む
    • 相対パスならパーサはパス解決する必要がある
  • コンテキスト IRI
    • デフォルトではリクエストされているリソース自体がコンテキスト IRI が指すリソース
    • 404 で該当リソースが存在しないときは anonymous になる
  • 関連型
    • rel パラメータで関連型の値を指定する
    • revrel の逆向きになっている関連であり、歴史的事情で存在しているが、もう使わない
  • ターゲット属性
    • hreflang は参照先の言語のヒント
    • media は参照先のメディアスタイル(画面表示、読み上げ、etc.)のヒント
    • title, title* は参照先リソースのタイトルで、title* は別の文字セットを使うことができる
    • type は参照先のメディアタイプのヒント

例を示す。

“chapter2” が現在のリソースの前に位置していることを示すリンク。

Link: <http://example.com/TheBook/chapter2>; rel="previous"; title="previous chapter"

ルートリソース (“/”) が拡張関連型 http://example.net/foo でこのリソースと関連していることを示すリンク。

Link: </>; rel="http://example.net/foo"

title* 属性でドイツ語で「前の章」、「次の章」というタイトルを指定しているリンク。

Link: </TheBook/chapter2>; rel="previous"; title*=UTF-8'de'letztes%20Kapitel,
      </TheBook/chapter4>; rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel

複数のリンクも持てる。

Link: <http://example.org/>; rel="start http://example.net/relation/other"

結局、ページネーションでは、Link Relations あたりに載っているリンク関連型から、first, last, next, previous を使ってリンク先を示すようなメタ情報をヘッダに入れておけば十分そうです。

参考資料

MySQL 徹底入門 第 3 版を読んだ

『MySQL 徹底入門 第 3 版』を読みました。

MySQL徹底入門 第3版 ~5.5新機能対応~

MySQL徹底入門 第3版 ~5.5新機能対応~

  • 作者: 遠藤俊裕,坂井恵,館山聖司,鶴長鎮一,とみたまさひろ,班石悦夫,松信嘉範
  • 出版社/メーカー: 翔泳社
  • 発売日: 2011/08/26
  • メディア: 大型本
  • 購入: 9人 クリック: 82回
  • この商品を含むブログ (9件) を見る

なぜ読んだか

  • 特定の DB プロダクトについての知識がなかった
  • Web サービス開発の仕事に転職して、開発のために MySQL の操作をするようになったけど、知識が少ないので時間がかかる

こういうとき、幅広く薄く体系的にとっかかりとなる知識があると、頭のなかにインデックスができて調べやすくなるかなと考えました。入門系の本を何冊か Amazon で見て、よさそうなのを選んだ記憶があります。2011 年初版で「5.5 対応」と書いてあり、それほど新しい本ではないけれど、基本的な機能の学習には影響がそれほどないだろうと判断しました。

Vagrant で CentOS 7 の VM を立ち上げて MySQL をインストールし、それを触りながら読んでました。

感想

3 章でクライアント ( mysql ) を通じた操作の基本がだいたいわかり、4 章で周辺ツール( mysqladmin, mysqldump など)やユーザ管理、サーバ設定、ログなどについての基本がだいたいわかりました。このへんは求めていたことだったのでよかったです。

ストレージエンジンの説明やコマンドのオプションも載っていますが、わりとさらっとしてるので、このあたりは素直にマニュアルを読もうと思いました。

おわりに

MySQL の基本的な使いかたを体系的に把握することはできた本でした。あとは公式のリファレンスマニュアルを参照する生活を過ごしたいと思います。

Web 上のリソースとその表現

RSpec で request spec を書くとき、get "/users/:id.json" と書くかわりに get "/users/:id" と書くとエラーになりました。

ActionController::UnknownFormat:
  UsersController#show is missing a template for this request format and variant.

リソース /users/:id に対して拡張子で指定しないならば、HTTP リクエストのヘッダに Accept: application/json をつけて、クライアントが利用したいデータ形式を指示する必要があります。

そもそもなぜこういうふうになっているのかを整理しました。

リソース

リソースとは Web 上に存在する情報そのものであり、URI (Uniform Resource Indicator) で指し示せます。たとえば次のようなリソースが考えられます。

  • 人のプロフィール
    • URI 例:http://example.com/users/john
  • ブログ記事
    • URI 例:http://example.com/entries/2017/01/01

わかりやすい URI でリソースを指定することで、どのような情報なのかが人間にとってわかりやすくなります。なお、ここでは、その情報がどのような見た目(表現)になっているかについては言及していません。

また、リソースに対して、HTTP のメソッド (GET, POST, PUT, PATCH, DELETE) を使うことで CRUD をはじめとする各種操作が実行できます。

リソースの「表現」

「表現」という言葉はリソースがどのような形式のデータになって、サーバ/クライアント間で通信されるかを指しています。表現には次のようなものがあります。

  • メディアタイプ
    • HTML, XML, JSON, JPEG, PDF など
  • 言語
    • 日本語、英語、中国語など
  • 文字エンコーディング
    • UTF-8, Shift_JIS など

リソースは複数の「表現」をとれる

あるリソースは次のように複数の「表現」をとりえます。

  • 人のプロフィールが HTML, JSON, PDF で表現できる
  • ブログ記事が日本語、英語で表現できる

リソース表現の指定方法

HTTP を介したサーバとのやりとりにおいて、リソースの表現をどう指定するかについては次のような方法があります。

  • クエリ文字列
    • GET http://example.com/users/john?format=json で人のプロフィールの JSON 表現を得られる
  • 拡張子
    • GET http://example.com/entries/2017/01/01.en でブログ記事の英語表現を得られる
  • コンテントネゴシエーション
    • GET http://example.com/users/john のリクエストヘッダに Accept: application/pdf を利用するとプロフィールの PDF 表現を得られる
    • ほかには Accept-LanguageAccept-Charset など

リソースと表現を分離する利点

リソースと表現の関係を疎にして、リソースが複数の表現をとれるようにすると、さまざまなクライアントが求める形式のデータを統一された HTTP のインタフェースで提供できるようになります。また、拡張性の面でも利点があります。

参考資料

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Rails 5.0.1 で rails new するとできる Gemfile の git_source ブロックの意味

Rails 5.0.1 で rails new したときに作成される Gemfile の先頭に次のブロックが挿入されるようになっていました。

git_source(:github) do |repo_name|
  repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
  "https://github.com/#{repo_name}.git"
end

これの意味

Gemfile の中で :github オプションをつけた gem について、HTTPS 経由で GitHub から取得します。

git_sourceBundler で定義されているメソッド で、引数のオプション(ここでは :github)が付いた gem の取得先 URL をブロック内で指定できます。

やる理由

Bundler が持つ :github オプションは Git プロトコルで通信するのでセキュアでなく、さらに Bundler 1.13 以降は :github オプションを使っていると警告が出ます。公式ドキュメントでも :github オプションを使うのを避けるよう明記してあります。

このコードを入れておくと、HTTPS 経由で GitHub から gem を取得できるので警告が出ずにすみます。ちなみにデフォルトの Gemfile では web-console:github オプションを使っていますね。Bundler 2.0 ではこの点が対策されるようで、それまでの対処ということです。

入ったプルリクはこちらです。

github.com

参考資料