【読書記録】ALL for SaaS
ALL for SaaS SaaS立ち上げのすべて | 宮田 善孝 | ビジネス・経済 | Kindleストア | Amazon
読書背景
現在業務でSaaS開発に携わっているのですが、ビジネスサイド等と戦略や案件について議論する際に、自分がこれまでの他領域や他職種時代に得たなんとなくの経験やノウハウでやり過ごしていることにモヤモヤしていました。
年末のタイミングに一度スタンダードなSaaSについての考え方を知ろうと思い、本書を手に取りました。
読んだ感想、おすすめ対象
- 個人的には勉強になることが多く、良書に感じました
- SaaSに関して考慮すべき観点を薄く広く網羅してある
- 一見どれも当たり前のようなことだが、それらが広く全てちゃんと言語化されていることに価値がある印象
- 全てをちゃんとできている人はかなり限られていると思うので、自プロダクトのチェックシート、振り返りとしても使えそうな印象
- 以下のような各種トピックについては、書籍内でも注釈があるように別の良書が存在
- freeeのPdMが著者で、freee時代の経験が基に汎用的、再現性があるナレッジに落とし込まれている
- freeeの具体的な立ち上がりストーリーやエピソード等はあまり記載されていないので、そういったノンフィクションストーリー的な面白さは無い
- 読むことをおすすめできる人は以下
- 逆に読むことをおすすめできない人は以下
個人的に参考になったトピック
- BtoB, BtoCの違い
- SaaSの評価方法、LTV、CAC
- ゴートゥーマーケット戦略
- MRR(1ユーザあたりの平均月額利用料)の構造理解、分解
- 各プラン、ユーザー数、単価等
- 費用、顧客獲得のチャネル、ファネル等の理解
- リーチできる潜在顧客数、商談実施した顧客数等
- MRR(1ユーザあたりの平均月額利用料)の構造理解、分解
- OKR
- チーム、コミュニケーションデザイン
- アライメント:一つの方向へ向かうこと
- オートノミー:自主性の尊重すること
- ステコミ、OKRレビュー、スプリントレビュー等
直接関係する人は少なそうだが、面白いと感じたトピック
- プライシング
- ユーザーへの提供価値視点
- SaaSを利用した際に得られる価値に見合うコストか否か
- 提供側のコスト視点
- 立ち上げ、開発、維持費等のコストに利益を載せる
- 市場、競合視点
- 競合のサービスと比較した提供価値等の差異
- ユーザーへの提供価値視点
- プロダクトサイドにおける人事評価設計
- プロダクトやタスクフォースでの評価
- 所属部署等での評価
- 上記の折衷案
- リーガル対応
その他
- たまたまfreeeのエンジニアにカジュアルで話す機会がありましたが、組織としてかなり成熟していて、プロダクト開発していて楽しそうな環境に見えました。
- freeeの技術ブログにも、本書とかなり似た内容がありました。 developers.freee.co.jp
Active Storage導入しました
背景、経緯、目的
業務でRailsを使ってWebサービスを開発しているのですが、
ユーザーの画像設定やファイル添付機能等をシンプルに実施したく、
またRailsのバージョンアップ等のタイミングもあり、 Active Storageの導入を決めました。
(CarrierWave vs Active Storageみたいな議論は本記事では扱いません)
純粋にRailsからアップロードし、AWS S3に保存するような設計です。
環境
主な使用方法
- 以下コマンド等でテーブルを作成、migrationを実施
rails active_storage:install rails db:migrate
- 各種設定 今回は開発環境ではlocal、本番環境等ではAWS S3に保存するイメージです。
# Gemfile gem 'aws-sdk-s3'
# config/environments/development.rb config.active_storage.service = :local # config/environments/production.rb config.active_storage.service = :amazon
# config/storage.yml local: service: Disk root: <%= Rails.root.join("storage") %> /storage/* !/storage/.keep amazon: service: S3 access_key_id: <%= ENV['AWS_S3_ACCESS_KEY_ID'] %> secret_access_key: <%= ENV['AWS_S3_SECRET_ACCESS_KEY'] %> region: <%= ENV['AWS_S3_REGION'] %> bucket: <%= ENV['AWS_S3_BUCKET'] %>
- 該当Modelに紐付け
# app/models/book.rb class AdminUser < ApplicationRecord # 1対1の場合 has_one_attached :image # 1対複数の場合 has_many_attached :images end
- フォーム等イメージ
-# app/views/books/edit.html.haml -# 複数の場合 multiple: trueを指定、accept: 'image/jpeg,image/png'等で受付可能なMIMEタイプ指定可能 = f.file_field :files, accept:
- 表示イメージ
-# app/views/books/show.html.haml %dl %dt 表紙イメージ %dd= image_tag @book.image
Tips
attribute代入タイミング
有名な話なのですが、Rails5.2〜6.0までは、attributeに代入した時点で、
アップロードしたファイルがストレージに保存されてしまいます。
以下のようにすることが良くあるかと思いますが、バリデーションエラー時等に、
そのままファイルが残ってしまうという問題点があります。
@book = Book.new(title: 'サンプル本', image: params[:image]) if @book.save # 省略 redirect_to dashboard_path else # 省略 render 'edit' end
Rails6.0以上ではsave後に保存されるようになったので、気にする必要は無いのですが、 それ以前のバージョンでは、以下等の対応が必要になります。
- バリデーションエラーの場合、purgeを呼び出す
@book.image.purge errors.add(:image, '指定のファイル形式ではありません。')
- バリデーション等実施、save後にattachするようにする(別途save前にimageのバリデーションも必要)
if @book.save @book.image = params[:image] if params[:image] else end
purge_later
ActiveStorageでは、
has_one_attached
、has_many_attached
において、
デフォルトでdependent: :purge_later
となっており、該当インスタンスが削除された場合、
Active Jobを介して、関連付けられているモデルと実際のリソースファイルを非同期で破棄します。
また、キューは以下のように必要に応じて指定が必要です。 (Rails6.1以降はqueues.analysis, queues.purgeに分割して指定することが推奨されています。)
# config/application.rb config.active_storage.queue = 'default'
nginx設定、413エラー等
個人的に一番ハマったポイントです。 まずデフォルトではnginxのPOST最大サイズは1MBのようです。 必要に応じて、以下のように
client_max_body_size
を指定する必要があります。
# script/nginx/nginx-development.conf
server {
listen 8080;
server_name xxx.xxx.xx.xxx;
root /app/public;
client_max_body_size 10m;
}
ただこのままでは、サイズ超過したファイルをアップロード時に、
413エラー画面がnginxのデフォルトの画面になってしまいます。
本件を解決する方法はいくつかあるかと思いますが、Webアプリ側はGET http(s)://{host}/payload_too_large
でエラーページを表示できるようにし、
413エラーを以下のようにリダイレクトを使い、
POSTをGETとし、オリジナルのエラーページを表示できるようにしています。
location / { error_page 413 @413; } location @413 { return 303 https://$host/payload_too_large; }
これで全て解決と思ったのですが、
ChromeやFirefoxでは実際にサイズ超過したファイルをアップロードすると、
数百MBまでは問題ないのですが1GB等を超過すると、以下エラーが出るようになりました。
(Safariではどれだけサイズが大きくても、問題なくリダイレクトされる)
ChromeやFirefoxでは、nginxが413エラーを返した場合も、
アップロード処理を継続してしまうという仕様のようです...
よって、JS等フロントで予め必要以上のサイズのファイルは、
アップロードできないよう制御しておくのが良さそうです。
以下サンプルではファイルの複数アップロードを想定し、
ファイル数およびファイルサイズで制御をかけています。
// book_file_upload.js document.addEventListener('DOMContentLoaded', function() { const lengthLimit = 3 const sizeLimit = 1024 * 1024 * 10 const inputTemplate = document.getElementById('book_files'); var fileLength = 0 $('.file-uploads').on('change', function(e){ var files = e.target.files; // 添付ファイルボタンのinputは空に戻す e.target.files = new DataTransfer().files; fileLength += files.length // ファイル数バリデーションエラー表示 if (fileLength > lengthLimit) { alert('1つのBookに添付できるファイル数は最大3つです。'); fileLength -= files.length return; } var error_message = '' for (let i = 0; i < files.length; i++) { var file = files[i] if (file.size > sizeLimit) { error_message = 'Bookに添付できる1ファイルの最大サイズは10MBです。'; continue; } // アップロード予定ファイルプレビュー表示 var file_preview = $('<div>', { class: 'file-preview' }); file_preview.append(`<i class= "fa fa-file"></i>${file.name}`); file_preview.append('<span class="btn-delete">削除</span>'); // ファイルを個別に削除等できるよう分けてinputへ格納 var dt = new DataTransfer(); dt.items.add(file); var input = inputTemplate.cloneNode(false); input.files = dt.files; input.id = `book_files_${file.name}`; file_preview.append(input); $('.uploaded-files').append(file_preview); }; // ファイルサイズバリデーションエラー表示 if (error_message != '') alert(error_message); }) // アップロードファイルの削除 $('.uploaded-files').on('click', '.btn-delete', function(){ fileLength -= 1 $(this).parent().remove(); }); });
spec
上記等のバリデーションテスト等で、サイズ上限のバリデーション等を試したいこともあるかと思います。
ただその為だけに、大きいサイズのファイルを用意はできないケースが多いと思うので、モック等で対応するのが良いかと思います。
let(:images) { [image] } let(:image) { Rack::Test::UploadedFile.new(Rails.root.join('app', 'assets', 'images', 'sample.png'), 'image/png') } before do allow(image).to receive(:size).and_return(11.megabytes) end
感想
- Rails6.0以降改善されているとのことですが、
やはりsave前のattribute代入でアップロードされてしまうのはやや面倒 - シンプルなアップロード機能であれば、特に問題は感じませんでした!
参考記事
Rails ガイド railsguides.jp
413エラーのリダイレクト qiita.com
413エラー、ブラウザによる違い
stackoverflow.com
Draper導入しました
背景、経緯、目的
業務でRailsを使ってWebサービスを開発しているのですが、
一部Modelにおいて、徐々にメソッドが増え肥大化してきたため、チームでDraperの導入を決めました。
以下のようなメソッドをModelからDecoratorへ移せたらいいなというイメージでした。
- 表示に関するメソッドだが、オブジェクトに依存しているメソッド
- 多くのページで使用するメソッド
- 以下例
# app/models/book.rb # 本の在庫状況を表示 def display_status sold_out? ? '売り切れ' : '在庫有り' end
主な使用方法
- gemを入れる
gem 'draper'
- 以下コマンド等で既存モデルに対して、Decoratorファイルを生成
rails generate decorator Book
- Decorator側に移すメソッド等を記載
# app/decorators/book_decorator.rb class BookDecorator < Draper::Decorator # 本の在庫状況を表示 def display_status sold_out? ? '売り切れ' : '在庫有り' end end
delegete_all
呼び出しによって、BookModel内のメソッドを利用可能にします。
以下例のようにsold_out?
メソッドがModel内にあっても、Decorator側で利用できます。
(上記コマンドで生成した場合、初めからdelegete_all
は記載されているかも。)
# app/decorators/book_decorator.rb class BookDecorator < Draper::Decorator delegate_all # 本の在庫状況を表示 def display_status sold_out? ? '売り切れ' : '在庫有り' end end
- Controller側からViewにインスタンス変数を渡す直前に
.decorate
でデコレート
View側でDecorateファイルに記載したメソッド等が使えます。
# app/controllers/books_controller.rb @book = Book.first.decorate
-# app/views/books/show.html.haml %dl %dt 在庫状況 %dd= @book.display_status
- ActiveRecord_Relationに対しても、まとめてデコレートし、View側で個別にメソッドを使用可能です。
# app/controllers/books_controller.rb @books = Book.all.decorate
-# app/views/books/index.html.haml - @books.each do |book| %dl %dt 在庫状況 %dd= book.display_status
その他、Tips
association
associationを記載しておくと、関連するModelも合わせてデコレートできる。
# app/decorators/book_decorator.rb class BookDecorator < Draper::Decorator # 対1 decorates_association :author # 対複数 decorates_associations :comments end
ActiveAdmin
ActiveAdminを使用した管理画面で、Decoratorメソッドを使用する場合、
そのままでは使えず、decorate_withで渡してあげる必要があります。
# app/admin/book.rb ActiveAdmin.register Book do decorate_with BookDecorator end
kaminari
ページネーションでkaminari等を使用している場合、
ActiveRecord_Relationにメソッドを追加されているので、
そのままだと、ActiveRecord_Relationをデコレート後には使用できません。
該当メソッド類をdelegete
して使用できるようにする必要があります。
# app/decorators/collection_decorator.rb class CollectionDecorator < Draper::CollectionDecorator delegate :current_page, :total_pages, :limit_value, :entry_name, :total_count, :offset_value, :last_page? end
特定のModel等ではなく、多くのModel等で使用する場合、まとめてしまって良いかも。
(各DecoratorはApplicationDecoratorを引き継ぐようにする)
# app/decorators/application_decorator.rb class ApplicationDecorator < Draper::Decorator def self.collection_decorator_class CollectionDecorator end end
# app/decorators/book_decorator.rb class BookDecorator < ApplicationDecorator end
CollectionDecorator
基本的に使うケースは少ないかつ、あまり推奨されないかと思いますが、
デコレートした後のCollectionDecoratorに対して、
どうしてもビュー側でfind_by等を使いたくなるケースもあります。
CollectionDecoratorにそういったメソッドは用意されていないので、自分で追加する必要があります。
# app/decorators/collection_decorator.rb class CollectionDecorator < Draper::CollectionDecorator def find_by(attribute = {}) object.find_by(attribute)&.decorate end end
spec
上記のコマンドで生成した場合、specファイル等も合わせて作成されます。
特に変わった点はないが、当然オブジェクト生成時にデコレートする必要があります。
# spec/decorators/book_decorator_spec.rb RSpec.describe BookDecorator do describe '#display_status' do subject { book.display_status } let(:book) { build(:book).decorate } context '...' do end end end
感想
- お試し的に、associationが複雑でないModel等から導入しましたが、
インスタンスメソッドが24個→11個になったModel等もあったので、
当初のFatModelの回避という点では目的を達成できていそう。 - 1つのModelにインスタンスメソッドが20個以上ある等が無ければ、
個人で開発する小規模なサービス等ではメリットを感じる場面が少なさそう。 - 途中から導入すると移行がそれなりに大変なので、
規模が大きくなるとあらかじめ分かっているサービスであれば、最初から導入したい
(現在も関連箇所を触る際等に、適宜移行中)
参考記事
Draper公式ドキュメント
READMEが丁寧に書いてあり、かつそんなに文量も多くないので、
基本的に公式ドキュメントで事足りるかと思います。
github.comDecoratorの役割とDraperについて
そもそものDecoratorの役割等について、簡潔に記載されています。 qiita.comDraperの使い方 まとめ
上記公式ドキュメントの日本語訳的な位置づけかと思います。
公式ドキュメントに書かれている使用方法は大体書かれています。 nekorails.hatenablog.com