メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

Rails 5.1にスムーズにアップグレードするためにやった6つのこと

こんにちは。Ruby on Rails(以下Rails)のリードエンジニアを担当している橋本と申します。

先日、6/28にメドピアでは、MedBeer - Rails 5.1での開発についてというイベントを開催しました。このイベントでは今年(2017年)4月にリリースされた、Rails 5.1の新機能や開発環境に関してさまざまな発表が行われ、来場したエンジニアの方からも好評のうちに終えることができました。

このイベントの直前に、今までRails 5.0で動いていたメドピアのWebアプリケーションをRails 5.1へとアップグレードを行い、当日、その内容の発表を行いました。今回のブログ記事ではその発表を元に、アップグレードのためにやった6つのことについて紹介を行います。

スムーズにアップグレードのするためにやった6つのこと

  1. 調査用ブランチと本番用ブランチを用意
  2. 専用の検証環境を用意する
  3. デプロイ時の差分を小さくしておく
  4. アップグレードガイドとリリースノートに目を通す
  5. アップグレード前後でコードを正しく修正する
  6. アップグレード時に大きな変更を行わない

1. 調査用ブランチと本番用ブランチを用意

Rails 5.1をはじめ、アップグレードされたGemを安全に本番環境に投入できるように、以下の2つのブランチを作成しました。

調査用ブランチ 本番用ブランチ
主な動作環境 開発環境 本番環境
目的 全体の作業量・修正の見通しをつける 安全に本番環境で動作させる
アップグレード対象 Rails 5.1に依存するGemのみ すべてのGem
Gemのグループ分け しない する

調査用ブランチ

開発環境で、手早くRails 5.1で動かせる状態にするためのブランチで、アップグレードに必要な作業量と、コード修正の見通しをつけるためのものです。このブランチでは、Rails 5.1に依存するGemのみ手早くアップグレードを行いました。

まず、Gemfileに以下のように指定を行い、bin/bundle update railsを実行します。

gem 'rails', '~> 5.1'

コマンドの実行後に、下記のようにRails本体に含まれるGem(具体的にはactivesupportactiverecordなど)へ依存関係のあるGemの競合解消に失敗したというエラーが表示されるので、1つ1つアップグレードしていきます。

Bunlder could not find compatible versions for gem "activesupport":
  In Gemfile:
    act-fluent-logger-rails was resolved to 0.3.1, which depends on
      activesupport (< 5.1, >= 4)

この場合は、act-fluent-logger-railsのバージョンを0.3.1がactivesupportの5.1より下のバージョンでしか動かないという内容なので、Gemfileでバージョン固定を外すか、Rails 5.1に対応したバージョンを指定して、bin/bundle update act-fluent-logger-railsを実行します。

エラーメッセージが表示されなくなるまで各依存パッケージのアップグレードを行うと、Rails 5.1で動作できる状態になります。(コードの修正は別途行う必要があります。)

本番用ブランチ

本番環境でRails 5.1を動かすためのブランチで、調査用ブランチと異なり、(可能な限り)全てのGemのアップグレードを行なったものです。次節のようなGemのグループ分けを行い、各グループを順番にアップグレードを行いました。

一気に全体のbin/bundle updateを行わず、グループに分けたGemのアップグレードを行ったのは、後述のデプロイの差分を小さくするためと、グループごとに利用する環境や検証の作業が異なるためです。

本番リリースに必要なコードの修正も最終的にこのブランチにコミットを行いました。

Gemのグループ分け

本番用ブランチでのGemのグループ分けは以下のようにしました。

a) パッチバージョンのみアップグレードしたもの

あるGemのバージョンをX.Y.Z(X, Y, Zは数字)とした場合、最新バージョンでZの数字のみ上がっていて、大きな仕様変更はないGem

b) 開発環境、テスト環境のみ使用するGem

例)bullet, rspec-railsなど

開発支援ツールやテストツールなど本番環境での動作の必要がないGem

c) 本番環境でも使用するGemで影響が少ないもの

例)activerecord-import, draperなど

Railsの基本機能拡張など、本番環境での動作を行うGem

d) 本番環境でも使用するGemで影響が大きいもの

例)administrate, sidekiqなど

管理画面やジョブ実行システムなど、上記3よりも大規模なGem

e) Rails本体

Rails公式のGem(activerecord, actionpack, railtiesなどを含む)

グループ分けしたGemはアップグレード後に、各環境のサーバーで検証が行われました。

2. 専用の検証環境を用意する

今回は、通常のデプロイフロー(ステージング環境で検証して本番環境へリリースという流れ)を妨げないように、Rails 5.1アップグレード用の検証環境を用意しました。

以下の図のように、影響が小さいGemのアップグレードの場合は、通常のデプロイフローに乗せ、影響が大きいGemのアップグレード「d) 本番環境でも使用するGemで影響が大きいもの」と「e) Rails本体」の場合は、検証環境で手動テストを含む十分な検証を実施してから、本番環境へのリリースを行いました。

f:id:ryohashimoto:20170714112243p:plain

3. デプロイ時の差分を小さくしておく

本番環境へのリリース(デプロイ)の際には、できるだけ以前のリリースとの差分が小さくなるようにしておきました。理由としては、差分が大きくなると不具合が発生した際に、原因(どのGemのアップグレード・どのコードの変更によるものか)の判別が難しくなるからです。

特に、Rails本体を5.0から5.1へアップグレードしてデプロイする際にはリリースの差分が以下のようになるようにしました。

  • Gemfile.lockの変更がRails本体に含まれるGemのアップグレードのみとなっている。
  • 他のコードも、5.1でしか動かないコードの変更のみとなっている。

このようにして、できるだけRails 5.0の段階で準備を行っておき、その後のRails 5.1でのリリースでの変化を小さくしておくようにしておきます。

4. アップグレードガイドとリリースノートに目を通す

アプリケーション側でどの部分のコードを変更するか把握するために、以下の公式のアップグレードガイドをチェックしました。

特に、Rails 5.0からRails 5.1へのアップグレードに関する部分に目を通しておきます。

Rails 5.1の変更内容や新機能の詳細に関しては、リリースノートに書かれているので、こちらもチェックを行いました。

ここで非推奨や廃止となったメソッドなどの記述についても詳しく書いており、アプリケーションで使用していないかチェックを行いました。

5. アップグレード前後でコードを正しく修正する

上記のアップグレードガイドとリリースノートの内容を元に、コードの修正を行いました。コードの修正は、Rails 5.1へのアップグレードの前後で行う必要がありました。

アップグレード前 (Rails 5.0) の対応

テストコード実行時や、アプリケーションの起動時のログにDEPRECATION WARNING(非推奨の警告)が出力されている場合は、該当する記述を修正する必要があります。

