Backbone.js + Rails + AWSでWebサービスを開発したお話(その1:Rails)

Pocket

弊社では、新サービス「otacco – おたっこ|オトナになったオタク女子のための情報サイト」を先日ローンチいたしました。

otacco

サービスの仕様が、弊社の他のアプリケーションとかなり異なるということもあり、今回は新しい開発手法にチャレンジしてみようということで、フロントエンドにBackbone.js、バックエンドにRuby on Railsという組み合わせを採用いたしました。

今回から3回に渡って、SPA(シングルページアプリケーション)における、主にバックエンド側の開発方式やインフラ構成、およびSEO/OGPのノウハウ等に関してご紹介させて頂きたいと思います。

(Backbone.jsの情報はほんのちょっとしか出てきません。ごめんなさい)

基本情報

Rubyのバージョンは2.2、Railsは4.2.1、DBはMySQL5.6、セッションストレージやキャッシュ管理にはMySQL5.6から導入されたInnoDB memcached pluginを使用しています。

ソース管理はGithub、開発フローはGithub Flowを採用、コーディング規約は下記を参考にしました。

The Ruby Style Guide
The Rails Style Guide

スキーマ管理(マイグレーション)

RailsのDBスキーマ管理に関しては、ActiveRecordのマイグレーション機能で管理する手法がスタンダードだと思いますが、下記のような理由でこの方式は採用しませんでした。

  • 開発の初期段階ではスキーマ構造に頻繁に変更が入ることが予想されるが、その度にマイグレーションファイルを作成すると、ファイル数が無駄に多くなる。
  • ActiveRecordのマイグレーション方式のメリットである「任意の時点のスキーマを再現できる」という機能が必要になるケースは滅多にない。
  • マイグレーション用のコードを書くのが面倒。

ということで他の手法を探してみたところ、Cookpadさんが公開しておられる「Ridgepole」というツールが、我々の要望に非常にマッチしている感じでしたので、機能検証後、こちらのツールを使用させて頂くことにしました。

ridgepole

Ridgepoleを使うことで下記のようなメリットがあります。

  • 各テーブルごとに1つのschemaファイルを用意すれば良いだけなので管理するファイルの数が少なくて済む。
  • schemaファイルからDBテーブルを作成するだけでなく、DBテーブルからschemaファイルを生成することが可能。
  • schemaファイルがDB定義書を兼ねているので、DB定義書を作成/更新する必要がない。
  • マイグレーション用のコードを書く必要がない。

詳細に関してはCookpadさんの下記の記事をご参照ください
クックパッドにおける最近のActiveRecord運用事情

otaccoの「記事」用テーブルのschemaファイルは、例えば下記のようになっています。(テーブルごとにschemaファイルを分割しています)

create_table "entries", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" do |t|
  t.integer  "category_id", limit: 4,               null: false
  t.string   "title",       limit: 255,             null: false, collation: "utf8_general_ci"
  t.string   "image",       limit: 255,                          collation: "utf8_general_ci"
  t.string   "description", limit: 255,             null: false, collation: "utf8_general_ci"
  t.integer  "status",      limit: 4,               null: false
  t.datetime "created_at"
  t.datetime "updated_at"
end
add_index "entries", ["category_id"], name: "idx_entries_01", using: :btree

こちらのschemaファイルと、DBの実際のスキーマ構造を照合して、自動的にSQL文を生成/発行したり、あるいはテーブルからschemaファイルを生成してくれたりします。ものすごく便利です。

# 差分をチェック
ridgepole -E development --enable-mysql-awesome --diff config/database.yml schemas/Schemafile
# DRY-RUN
ridgepole -E development --enable-mysql-awesome -c config/database.yml --apply --dry-run -f schemas/Schemafile
# CREATE文やALTER文実行
ridgepole -E development --enable-mysql-awesome -c config/database.yml --apply -f schemas/Schemafile
# DBからschemaファイルを生成
ridgepole -c config/database.yml -E development --export --split --enable-mysql-awesome --output schemas/Schemafile

ちなみに、弊社ではDBの文字コードにutf8mb4を採用しておりますが、この場合innodbのファイルフォーマットにはBarracudaを使用して、ROW_FORMATをDYNAMICに設定しないと、インデックスに使用する列のサイズによっては「Index column size too large」というエラーが出る場合があります。

この問題に対応するため、activerecord-mysql-awesomeというgemも併用しております。(上記のコマンドにはそのgemを併用する際のオプションも含まれています)

管理画面

管理画面に関しては、独自にゼロから開発するという方式も考慮したのですが、開発工数を鑑みてActive Adminを採用しました。

active_admin

ちょっと気の利いた機能を追加したいという場合にはあまり融通が効かないという弱点はあるものの、こちらも非常に便利なgemです。

ほとんど定型的な使い方しかしていないとは思うのですが、ちょっとしたtipsとして、「多対多」の関係にあるレコード同士を編集画面で関係付けるための設定をご紹介します。

f.has_many :entry_keywords, allow_destroy: true, heading: false, new_record: true do |entry_keyword|
  entry_keyword.input :keyword_id, label: I18n.t('activerecord.models.keyword'), as: :select, include_blank: false,
    collection: Keyword.all.map { |k| [k.title, k.id] }
