はじめに
これまでこのブログで何度かお知らせしていますが、熊本地震の災害支援のために2016年4月25日から2016年5月31日まで「Everyday Rails - RSpecによるRailsテスト入門」のチャリティセールを実施しています。
今回のエントリでは、昨日(2016年5月15日)時点での収益(=募金額)と、収益を集計するRailsアプリの紹介をします。
開始から3週間で899.55ドル(約9万7000円)に到達!
ありがたいことに、昨日の時点で募金額はなんと899.55ドル(約9万7000円)になりました。
上のグラフを見てもらえれば分かるとおり、日によって多少の違いはあるものの、今のところ毎日売上げが発生しています。
購入してくださったみなさん、どうもありがとうございました。
この調子でいくと当初の目標額だった500ドルの2倍、1000ドル(約10万8000円)も夢じゃないかもしれません。
みなさん、引き続きご協力よろしくお願いします!
Leanpub用・収益集計Railsアプリの紹介
ところで、上のグラフは僕が作ったRailsアプリケーションで表示したものです。
「お金の話だけじゃなくて技術的なネタも読みたい!」と思っているエンジニアさん向けに、このRailsアプリの仕組みを説明します。
処理の流れ
このRailsアプリはこんな感じで処理しています。
- LeapubのAPIを叩いて売上データを取得し、データベースに保存する
- Web画面を開くとデータベース内のデータを集計し、グラフに表示する
まあ特に変わったところはない、非常にオーソドックスな流れですね。
売上データをAPIからデータベースに保存する処理
LeanpubのAPIからは次のようなJSONデータが返ってきます。
author_royaltiesの1.5(ドル)が収益です。
{ "cause_royalty_percentage":"0.0", "author_royalty_percentage":"5.0", "author_royalties":"1.5", "cause_royalties":"0.0", "author_paid_out_at":null, "cause_paid_out_at":null, "created_at":"2016-04-21T15:58:45.000Z", "royalty_days_hold":45, "publisher_royalties":"0.0", "publisher_paid_out_at":null, "author_username":"foo_bar", "publisher_slug":"", "user_email":"", "purchase_uuid":"HlKg4GLtj-fVwM4UqcRj2", "invoice_id":"DimXToR5rhc2z_g8A34LM", "date_purchased":"2016-04-21T15:58:45.000Z" }
上のJSONは1レコード分なので、実際は複数レコードが配列になって返ってきます。
あとはRails側でAPIを呼び出して、配列をぐるぐるループさせながらJSONデータ(Ruby内ではハッシュとして扱う)を保存していけば終わりです。
実際のコードとは若干異なりますが、イメージ的には以下のようなコードになります。
slug = 'everydayrailsrspec-jp' api_key = '(api key)' url = "https://leanpub.com/#{slug}/individual_purchases.json?api_key=#{api_key}" # APIを叩いて売上データを取得 hash_array = JSON.load(open(url)) hash_array.each do |hash| # 1件ずつ売上データをデータベースに保存 purchase_uuid, author_username = hash.values_at('purchase_uuid', 'author_username') sale = Sale.find_or_initialize_by( purchase_uuid: purchase_uuid, author_username: author_username) hash.each do |key, value| sale.send("#{key}=", value) end sale.save! end
データを保存する処理のテストコード
JSONをパースしてデータベースに保存するところはRSpecでテストも書いています。
(Everyday RailsはそもそもRSpecの本ですからね!)
RSpec.describe Sale, type: :model do describe '::save_records_from_hash_array!' do let(:json_text) do <<-'JSON' [ { "cause_royalty_percentage":"0.0", "author_royalty_percentage":"5.0", "author_royalties":"1.5", "cause_royalties":"0.0", "author_paid_out_at":null, "cause_paid_out_at":null, "created_at":"2016-04-21T15:58:45.000Z", "royalty_days_hold":45, "publisher_royalties":"0.0", "publisher_paid_out_at":null, "author_username":"foo_bar", "publisher_slug":"", "user_email":"", "purchase_uuid":"HlKg4GLtj-fVwM4UqcRj2", "invoice_id":"DimXToR5rhc2z_g8A34LM", "date_purchased":"2016-04-21T15:58:45.000Z" } ] JSON end it 'creates record' do # メソッドを呼び出すとレコードが増えることを確認 expect { Sale.save_records_from_hash_array!(JSON.parse(json_text)) }.to change(Sale, :count).from(0).to(1) # JSONの各値がちゃんと保存されていることを確認 sale = Sale.first expect(sale).to have_attributes( cause_royalty_percentage: BigDecimal('0.0'), author_royalty_percentage: BigDecimal('5.0'), author_royalties: BigDecimal('1.5'), cause_royalties: BigDecimal('0.0'), author_paid_out_at: nil, cause_paid_out_at: nil, royalty_days_hold: 45, publisher_royalties: BigDecimal('0.0'), publisher_paid_out_at: nil, author_username: 'foo_bar', publisher_slug: '', user_email: '', purchase_uuid: 'HlKg4GLtj-fVwM4UqcRj2', invoice_id: 'DimXToR5rhc2z_g8A34LM', date_purchased: '2016-04-21T15:58:45.000Z'.to_time ) end end end
データを集計する処理
データの集計はがっつりSQLを書いて集計しています。
class Sale < ActiveRecord::Base def self.sum_by_date sql = <<-SQL WITH jst_sales AS ( SELECT *, date_purchased + INTERVAL '9 hours' AS jst_date_purchased FROM sales ), sum_by_date AS ( SELECT to_char(jst_date_purchased, 'YYYY/MM/DD') AS purchased_on, count(DISTINCT purchase_uuid) AS purchase_count, sum(author_royalties) AS royalties FROM jst_sales GROUP BY purchased_on ) SELECT *, SUM(royalties) OVER ( ORDER BY purchased_on) AS cum_royalties FROM sum_by_date ORDER BY purchased_on DESC SQL find_by_sql(sql) end end
"WITH jst_sales AS ( )"の部分はCTE(共通テーブル式)というPostgreSQLの機能を使っています。
CTEはSQL内だけで有効なビュー(Railsではなく、データベースのVIEW)を作り出す機能と考えてもらえばOKです。
まずここで購入日時を日本時間に変換しています。
"sum_by_date AS ( )"の部分もCTEです。
ここは普通に日付単位で収益を集計しています。
一番最後に出てくるSELECT文が最終的に返却するデータのSELECT文になります。
ポイントは "SUM( ) OVER ( ORDER BY )" の部分です。
ここはPostgreSQLのWindow関数という機能を使って、収益の累積値を計算しています。
このSQLを実行すると以下のような結果が返ってきます。
日付 | 購入数 | 収益 | 累計額 |
---|---|---|---|
2016/05/15 | 5 | $55.31 | $899.55 |
2016/05/14 | 1 | $7.60 | $844.24 |
... | ... | ... | ... |
2016/04/28 | 3 | $22.80 | $427.29 |
2016/04/27 | 16 | $151.94 | $404.49 |
2016/04/26 | 17 | $167.93 | $252.55 |
2016/04/25 | 9 | $84.62 | $84.62 |
データをグラフとして表示する処理
グラフの表示は Flot というjQuery用ライブラリを使っています。
RailsからJavaScriptには直接データを渡せないので、divタグのdata属性にFlot用に加工したデータを埋め込みます。
<div data-cum-royalties="[ [1463238000000, 899.55], [1463151600000, 844.24], ... , [1461510000000, 84.62] ]" ... id="placeholder"></div>
"1463238000000"は日付を表すタイムスタンプで、"899.55"が値(ここでは収益の累積値)です。
最後にJavaScript側でデータを読み取って Flot に渡せばおしまいです。
$(function () { // 売上冊数 var purchaseCount = $('#placeholder').data().purchaseCount; // 収益の累積値 var cumRoyalties = $('#placeholder').data().cumRoyalties; // 目標値 var goalData = $('#placeholder').data().goalData; var data = [{ data: purchaseCount }, { data: cumRoyalties }, { data: goalData }]; var options = { legend: {position: "ne"}, colors: ["#eb941f", "#55a868", "#c60c30"], grid: { hoverable: true } }; // グラフを表示 $.plot("#placeholder", data, options); });
このRailsアプリのソースコード
このRailsアプリのソースコードはGitHubに置いています。
上で説明したコードはブログ用に簡略化してる部分も多いので、処理の詳細を知りたい方はGitHubのコードを参照してください。
(自分用に作ったRailsアプリなので、公開するには恥ずかしい部分もいろいろあります。悪しからず・・・)
まとめ
というわけで、このエントリではEveryday Railsチャリティセールの経過報告と集計用Railsアプリの紹介をしました。
Everyday Railsのチャリティセールは5月31日まで続きます。
引き続きみなさんのご協力をお願いします!
Everyday Rails - RSpecによるRailsテスト入門
チャリティセールの詳細や購入方法についてはこちらのエントリをご覧ください。