has_manyclass_nameオプションにクラスを指定している部分を修正

モデルのhas_manyのオプションのclass_nameにクラスを直接渡していた部分で上記の警告が出力されていたので、文字列に変更を行いました。

paramsをハッシュとして扱っている部分を修正

Rails 5.0からActionController::ParametersHash(のサブクラスのActiveSupport::HashWithIndifferentAccess)から継承されなくなったのに伴い、コントローラのparamsに対して、symbolize_keysを実行している箇所で警告が発生していたいので、修正を行いました。

アップグレード後 (Rails 5.1) の対応

各種設定ファイルを5.1に合わせて更新していきました。

bin/rails app:updateを実行

新しいバージョンに対応した設定ファイルの作成や、更新を行うタスクのbin/rails app:updateを実行します。このタスクを実行すると独自にカスタマイズした内容が上書きされる可能性があるので、実行の前後のファイルの差分を確認して妥当な内容にする必要があります。

config.load_defaults 5.1を設定

Rails 5.1からconfig.load_defaultsというメソッドが提供されるようになり、バージョンごとの推奨の設定を読み込めるようになりました。今回は、config/application.rbに以下の内容の記述を行いました。

config.load_defaults 5.1
config/secrets.ymlを読み込む際のハッシュのキーを文字列からシンボルに

config/secrets.ymlに記述されている内容がネストされている場合、その内容を参照する際に、これまでハッシュのキーとして文字列を指定していましたが、Rails 5.1からはシンボルにする必要があります。

Rails 5.0までは以下のように参照していたものが、

Rails.application.secrets[:smtp_settings]["address"]

Rails 5.1からは、以下のようにシンボルにしないと参照できなくなっています。

Rails.application.secrets[:smtp_settings][:address]

6. アップグレード時に大きな変更を行わない

今回は、Railsのアップグレードと同時に工数が発生するような大きな変更を加えないことにしました。 具体例として、プライマリキー(id)をBIGINT型にしないようにしました。

プライマリキー(id)をBIGINTにしない

Rails 5.1では新しく作成されるテーブルのプライマリキー(id)の型がBIGINTになるという大きな変更が加えられました(PostgreSQL/MySQLの場合)。

メドピアでは、特に大きな数のidを扱う予定がなく、予期せぬ不具合の発生を防ぐため、これまで通りidの型としてINT (integer)を使うという選択を行いました。

これに伴い、マイグレーションを新たに実行した際に、既存のテーブルとRails 5.1で作成した新しいテーブルでidがINT型になるように以下のような対応を行いました。

既存のテーブルへの対応

既存のテーブルに関しては、bin/rails db:migrate:resetを再実行して、生成されたdb/schema.rbcreate_tableの部分にid: :integerのオプションがつくようにしました。

Rails 4.2で作成された古いマイグレーションファイルを実行する際にバージョン表記がないという エラーになってしまっていたので、ActiveRecord::Migrationの部分に例えば以下のようにバージョンを付与しました。

class CreateUsers < ActiveRecord::Migration[4.2]
新規のテーブルへの対応

Rails 5.1で作成されたマイグレーションファイルをそのまま実行すると、idがBIGINTになってしまうので、 以下のようにマイグレーションファイルのcreate_tableの部分にid: :integerのオプションをつけるようにしています。

class CreateQuestions < ActiveRecord::Migration[5.1]
   create_table :questions, id: :integer do |t|
     ...

まとめ

Railsをスムーズにアップグレードするために上記の6つのことを実施しました。

現在はRails 5.1環境で安定して稼働しており、フロントエンド周りの新機能を生かした開発なども進めることができています。

今回のアップグレードを通して、古いRailsやGemを使い続けることによるセキュリティへのリスクを回避でき、また、チーム内のメンバーのRailsに対する意欲や関心を高めることができたのはよかったと思います。

Rails 5.1を題材に取り上げましたが、内容としては過去や将来のRailsのバージョンに対しても適用できる内容だと思うので、参考にしていただけると幸いです。