end

ActiveRecord側で「記事 – 中間テーブル – キーワード」という多対多の関係が成立している場合に、記事の編集画面でキーワードを複数関連付けたい場合には上記のようなコードを記述します。(実際に表示される画面は下記のようになります)

active_admin_has_many

API

フロントエンドから呼び出されるAPIは、GrapeJbuilderを使用して開発しました。

こちらも基本的には標準的な手法で開発しておりますが、ローカル環境においては、WEBrickで起動したバックエンドを、タスクランナー(弊社ではGulp)で起動しているフロントエンドからいわゆる「CORS」(Cross-Origin Resource Sharing)を使用して呼び出せるようにする必要がありますので、rack_corsというgemを導入し、下記のようなコードをローカル開発環境用のファイルに記述して対応しました。

# config/environments/local.yml
config.middleware.use Rack::Cors do
  allow do
    origins 'http://localhost:3000', 'http://localhost:5000'
    resource '/api/*', :headers => :any, :methods => [:get, :post, :options, :put], :credentials => true
  end
end

画像管理

画像のアップロードに関してはCarrierWavefog-awsminimagickを使用しました。(rmagickはメモリ消費が激しいという情報がありましたので使いませんでした)

画像は、ステージング環境と本番環境に関してはS3にアップし、CloudFrontから参照出来るように構成してあります。設定は下記のように行いました。ご参考までに。

# config/initializers/carrierwave.rb
CarrierWave.configure do |config|
  config.fog_provider = 'fog/aws'
  config.fog_credentials = {
      :provider              => 'AWS',
      :aws_access_key_id     => Settings.aws.access_key_id,
      :aws_secret_access_key => Settings.aws.secret_access_key,
      :region                => 'ap-northeast-1'
  }
  config.storage = :fog
  config.cache_storage = :fog
  config.fog_directory = Settings.fog_directory
  config.asset_host = Settings.asset_host
  config.fog_public = true
end

アセット管理

Rails側で管理しているcss/jsファイル等の静的アセットファイルは、asset_syncを使用して、デプロイ時に画像と同様S3にアップしています。こちらも設定ファイルを記載しておきますのでご参考までに。

# config/initializers/asset_sync.rb
AssetSync.configure do |config|
  config.fog_provider = 'AWS'
  config.aws_access_key_id = Settings.aws.access_key_id
  config.aws_secret_access_key = Settings.aws.secret_access_key
  config.fog_directory = Settings.fog_directory
  config.fog_region = 'ap-northeast-1'
end

定数管理

定数管理にはrails_configを使用して、「Settings」というクラス名で定数を参照出来るようにしています。環境ごとに異なる定数値が必要な場合は、環境別のymlファイルを使用して定数を上書きします。例えば下記のような感じです。

# settings.yml
aws:
  access_key_id: 'HOGE'
  secret_access_key: 'hhhOOOGGGEEE'
search:
  index_name: "hoge"
# settings/local.yml
search:
  index_name: "hoge_local"

さらに、「設定値を連結したい」というケースに対応するため、下記のような処理を追加しています。

# settings/merge.yml
uri:
  origin: <%= format('%s://%s', Settings.uri.schema, Settings.uri.host) %>
# config/application.rb
Settings.add_source!("#{Rails.root}/config/settings/merge.yml")
Settings.reload!

上記により、Settings.uri.originというようなコードで、連結された文字列を取得することが可能になります。

区分値管理

テーブルの区分値用の列に関しては、MySQLのenum型を使用するという手法もあるのですが、「値の定義を追加したい場合にALTER文の実行が必要になる」というデメリットがあるため、この方式は採用しませんでした。

また、単に定数で管理するという手法だと、例えば「区分IDから区分名を取得したい」という場合にうまく対応出来ないため、区分値に関してはActiveHashを使って列挙型のように扱う方式で対応しました。

class SpanType < ActiveHash::Base
  include ActiveHash::Enum
  self.data = [
    {id: 1, name: I18n.t('activerecord.attributes.values.span.daily'), value: 1.day, span_type: 'DAILY'},
    {id: 2, name: I18n.t('activerecord.attributes.values.span.weekly'), value: 1.week, span_type: 'WEEKLY'},
  ]
  enum_accessor :span_type
end

enum_accessorを指定することで、SpanType::DAILY.idというコードで区分IDを、SpanType::DAILY.nameというコードで区分名を取得することが可能です。

また、ActiveRecordと同様なメソッドでコレクションを取得出来るので、例えばActive Adminのプルダウン型の項目を、下記のようにシンプルなコードで実装出来ます。

f.input :span, required: true, as: :select, include_blank: false,
  collection: Entry::SpanType.all.map { |s| [s.name, s.id] }

次回は、otaccoのインフラやミドルウェア構成に関してご紹介させて頂きたいと思います。

Backbone.js + Rails + AWSでWebサービスを開発したお話(その2:AWS/ミドルウェア)


「世界NO1のotakuマーケティングカンパニー」を目指す株式会社アリスマティックでは、インフラ・サーバーサイド・ネイティブ・フロントエンドなど、各職種でエンジニアを絶賛募集中です!

採用ページ

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です