是非読者になってください(ง `ω´)ง


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

NextCloudを導入してみた

皆様こんにちは。
今回は、社外の取引先様等に重要な情報を安心して受け渡しするツールが決まってなかったので、NextCloudを導入してみたエントリーとなります。

以下、これまでの課題となります。

  • 手渡しとか郵送はコストが掛かりすぎてしまう
  • メールは平文のため危険、添付ファイルを暗号化してパスワードは別メールにて送信も危険となる
  • 各種インターネットサービスは適切に管理されてるか監督が難しい、機能が多すぎて誤操作と脆弱性が心配、受け取り側がアカウントを作成して頂かなければいけない場合等々

NextCloudとは、ファイルホスティングサービスを構築するための、PHPJavascriptで書かれたオープンソースソフトウェアとなります。機能的に似たようなサービスとして比較されるのはdropboxとなります。元々ownCloudという名前で開発されていたのですが、方針の食い違いによりフォークされてNextCloudとなったようです。

要件

はじめに、要件のまとめとなります。

  • 弊社社員ユーザーが認証され、オフィスからブラウザーでアクセスしてファイルをアップロードできる
  • アップロードされたファイルは暗号化され、アップロードした社員と指定された(取引先様などの)社外ユーザだけがアクセスできる
  • (取引先様などの)社外ユーザは認証され、ブラウザーでアクセスして復号されたファイルをダウンロードできる
  • ファイルのダウンロードは指定された日数をすぎると無効になる

構成

下記、構成の詳細となります。

Amazon Elastic Load Balancing

  • ポート443:インターネットからのアクセス(SSL)を受け入れ、nginxにフォワードします
  • ポート8000:オフィスからのアクセスを受け入れ、nextcloudにフォワードします

Amazon EC2

nginx・fail2ban

https://nextcloud.hogehoge.co.jp/index.php/s/IWpva4t3FoYgnkSのようなNextCloudのURLリンクでの共有で、ブラウザーでアクセスされるURLリンクのパスだけをプロキシして、NextCloudに投げます。nginxのコンフィグファイルの内容は以下となります。

server {
    【省略】
    # /index.php/heartbeat
    # /index.php/s/*
    # /index.php/s/*/authenticate
    # /index.php/s/*/download
    # /index.php/core/js/*.js
    # /index.php/apps/files_sharing/ajax/publicpreview.php
    # /core/img/*
    # /core/css/*.css
    # /core/fonts/*.woff
    # /core/js/*.js
    # /core/vendor/*.css
    # /core/vendor/*.js
    # /apps/encryption/*.js
    # /apps/files/*.js
    # /apps/files_sharing/*.css
    # /apps/files_sharing/*.js
    #
    location ~* ^(/index\.php/(heartbeat|s/[A-Za-z0-9]+(/authenticate|/download)?|core/js/.+\.js|apps/files_sharing/ajax/publicpreview\.php)|/core/(img/.+|css/.+\.css|fonts/.+\.woff|js/.+\.js|vendor/.+\.(css|js))|/apps/(encryption/.+\.js|files/.+\.js|files_sharing/.+\.(css|js)))$ {
        proxy_pass http://nextcloud;
        proxy_set_header Host $http_host;
    }
}

これで共有されたファイルがブラウザーでダウンロードできるギリギリのパスだけを許可となります。それと、fail2banでnginxに来たアクセスを監視して、不自然なアクセスが有ればブロックとなります。

NextCloud

NextCloudで行った設定は下記となります。ブラウザーにて管理者のアイパスでログインし、管理画面から設定が可能となります。

  • 共有
    • URLリンクでの共有を許可する
      • 常にパスワード保護を有効にする
      • 有効期限のデフォルト値を設定する
  • 暗号化
    • サーバーサイド暗号化
      • サーバーサイド暗号化を有効にする
  • 追加設定
    • Password policy
      • Minimal length
      • Forbid common passwords
      • Enforce upper and lower case characters
      • Enforce numeric characters
      • Enforce special characters

この画面にて、ユーザーがパスワードを忘れてしまった場合に、ファイルを復元するためのリカバリキーを設定できるのですが、このNextCloudはあくまで一時的なファイル置き場のため、リカバリキーの漏えいのリスクを心配してあえて設定しません。それと、アプリの管理画面から不要と思われる大量のプラグインを無効化しました。NextCloudは、モバイル・デスクトップクライアントからアクセスできたり、外部ストレージに接続できたり、リッチなファイル閲覧・編集など、豊富な機能が特徴の一つですが、要件以外の機能は、リスクを減らすため徹底して排除となります。

Amazon RDS

NextCloudからログインする普通のMySQLデータベースとなります。

利用シナリオ

利用時のシナリオとなります。

【社員ユーザー】
①会社のパソコンのブラウザーでhttps://nextcloud.example.com:8000/を開きます
②IDとパスワードを入力してログインします
③画面内の「+」をクリックし「アップロード」を押下します
④ダイアログボックスで、アップロードするファイルを選択します
⑤共有するファイルの「共有アイコン」を押下します
⑥「URLで共有」をチェックします
⑦「URLによる共有のパスワード」を入力します
⑧表示されたURLリンクを共有先にメールで通知します
⑨パスワードをメール以外の安全な方法で共有先に通知します

【受け取り側ユーザー】
①通知されたURLリンクをブラウザーで開きます
②通知されたパスワードを入力してログインします
③ファイルをダウンロードします

Amazon EC2インスタンスのアップデート

yum-cron-securityパッケージをインストールしておくことで、自動的に1日1回、インストール済みのパッケージにセキュリティーアップデートがないかチェックされ、有れば、自動的にインストールとなります。それで、システムにトラブルが起きる可能性も有りますが、社内用ツール&一時的ファイル置き場ということで割り切ります。 カーネルがアップデートされてたら、再起動が必要なので、こちらからお借りしたスクリプトで実施となります。

おまけ

https://nextcloud.com/security/advisories/ のページの情報を監視して、更新があったら、対応します。

以上となります。

弊社にてNextCloudを導入した件について紹介させて頂きました。導入後は社内のニーズに対し、取り急ぎこのツールに誘導できるようになりました。


是非読者になってください(ง `ω´)ง


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

【初心者向け】レビュワーをイライラさせるRSpec集と解決方法

こんにちは。メドピアにjoinして3ヶ月目の保立です。
毎週のように新しい開発が進むため、毎日楽しくソースコードを書かせてもらっています。

テストコードを制するものは、Railsを制す!!!
ということで、今回はメドピアのRSpecについてです。
メドピアでは、RSpecを用いてテストコードを書いており、
- 1) models配下に記載するビジネスロジックに対するUnitTest
- 2) 機能ごとのEndToEndTest (E2E Test)
の2種類のテストコードを書いています。

RSpecについて、書こうと思ったきっかけ

RSpecは(というかRuby自体が)様々な書き方で動かすことができるため、統一したルールがないと、書いた人によってバラバラなテストコードになります。
私も初めてRSpecを書いた際に、参考書やソースコードによって書き方がバラバラで、どのRSpecを参考にすればよいのか迷いました。
RSpecソースコードの仕様を理解できると、レビュワーや後から開発する人のストレスが軽減できますが、テストコードごとのフォーマットがバラバラだと、テストコードを理解するのに、時間がかかってしまいます。(イライラする人が出てきちゃいます)
そこで、今回は、メドピア内部のルールや、今まで指摘を受けたこと、私が気をつけていることをまとめていきます。


目次

  1. describe/context/itのフォーマットが統一されていない
  2. インスタンス変数を使用している
  3. letで定義した変数名が、何を表しているかわからない
  4. beforeブロック内でテストを行う
  5. テスト対象が同じ、複数のテストケースで、subjectが使われていない
  6. 環境や時間によって失敗する

1. describe/context/itのフォーマットが統一されていない

テスト対象、条件、振る舞いが決まったところに決まったフォーマットで記載されていれば、そのテストコードが何を表しており、実際のソースコードがどのような仕様なのかを簡単につかむことができます。
下記は、フォーマットが統一されていない例です。

# contextが日本語だったり英語だったり式だったり
describe 'valid?' do
  context '非公開のとき'
  context 'publish'
  context 'expired < Time.now'  
end

# itに条件が書かれている
describe 'valid?' do
  it '◯◯で△△のとき、回答が登録されること'
end

テストの条件や結果のフォーマットが異ならないように、メドピアでは、以下のように記載しています。

内容 書き方
describe テストの対象 #インスタンスメソッド名
.クラスメソッド名
〜画面 〜処理(E2Eテスト)
context テストの前提条件 〜のとき
it テストの結果 〜されること 〜となること

また、なるべくcontextやitでは主語を書くようにしています。
context '非公開のとき' ではなく、context '質問が非公開のとき'のように書いています。

2. インスタンス変数を使用している

RSpecでは「インスタンス変数」(@から始まる変数)を使わず、「let / let!」を使って、テストで使用する変数を定義します。

# インスタンス変数を使用する
describe 'valid?' do
  before do
    @question = create(:question)
  end

  it 'is valid' do
    expect(@question).to be_valid
  end
end

「let / let!」を使うと、以下のように書き換えることができます。

describe 'valid?' do
  let(:question) { create(:question) }

  it '質問が登録されること' do
    expect(question).to be_valid
  end
end

インスタンス変数を使わない方がいい理由については、以下のリンクをご参考ください。


3. letで定義した変数名が、何を表しているかわからない

letで「インスタンス変数」の代わりに、テストを行う変数を定義できます。 しかし、定義した変数名が何を表すかが一目でわからないと、わかりやすいテストコードとは言えません。

describe 'valid?' do
  let(:question_1) { create(:question, title: 'タイトル') }
  let(:question_2) { create(:question, title: nil) }
  
  it 'タイトルが設定されている質問が、登録できること' do
    expect(question_1).to be_valid
  end

  it 'タイトルが設定されていない質問が、登録できないこと' do
    expect(question_2).to be_invalid
  end
end

このように、モデル名_1, 2 … というように変数名を設定すると、後から読んだ人は、どの変数が何を表しているのか理解するのに時間がかかってしまいます。
letで宣言する変数名には、以下のように何を表しているのかわかりやすい変数名を選ぶことを心がけています。

describe 'valid?' do
  # 基本パターンの場合、model名をそのまま変数名にすることが多いです。
  let(:question) { create(:question, title: 'タイトル') }
  # 例外パターンの場合、model名の後に例外内容を表す変数名にします。
  let(:question_without_title) { create(:question, title: nil) }
  
  it 'タイトルが設定されている質問が、登録できること' do
    expect(question).to be_valid
  end

  it 'タイトルが設定されていない質問が、登録できないこと' do
    expect(question_without_title).to be_invalid
  end
end


4. beforeブロック内でテストを行う

beforeは、テストの前提条件を用意するためのブロックです。
しかし、beforeがやるべきでないテストの実施を、beforeブロックに書かれていることもあります。

context '登録ボタンをクリックしたとき' do
  # before内でテストしているケース
  before do
    within '#button' do
      expect(page).to have_css '.disabled' # 非活性であることをチェック
    end
    fill_in 'name', with: '山田太郎'
    within '#button' do
      expect(page).to have_css '.enabled' # 活性であることをチェック
    end
    click_on '登録する'
  end

  # 登録処理のテストが続く 
  it '登録されること'
  it '画面遷移すること'  
end

上記の例のように、beforeでテストをしてしまうと、テストの前提条件がどこで何をテストしているのかわかりづらいだけでなく、before内のテストで失敗した際に、後続のテストが行われなくなってしまいます。

5. テスト対象が同じ、複数のテストケースで、subjectが使われていない

subject を使用すると、そのメソッド内のテスト内容を一括で設定することができます。

# subjectを使わない
describe '#human_time_distance' do
  context '現在時刻と一致するとき' do
    let(:from_time) { now }
    it { expect(helper.human_time_distance(from_time)).to eq '' }
  end

  context '現在時刻より前の時刻のとき' do
    let(:from_time) { now - 1.second }
    it { expect(helper.human_time_distance(from_time)).to eq '過去' }
  end

  context '現在時刻より後の時刻のとき' do
    let(:from_time) { now + 1.second }
    it { expect(helper.human_time_distance(from_time)).to eq '未来' }
  end
end

expect(helper.human_time_distance(from_time))subjectにまとめると以下のようになります。

describe '#human_time_distance' do
  subject { helper.human_time_distance(from_time) }
  
  context '現在時刻と一致するとき' do
    let(:from_time) { now }
    it { is_expected.to eq '' }
  end

  context '現在時刻より前の時刻のとき' do
    let(:from_time) { now - 1.second }
    it { is_expected.to eq '過去' }
  end

  context '現在時刻より後の時刻のとき' do
    let(:from_time) { now + 1.second }
    it { is_expected.to eq '未来' }
  end
end

特に、メソッドの返却値のテストやバリデーションのテストでは、積極的にsubjectを用いて、DRYなテストコードを心がけています。

6. 環境や時間によって失敗する

ローカルでテストした時には動いたけど、特定の環境ではたまに上手くいかないテストケースもよくあると思います。
メドピアでも、ローカルではうまくいくのに、CI上では3, 4回に1回くらい失敗するテストがありました。 以下の例は、メドピアでも比較的多かったケースです。

# let!でmodel作成後、テストまで時間がかかるケース
let!(:question_published) { create(:question, published_at: Time.current) }
let!(:question_non_published) { create(:question, published_at: Time.current + 1.second) }

before do
  # 色々処理が書かれる
  # 色々処理が書かれる
end

it '質問が表示されること' do
  # 色々テストが書かれる
  # 色々テストが書かれる

  expect(question_published).to be_valid
  expect(question_non_published).to be_invalid
end

上記の例では、published_atが現在時刻以降のquestionのみ表示するテストですが、1秒後に公開されるquestion_non_publishedを作成してからテストするまでにタイムラグが生じ、たまにうまくいかないことがあります。
このような場合は、テストを分割するか、1.secondをもっと大きな値に変更することで、解決できます。
常に成功するテストでないと、プロジェクト全体の生産性に影響を与えるので、現在の環境で常に動くテストコードを書く必要があります。



おわりに

今まで指摘されたこと、気をつけていることをまとめました。
他にも、Better Specs などを見て、勉強しています。 たかがテストコードですが、テストコードが読みやすいかどうかは、企業の文化によって大きく違うと思います。
RSpecのテストコードを書いたり、レビューする一助になれば幸いです。


是非読者になってください(ง `ω´)ง


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

form objectを使ってみよう

こんにちは。メドピアのRuby(Rails)化をお手伝いしている@willnetです。

Ruby化のプロジェクトが始まって1年が過ぎました。新しいメンバーも入り、Railsのコード量は日に日に多くなっています。可読性を保ちつつアプリケーションを大きくしていくために、使える知見をチームメンバーに効率よく伝えていくのが大事だと感じる今日このごろです。

普段メドピア内ではコードレビューや社内勉強会などで知識のシェアを行っています。そんなとき、ブログ記事や書籍などのまとまった文章があると「これ読んでおいて」と言うだけで良くなるので楽です。先日form objectを使ったほうがいいですよーという内容でレビューコメントをつけようとしたところ、日本語で詳しくまとまった文章が見当たりませんでした><なければ自分で書くしかありません。そこで今回はRailsにおいて可読性を保つための知見である、form objectについてまとめたいと思います。

form object って何?

form_withのmodelオプション*1にActive Record以外のオブジェクトを渡すデザインパターンです。form_withのmodelオプションに渡すオブジェクト自体もform objectと呼びます。

利点は大きく次の2点です。

  • DBを使わないフォームでも、Active Recordを利用した場合と同じお作法を利用できるので可読性が増す
  • 他の箇所に分散されがちなロジックをform object内に集めることができ、凝集度を高められる

具体例を見ていきましょう。

具体例

Rails 5.1.0でサービス管理者にフィードバックを返すフォームを作ってみます。フィードバック内容をデータベースに保存したい場合、素直に書くと次のようになるでしょう*2

class Feedback < ApplicationRecord
  validates :title, :body, presence: true
end

class FeedbacksController < ApplicationController
  def new
    @feedback = Feedback.new
  end

  def create
    @feedback = Feedback.new(feedback_params)
    if @feedback.save
      redirect_to home_path, notice: 'フィードバックを送信しました'
    else
      render :new
    end
  end

  private

  def feedback_params
    params.require(:feedback).permit(:title, :body)
  end
end
<%= form_with model: @feedback, local: true do |f| %>
  <% if @feedback.errors.any? %>
    <% @feedback.errors.full_messages.each do |message| %>
      <%= message %>
    <% end %>
  <% end %>
  <%= f.label :title %>
  <%= f.text_field :title %>
  <%= f.label :body %>
  <%= f.text_area :body %>
  <%= f.submit %>
<% end %>

ここでフィードバックをデータベースに保存せずに、サービス管理者へメールを送信するようにコードを変更してみましょう。データベースに保存しないので、ApplicationRecordを継承したモデルは使用しません。

そうしたときにこんな感じのコードを書いてしまう人が多いのではないでしょうか。

class FeedbacksController < ApplicationController
  def new
  end

  def create
    if params[:title].present? && params[:body].present?
      AdminMailer.feedback(params[:title], params[:body]).deliver_later
      redirect_to home_path, notice: 'フィードバックを送信しました'
    else
      @error_messages = []
      @error_messages << 'タイトルを入力してください' if params[:title].blank?
      @error_messages << '本文を入力してください' if params[:body].blank?
      render :new
    end
  end
end
<%= form_with url: feedbacks_path, local: true do %>
  <% @error_messages && @error_messages.each do |message| %>
    <%= message %>
  <% end %>
  <%= label_tag :title %>
  <%= text_field_tag :title, params[:title] %>
  <%= label_tag :body %>
  <%= text_area_tag :body, params[:body] %>
  <%= submit_tag %>
<% end %>

少し乱雑なコードになりました。まず、コントローラにロジックを記述してしまっています。また、独自のお作法でフォームを作成したため、ビューとコントローラを両方読まないとどのように動くのか、すぐに理解することができなくなりました。例えばビューとコントローラで@error_messagesという変数を利用していますが、どのような形式で情報が格納されているのか、どのように使用したらいいのかはビューとコントローラ両方を読まないと判断できません。

今回の例は項目数が少なく簡単なため、この程度であれば問題ないと判断する方もいるかもしれません。しかし、フォームの入力項目数が増えたり種類(セレクトボックス、チェックボックス、ラジオボタン)が増えたときのことを想像してみてください。コントローラが肥大化して読みづらくなり、ビューにどんなときに何が描画されるのかもわかりづらく、修正するのが大変になりそうですね。

これを解決するためにform objectを利用してみます。

form object を利用した例

form objectとして、ActiveModel::Modelをincludeしたクラスを用意します。 Active Modelは、Active RecordからDBに依存する部分を除いた振る舞いを提供しているライブラリです。これを利用することにより、DBを利用しないフォームでもActive Recordを利用したときと同じような記述をすることができます。

コードを見てみましょう。

class Feedback
  include ActiveModel::Model
  attr_accessor :title, :body

  validates :title, :body, presence: true

  def save
    return false if invalid?
    AdminMailer.feedback(title, body).deliver_later
    true
  end
end

class FeedbacksController < ApplicationController
  def new
    @feedback = Feedback.new
  end

  def create
    @feedback = Feedback.new(feedback_params)
    if @feedback.save
      redirect_to home_path, notice: 'フィードバックを送信しました'
    else
      render :new
    end
  end

  private

  def feedback_params
    params.require(:feedback).permit(:title, :body)
  end
end
<%= form_with model: @feedback, local: true do |f| %>
  <% if @feedback.errors.any? %>
    <% @feedback.errors.full_messages.each do |message| %>
      <%= message %>
    <% end %>
  <% end %>
  <%= f.label :title %>
  <%= f.text_field :title %>
  <%= f.label :body %>
  <%= f.text_area :body %>
  <%= f.submit %>
<% end %>

Feedbackクラスを除けば、Active Recordを利用した例と全く同じコードになりました。

このようにform objectを利用すると、Active Recordを利用した場合と同じお作法でビューとコントローラを記述することができて大変便利です。

個人的には、form_withでは必ずmodelオプションを利用する(form_tagを使わずにform_forを使う)という規則をチーム内に作ってしまってもよいのではないかと考えています。

その他の利用例

ここまでの文章を読むと「form objectはActive Recordを使わないフォームでだけ利用する」という理解をしてしまう方が多いのではないでしょうか。しかしform objectはActive Recordを利用する場合にも使えます。

例えば次のようなUserモデルがあるとします。

class User < ApplicationRecord
  has_secure_password

  validates :email,
            presence: true,
            format: { with: /\A.+@.+\z/ }
  validates :password, length: { minimum: 6 }, on: :create

  after_create_commit :send_welcome_mail

  def send_welcome_mail
    UserMailer.welcome(self).deliver_later
  end
end

よくある形だと思いますが、passwordのバリデーションやコールバックは、ユーザ作成時以外は必要のないものです。emailのバリデーションもユーザ作成時とメールアドレス変更時だけ必要なものなので、常に必要というわけではありません。

こんな時にform objectを利用してUserモデルに書かれたロジックを外出しすることができます。

class Signup
  include ActiveModel::Model

  attr_accessor :email, :password, :password_confirmation

  validates :email,
            presence: true,
            format: { with: /\A.+@.+\z/ }
  validates :password, presence: true, length: { minimum: 6 }, confirmation: { allow_blank: true }

  def save
    return false if invalid?

    user = User.new(email: email, password: password, password_confirmation: password_confirmation)
    user.save!
    UserMailer.welcome(user).deliver_later
    true
  end
end
class User < ApplicationRecord
  has_secure_password
end

Userモデルがスッキリしましたね。

他にも、複数のActive Recordを一度に保存するようなフォームなどでもform objectを活用することができます。

form objectをもっと活用する

これまでの例では、すべてのform objectにActiveModel::Modelをincludeしてきました。簡単なコード例なのでこれで十分だと思いますが、実際の現場でのコードはもっと複雑になります。そんなときActive Modelだけだと少し面倒に思う部分も出てくるはずです。

例えば先程のユーザ登録用のform objectに、サービス運営側からのダイレクトメールを受け取るか否かのチェックボックスを追加したとしましょう。Railsのビューヘルパーであるcheck_boxメソッドはチェックした場合は文字列の"1"、チェックしない場合は"0"がパラメータとして送信されてきます。Active ModelはActive Recordとは異なり、属性の自動的なキャストは行いません*3。booleanとして扱いたい場合、キャストの処理は自分で書く必要があります。

class Signup
  attr_accessor :email, :password, :password_confirmation, :accept_dm

  # 略

  def accept_dm
    ActiveRecord::Type::Boolean.new.cast(@accept_dm)
  end
end

signup = Signup.new(accept_dm: '1')
signup.accept_dm #=> true
signup = Signup.new(accept_dm: '0')
signup.accept_dm #=> false

独自にキャストしないといけない属性の数が多かったり、種類が豊富だったりしたら面倒ですね。そんなときに使える、属性の型を定義できるgemやform objectのためのgemがいくつか存在するので、form objectをバリバリ使ってみたくなった方は一度目を通しておくと良いと思います。

まとめ

form objectの利点や使いどころについて一通り解説しました。

form objectという概念自体は4~5年ほど前から広まっているはずなのですが、日本語でのまとまった文章がないことから、日本ではあまり普及していないような気がします。このエントリがform object普及の一助になって、読みやすいRailsのコードが少しでも増えることを願っています。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

*1:もしくはform_for

*2:ビューテンプレートのデザインは省略しています

*3:カラムの型情報がないので当たり前ですが

2泊3日の開発合宿 in 湯河原 おんやど 恵に行ってきました!!!!

こんにちは。メドピアに入社してもうすぐ1年の栢割(カヤワリ)です。
そろそろ開発合宿の時期がやって参りました!!!!弊社は2013年から開発合宿を定期的に行っており、今回で第8回目になります。
(自分は第6回目にも参加し、ブログも書かせて頂きました。)

tech.medpeer.co.jp
今回も私の方から合宿の様子を書かせていただきます。よろしくお願いします!!!!

合宿テーマ

開発合宿のテーマは毎回異なり、技術研鑽や技術的負債の解消、他にも新規サービス立ち上げ等を行ったこともあります。過去の開発合宿では新規サービス立ち上げをテーマに実施され、弊社のサービスとして事業化されているものもあります。

合宿に行く前に各自、取り組む内容について宣言してから開発します。今回はこんな感じ。

  • 社内の業務改善ツール開発
  • これまで懐に温めてきた医療系サービス開発
  • メドピア既存サービスに新機能追加
  • 話題の新技術を使って開発

取り組み方は自由でチームを組んでも良し。個人でモクモクと取り組むも良し。特に取り組み方について制約は無いので、自身が取り組みたかった開発がしやすいです。
しかし、ただ開発するだけでは自己満足で終わってしまうので、今回は最終日に会社へ戻り、成果発表LTをすることになりました。

エンジニアに嬉しい旅館「湯河原 おんやど 恵」さん

f:id:takayukikayawari:20170314143607j:plain さっそく足湯に浸かっている写真ですが、これを見てピンときた方いるのではないでしょうか?
今回の合宿は湯河原の「おんやど 恵(めぐみ)」さんにお邪魔しました。SE出身の社長さんが経営されている旅館で、エンジニアに嬉しい「開発合宿プラン」などが用意されています www.onyadomegumi.co.jp
当日は湯河原駅に直接集合!湯河原駅からタクシーで5~10分程度で到着しました。 f:id:takayukikayawari:20170314143650j:plain

お部屋が空くまで少し時間があったので、先に会議室へ…。
かなり広い会議室でした。合宿参加者は7人でしたが、この人数には広すぎるくらい。電源タップ、スピーカー、USB充電器、おやつ、ホワイトボード、腰が痛くならないグッズ等、設備品も充実してました。食事をしてる最中も、旅館の方が会議室の飲み物やお菓子の補充してくれます。ありがたや〜。(余談ですが、ディスプレイのレンタルプランもあるそうです。) f:id:takayukikayawari:20170315100928j:plain

基本的にお食事、お風呂、寝る時以外は開発に没頭します!!レッドブルかまして夜遅くまで開発するメンバーもいました。 f:id:takayukikayawari:20170314180547j:plainf:id:takayukikayawari:20170315104859j:plain お菓子も摘みながらモクモク…。 f:id:takayukikayawari:20170314180820j:plain

開発の合間には戦士の休息…。足湯でリフレッシュします。気持ちいいべ〜。もちろん温泉もあります。露天風呂は最高でした。 f:id:takayukikayawari:20170314181211j:plain

美味しいご飯レポート

旅館のご飯も美味しかったです。
(※ココから美味しそうな写真が続くので、ご飯時に見るとお腹が空いてきます。ご注意ください。) f:id:takayukikayawari:20170314181544j:plain f:id:takayukikayawari:20170314182243j:plain:w290f:id:takayukikayawari:20170314183252j:plain:w290f:id:takayukikayawari:20170314183653j:plain:w290f:id:takayukikayawari:20170315183027j:plain:w290 f:id:takayukikayawari:20170314183733j:plain:w290f:id:takayukikayawari:20170314183755j:plain:w290
宿の近くにあったラーメン屋さん。あさりラーメンが美味しかったです。 f:id:takayukikayawari:20170314182140j:plain f:id:takayukikayawari:20170314182005j:plain
お店を探している道中……
弊社のRailsプロジェクト開発で大変お世話になった株式会社 万葉さんが…!!??   f:id:takayukikayawari:20170314184527j:plain
……………お茶屋さんでした…。

そして最終日…成果発表へ!!

2泊3日の合宿を終え、成果発表のために会社へ戻ります。 f:id:takayukikayawari:20170314184148j:plain

お昼過ぎに会社に到着し成果発表LTをしました。弊社代表の石見をはじめ、エンジニア以外の方々も見に来てくれました。 f:id:takayukikayawari:20170315175325j:plain

まとめ

開発合宿は普段の業務でなかなか取り組めないタスクや負債を潰したり、新しい技術に集中して取り組める絶好の機会だと思います。 また、2泊3日という短い時間で成果を出す必要があるので、自身がこの短期間にどこまで出来るかを確認できる良い機会でもあると思います。次回も是非参加して、成果発表LTで皆さんに「おぉ!!」と驚かれる物を作りたいです…。

以上!2泊3日のメドピア開発合宿 in 湯河原 おんやど 恵の様子でした。

イマドキのジョブスケジューラについて考える

こんにちは。Ruby化をすすめるメドピアをお手伝いしている@willnetといいます。

メドピアではPHPからRubyに移行するにあたり、単純に言語を置き換えるだけではなく、言語以外の仕組みについても適宜見直しを行っています。今回はそのうちジョブスケジューラを見直した件について書いていきます。

言語を置き換えた話はこちらを参考にしてください。

レガシーな独自フレームワークから脱却してRailsへ徐々に移行している話 - メドピア開発者ブログ

そもそもジョブスケジューラってなに

「毎日1時になったら前日のアクセスログを集計して統計データとしてまとめる」などといった定期的に実行するジョブを登録するためのものです。

ウェブサービスを作るときのジョブスケジューラといったらやっぱりcronですよね。メドピアでもこれまでcronを活用していました。しかしサービスが小さいうちはcronでもそれほど問題ないのですが、サービスが育つにつれだんだん問題が顕になってきます。

cronの問題ってなに

以下列挙します

スケジュールをバージョン管理できない

crontab -e などで直接crontabファイルを編集した場合、変更の履歴が残りません。つまり以前のバージョンに切り戻したり、過去の履歴を振り返ったりすることができません。

余談ですがcrontab -eを打とうとしてeの隣のrを押してしまい、crontab -rでcrontabの設定を消去してしまった経験のある人はチームに一人くらいいるんじゃないでしょうか。

間違えやすい記法

crontabの記法は一見してわかりづらく、設定ミスをしやすいものになっています。

例えば次のようなcronを設定したとします。

5 2 1 * * /your/batch/command

これは毎月1日の2時5分に/your/batch/commandを実行するcronジョブですが、ぱっと見ただけで理解するのは難しくはないでしょうか。あとは*/10とか1-5,10-20とかの特殊記号を使ったときも、本当にこの書き方で想定通りの時間に実行できるのか不安になります。

cronサーバを分散できない

ウェブサービスで負荷が高まったときには、仮にアプリケーションサーバボトルネックであれば同じサーバを追加(スケールアウト)することで負荷を分散することができます。しかしcronサーバで同じことをすると複数のサーバで同じジョブが同時に実行されることになってしまいます。そのため大抵の環境においてcronサーバは1台だけで運用されているはずです。

開発が進みcronジョブが増えてくると、時間帯によっては複数のジョブが1台のcronサーバで並列に実行されて負荷が異常に高まり、予想しない挙動や障害につながっていきます。

デバッグしづらい

cronの環境と、普段使っているシェルの環境との違いでジョブが失敗することはよくありますが、特にそれを示唆するような出力はないことが多いです。そのためこの手の経験がないと「手元で実行すると動くのになぜかcronだと動かない!なぜ!?」のようにハマります。

ログを追いづらい

cronを実行した際の出力はメールで送信されますが、2017年の現在その機能を使っている方はあまりいないのではないでしょうか。普段は自前でログを頑張って出力したものを保持しておき、エラーが起きたときにはAirbrakeなどのエラー管理システムと連携して通知、ログを漁って原因を究明する…というケースが多そうです。

ジョブごとに成功/失敗のステータスやログがまとまっていると便利ですが、そこまで自前で実装するのは大変ですね。

どうやって問題を解決するか

cronの問題についていろいろ書きましたが、どのようにしたらこれらの問題を解決できるのでしょうか。cronそのものを改善させる方向と、cronをやめる方向で考えてみます。

whenever で cron を改善させる

これまで挙げたcronの問題のうちいくつかはwheneverというgemを利用することで改善可能です。使っている方も多いのではないでしょうか。

wheneverを使うと次のような記法でジョブをファイルに定義し、crontab用の記法に変換して登録することができます。

every 1.day, :at => '4:30 am' do
  runner "MyModel.task_to_run_at_four_thirty_in_the_morning"
end

このファイルをバージョン管理しておき、デプロイ時に自動でcrontabを更新するようにします。これで、バージョン管理の問題と、記法の問題を解決できました。

しかし、他の問題は変わらず残っています。

cron をやめる

調べると、cronを置き換えようとするプロジェクトはたくさんあることがわかります。

上記はなるべくRubyで作られたツールの中から選びました。つまりRubyにこだわらなければもっとたくさんの選択肢があるわけです。悩みますね><。

全ての問題を完全に解決するツールは存在しなかったので、次のような観点でツールを選定しました。

  • 適度に問題を解決していること
  • Ruby を使ったプロジェクトであること
  • メンテナンスが続いていること
  • 最悪メンテナンスが停滞しても自分たちでなんとかできること
  • 導入のしやすさ

結果として、sidekiq-cronを採用することにしました。

sidekiq-cron

sidekiq-cron はバックグラウンドジョブを扱うgemであるsidekiqを拡張し、cron的な機能を追加してくれるgemです。

主なメリットとしては、

  • sidekiqに相乗りする形で利用できるので導入が楽
  • ジョブ失敗時にリトライさせることができる
  • sidekiqのワーカーを増やすことで負荷分散できる
  • yamlでスケジューリングを定義するのでスケジュールのバージョン管理ができる
  • sidekiqがweb uiを用意しているので、ジョブの状態を確認できる*1

などがあります。慣れているsidekiqをそのまま使う感覚でいけるのがいいですね。

時間を指定する記法はcrontabと同じだったり、詳細なログ出力は独自で頑張る必要がありますが、cron単体の時よりはだいぶ前進できた気がします。

将来的にどうするか

sidekiq-cronは本番導入されており、今の段階では特に問題はないのですが、遅くても数年したら乗り換えを検討する必要がありそうです。

例えば複数のRailsアプリを管理するようになった場合に、複数アプリを横断したジョブスケジューラを管理したいという要望にはsidekiq-cronでは応えられません。また、ジョブの数が数十〜百になった場合にジョブをどうやって管理するかも悩ましいところですし、ログの出力が弱いことが問題となるケースも今後出てくるでしょう。

と、そんな折に昨年OSS化されたkuroko2を軽く触ってみたところ、なかなか良さそうでした*2。次回乗り換えを検討する際の有力な候補となりそうです。

kuroko2の詳細は以下のリンクを参考にどうぞ。

まとめ

開発しているアプリケーションの規模によって適切なツールは変わってきます。小規模なアプリケーションのジョブスケジューラであればcronはまったく問題ないと思いますが、アプリケーションが成長していくにつれてより適したツールに乗り換えていく必要があるでしょう。このエントリが次のツール選定の参考になれば幸いです。

*1:sidekiqの仕様上、WebUI上にログはほぼ残らないので、sidekiq-failuresを利用して失敗したジョブのエラーを閲覧できるようにしています。

*2:ツール選定当時はOSS化されていなかった

レガシーな独自フレームワークから脱却してRailsへ徐々に移行している話

みなさんこんにちわ。 メドピアでエンジニアをやっている内田と申します。

現在メドピアではPHPで作られたレガシーな独自フレームワーク (以下FW) からRailsへと移行するプロジェクトが進んでいます。 今回は移行に向けて行ったことについて共有したいと思います。

移行の計画

メドピア株式会社では、医師限定のコミュニティサイト「MedPeer 」を運営しています。 「MedPeer 」サービス内では、薬剤評価掲示版、症例相談、Forum、ニュースなど、医師同士が情報交換をするための、機能の異なる複数のサービスを提供しています。

それらサービスの内部では7年前に作られたPHPの独自FWが採用されており、コードが肥大化したことで機能の変更や追加がとても困難になっていたことが課題でした。

そうした課題を解決するために、アーキテクチャの見直しを含めたリプレースがエンジニアの主導で計画されました。 様々な言語を比較する中で、生産性の高さやコミュニティが活発なところに魅力を感じて言語はRubyへと決め、FWは独自で作るよりもOSSの方がより多くの開発者によって開発が行われているため、新しい機能が使えたり安定性が高いといったメリットがあるだろうと考え、Railsへと移行することに決めました。

しかし、移行するにしても「MedPeer」はとてもモノリシックな上に、機能によってはもはや誰も知らないコードなども存在しており、一筋縄ではいきません。 一度に全てを移行するという方針だと数年はかかりそうです。その間ビジネスを止めるわけにはいかないので、移行の間も新サービスの開発だったり機能の追加を常に行っていく必要があります。

そこで、既存の機能追加は今まで通りレガシーな旧環境で開発しつつ、新サービスやサービスごと移行したいものに関してはRailsの新環境で開発していく、という方針を取ることにして、両環境を運用しながら徐々に移行することにしました。そのためにまずは旧環境と新環境2つを連携して並列稼働させる仕組みが必要でした。

やったこと

具体的にやったことは以下の3つです

  • urlごとの振り分け

  • 共通処理の切り出し

  • データの同期

urlごとの振り分け

urlの振り分けは、nginxを使ったリバースプロキシー先の振り分けです。ドメインは両環境で同じにしてたとえば、/aaaときたurlは旧環境に、/bbbときたurlは新環境にと振り分けるようにしました。 幸いMedPeerは前述の通り複数のサービスで成り立っておりサービスごとにurlが異なり依存度も少ないためこの方法は合っていました。 注意点としては両環境でurlがバッティングしないように設計する必要があったり、同一ドメインであるためcookieの扱いに気をつける必要があることです。

f:id:yutauchida:20170130140305p:plain

共通処理の切り出し

両環境にて共通に処理する必要のある機能が存在します。たとえば認証です。認証したら環境をまたいでもセッションをセキュアに維持できる仕組みが必要です。 これに対しては既にAPIゲートウェイとして認証部分を別のアプリケーションとして切り出していたためそれを新環境でも利用するようにしました。こういった共通処理を切り出しておくと他サービスでも連携しやすいといったメリットがあります。実際にメドピアの他サービス(キャリア、イシコメ)でも認証部分はAPIゲートウェイを利用しており、サービスのスケールに寄与しています。APIゲートウェイを開発した際の話についてご興味ある方はこちらをご参照ください。

Golang(Go言語)を採用して、たった二人で基盤となるAPIゲートウェイを開発した話 - メドピア開発者ブログ

データの同期

両環境ではユーザーテーブルなど共通のデータを利用する必要があります。 当初は現行のDBを共用することも考えましたが以下の理由でやめました。

  • テーブルの設計がRailsに最適化されていない

  • 移行を機にDB構造を見直したい

RailsはDBに対してある一定の規約があります。例えば主キーはidが望ましいといったことです。 旧環境のDBでは主キーがid以外に設定されていたり、テーブル名が複数形になっていないなどRailsを利用する にあたって最適化されていないという問題がありました。また、長年DBを運用していて設計上見直したい部分が多々ありました。例えば 以下の内容です。

  • 不要なテーブル、カラムが存在している

  • カラム名が適切でないため理解しにくい

  • 正規化が適切にされていない

  • indexやunique制約が適切に貼られていない

移行をいい機会としてこれらを見直して再設計したいこともあり、DBは新しく作るという方針を立てました。 そのために2つのDBのデータを同期する必要があったため、両環境のDBを橋渡しする役割のアプリケーションを作りました。 それがDB-SYNCです。

f:id:yutauchida:20170130152525p:plain

DB-SYNCは旧環境のデータを新環境でも使いたい場合にデータを任意の形にコンバートします。例えば旧環境であるユーザーのEmailが更新されたとします。 その際に更新があったことを通知するテーブルに登録されます。DB-SYNCはcron&キューの処理としてそのテーブルをreadして更新のあった対象のテーブル とデータを判別します。その後、旧環境のDBから対象のデータをFetchして、新環境のDBに新しく設計されたテーブルのどのカラムに対応するのか を変換ルールが定義されたファイルを参照しながらUPSERTを行います。変換ルールが定義されたファイルは例えば以下のような内容が記載されます。

user:
  old_table: portal.users
  column_name_mappings:
    id:              old_user_id
    last_login_time: last_logged_in_at
    account_id:      email
    state_cd:        registration_status
    profession_id:   profession_type
    physical_deletable: true
    unsync_columns:
      - id

DB-SYNCでは命名の変更だけでなく、2つのテーブル を統合して再設計した一つのテーブルにデータを登録していくといったことも可能になっています。その他のポイントとしてはデータの流れは 旧環境から新環境への一方向のみとしています。理由としては、両環境でデータが行き来すると煩雑になり障害発生時に原因の究明に時間がかか るのではと考えたのと、逆方向へのデータ同期によりそれを使う機能を開発する要件があるならば、それは新環境での開発を促すべきだというポリシ ーのもとで、そのようにルール化しました。

まとめ

以上のような施策を行い2016年の夏にRailsで作られた新システムがリリースされており、現在は2つの環境が安定して動いています。 徐々に移行することで比較的小さいリリースとなるので大きな障害が起きにくいといった安心感があるのと、Railsでの開発ができるようになったので楽しんで開発しているエンジニアが見られるようになったのはポジティブな要素だと感じています。

また、Railsアプリケーションの立ち上げについてはパーフェクトRuby on Rails共著の前島さんを始め、株式会社 万葉のエンジニアさんなどRails に知見があるメンバーに推進してもらいました。今後規模が大きくなると予想されるアプリケーションで新しい言語やFWを採用するに あたって経験豊富なエンジニアにしっかりとした土台を作ってもらい、その上で開発していくことが長く運用していくうえで大事だと考えています。

近頃、大規模アプリケーションをRails化しているという案件を聞くことが多くなった気がします。そういった計画を考えている方に弊社 の事例が一つでも参考になれば幸いです。