メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックリーディングカンパニーのエンジニアブログです。PHPからRubyへ絶賛移行中!継続的にアウトプットを出し続けられるようにみんなでがんばりまっす!

Railsの太ったモデルをダイエットさせる方法について

こんにちは。メドピアのRuby(Rails)化をお手伝いしている@willnetです。最近はよくリファクタリングをしています。

今回は、最近僕がリファクタリングしている内容についてまとめようと思います。

メドピアではFat Model/Controllerを避けるために、rubocopの設定を利用しクラスの行数が300行以下になるよう制限しています*1。最近300行を超えるモデルが出てきたので、一部の処理を別のクラスに切り出し始めました。

このとき、Railsが提供している機能であるconcernsを利用すると楽に行数を減らすことができますが、それだとrubocopの指摘を回避できるという意味しかないので、なるべく委譲(composition)を利用して処理を別クラスに移していっています*2

複数モデルにまたがる処理を切り出す

Railsアプリケーションを書いていると、複数のモデルを一度に使った処理を書きたいケースがあります。その場合は処理をどこに書くのが適切でしょうか?

多くのwebサービスではユーザが起点となる処理が多いので、結果としてUserモデルにメソッドが集まってしまいUserモデルがどんどんFatになります。数百行のレベルで済めば良い方で、千行を超えるUserモデルを持つプロジェクトも多いのではないでしょうか。行数が多いクラスは内容を理解するのが大変ですし、コードを修正した際の影響範囲もすぐにはわかりません。こうなるとコードに触れるのが苦痛になってきます。

これを解決するには、Active Recordを継承しないPORO*3なモデルを作成し、処理を委譲します。

例として、Userモデルに「ブログを投稿して友達に通知をする」メソッドを書いてみます。

class User < ApplicationRecord
  has_many :posts
  has_many :friendships
  has_many :friends, through: :friendships
  
  def create_post_with_notifications!(body)
    transaction do
      posts.create!(body: body)
      friends.each do |friend| 
        friend.notifications.create!("#{name}さんが投稿しました")
      end
    end
  end
end

これはUserモデルに置かれることの多い典型的なメソッドです。Userモデルが投稿と友達の関連元になるので、一見収まりがよく見えます。開発初期でUserモデルが小さいときはとくに問題になりませんが、開発が進むにつれこのようなメソッドが大量に存在するようになり、邪魔になってきます。

そこで、「ブログを投稿して友達に通知をする」という単一目的のクラスを作ってみます。

class PostWithNotifications
  def self.create!(creator:, body:)
    new(creator: creator, body: body).create!
  end
  
  def initialize(creator:, body:)
    @creator = creator
    @body = body
  end
  
  def create!
    ActiveRecord::Base.transaction do
      create_post!
      create_notifications!      
    end    
  end
  
  private
  
  attr_reader :creator, :body
  
  def create_post!
    creator.posts.create!(body: body)
  end
  
  def create_notifications!
    creator.friends.each { |friend| create_notification!(friend) }
  end
  
  def create_notification!(friend)
    friend.notifications.create!("#{creator.name}さんが投稿しました")
  end
end

処理の内容的には以前と変わっていません。ただ、メソッドを切り出したついでに多少リファクタリングしています。単一目的のクラスで管理することにより、メソッドを抽出するなどのリファクタリングがより簡単になりました。

PostWithNotificationsクラスを作ったことにより、メソッド呼び出しがuser.create_post_with_notifications!('投稿内容')からPostWithNotifications.create!(creator: user, body: '投稿内容')に変更されました。インターフェースを変えずに処理を切り出したい場合は、次のように、元のUser#create_post_with_notifications!から処理を委譲するようにするとよいでしょう。

class User < ApplicationRecord
  # 略
  
  def create_post_with_notifications!(body)
    PostWithNotifications.create!(creator: self, body: body)
  end
end

複数のレコードを扱う処理を切り出す

単一のモデルを取り扱う場合でも処理を書く場所に困る場合があります。それは複数のレコードを取り扱うケースです。複数のレコードを取り扱うために、モデルのクラスメソッドに処理を書く場合が頻繁に見られます。モデルが小さい場合はそれでも問題ないですが、先程と同様開発が進むにつれモデルの見通しを悪くする要因になります。そもそも、Active Recordは本来レコードとオブジェクトを一対一でマッピングするデザインパターンなので、複数のレコードを扱うクラスは別に用意するのが適切です。これも先程の例と同じくPOROを使うことで解決できます。

例として、複数のメッセージを既読にする処理を考えてみます。

class Message < ApplicationRecord
  enum status: %i[unread read]
  
  def self.read!(messages)
    messages.unread.each(&:read!)
  end

  # たくさんのメソッド
end

(この例だとMessage.read!メソッドは十分小さいのであまり切り出す必要性は感じないかもしれません。もっと長いメソッドを想像して読み替えてください)

このMessage.read!メソッドはメッセージを扱うので一見妥当な場所に存在するように感じます。しかし、複数レコードを取り扱うクラスを作り委譲させることで、より見通しがよくなるケースが多いです。

class Message::Collection
  def self.read!(messages)
    new(messages).read!
  end

  def initialize(messages)
    @messages = messages
  end

  def read!
    @messages.unread.each(&:read!)
  end
end

Message::Collectionというクラスを作りメソッドを切り出しました。これで開発が進みread!メソッドが複雑になったとしても、全体を容易に把握できるはずです。

単機能として切り出せる処理を切り出す

ここまでの内容に沿って複数モデル、複数レコードの処理をPOROに切り出したあとは、Active Recordのモデルに書かれている処理は基本的に自分のモデルに関わることだけになっているはずです。それでも行数が多く取扱いに困るときは機能ごとにPOROに切り出しましょう。

どういう機能を切り出すべきかはモデルごとに判断するしかないのですが、例えばバッチ用の処理などは共通して切り出しやすいです*4

Active Recordのモデルに対してバッチ処理用のメソッドを生やすよりは、バッチ処理専用の小さいクラスを作ってしまった方が取扱いが楽なケースが多いです。

次のような、未読のメッセージがあったときにユーザにメールを送るようなメソッドがあるとします。

class User < ApplicationRecord
  # ...
  
  def self.notify_unread_messages
    User.active.find_each do |user|
      next unless user.messages.unread.exists?
      UserMailer.not_read_messages(user).deliver_now
    end
  end
end

これをバッチ用のクラスに移動させると次のようになります。

class UnreadMessagesNotification
  def self.notify(user: nil)
    new(user: user).notify
  end

  def initialize(user: nil)
    @user = user || User.active
  end

  def notify
    user.find_each do |user|
      next unless user.messages.unread.exists?
      UserMailer.not_read_messages(user).deliver_now
    end  
  end

  private

  attr_reader :user
end

メソッドを移動させたことでUserクラスからUser.notify_unread_messagesを削除できました。それだけではなく、メール送信の対象となるユーザを外部から注入できるようにしたことで、テストをしやすくなっています。

まとめ

POROを利用してモデルを小さく保つ方法について紹介しました。Railsの初心者は特に「モデルとはActive Recordのことだ」と考えがちです。その固定観念から抜け出すことで、可読性の高いアプリケーションを書く入り口に立てるのではないかと思います。この記事によって少しでも読みやすいRailsアプリケーションが増えることを願っています。


是非読者になってください(︎ ՞ਊ ՞)︎


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

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

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

■開発環境はこちら

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

*1:コントローラで300行は多すぎるので、いずれコントローラの場合は100行以下にしたいと思っています

*2:concernsについてはまた別の機会に書くかもしれません

*3:Plain Old Ruby Objectの略で、Active Recordなどを継承していないふつうのオブジェクトのことを指します

*4:この話の前提として、「基本的にバッチ用の処理はモデルに書き、rakeタスクはモデルのメソッドを呼び出すだけ」という慣習があります。これにより、処理内容がわかりやすく、かつテストが書きやすくなる効果があります

Webpackerへの移行を機にフロントエンド開発を改善

こんにちは。メドピアでエンジニアをしている村上(pipopotamasu · GitHub)です。 普段はRailsを触っていますが、時々フロントエンド周りの開発もしています。 今回はメドピアの環境におけるWebpacker導入とフロントエンド周りの改善をテーマに記事を書きます。

目次

  1. なぜWebpacker(Webpack)を導入するのか?
  2. フロントエンド改善計画
  3. Webpackerの導入で気をつけたこと

github.com

なぜWebpacker(Webpack)を導入するのか?

Webpackerを導入する背景として、主に2つの課題がありました。

  • JavaScriptのビルド時間が長い

  • パッケージのバージョン管理ができない

JavaScriptのビルド時間が長い

元々メドピアのフロントエンド開発においてBrowserifyというbundlerを使用していました。 しかし、これにはビルド時間が長いという弱点があります。 特に以下の2つの場面でその長さが目につきます。

  • CI, デプロイ, 環境構築時に走るassets:precompileのビルド

  • 開発時のJavaScriptのビルド

現状だと前者はビルドだけで10分近くかかってしまい(JavaScriptだけでなくcssなどの他のアセットのビルドも時間がかかるという理由もありますが)、 後者はBrowserifyと一緒に使っているbabel-plugin-transform-runtimeの実行に時間がかかり、JavaScriptの開発中は3秒ほどの時間がかかります。

依存パッケージのバージョン管理ができない

メドピアの環境のnpmのバージョンが3系であるため、パッケージのバージョンを固定するpackage-lock.json(npm5系から登場)がありません。 そのため、RailsのGemfile.lockのように依存パッケージのバージョンが管理できないという問題があります。


これら2つの課題はWebpackerを導入することで一気に解決されます。

JavaScriptのビルド時間が長い → ビルド時間の短縮

BrowserifyよりWebpackの方がビルドが早くなることのベンチマークは、以下の記事を参考にさせていただきました。

perkframework.com

Browserifyとの比較がとてもわかりやすく掲載されています。 唯一、全くの0からのビルド(Fresh build)がBrowserifyが速度的に上回っていますが、そのようなビルドをするのは初回の環境構築時くらいなのでほとんど気にする必要はないでしょう。

依存パッケージのバージョン管理ができない → yarnによりバージョン管理可能に

Webpacker導入により(というよりWebpackerが依存するyarnというパッケージ管理システムにより)、上記のpackage-lock.json, Gemfile.lockのようにyarn.lockで依存パッケージのバージョン管理ができるようになります。

他にもJavaScript以外のアセットファイルのビルドができたりエコシステムが充実しているという点はBrowserifyからWebpackへの乗り換えで大きな利点でもあります。

このような背景からメドピアではWebpackerを導入することとなりました。

フロントエンド改善計画

しかし、いきなり全てのビルドをBrowserifyからWebpackerに置き換えるということはできません。 今まで書いてきたJavaScriptのコード量が多いため、それをWebpacker用の領域(app/javascripts)に移し替えるのにテストを含め時間がかかるからです。 またせっかく移行するなら、同時にフロントエンドをもっと改善していくチャンスでもあります。 具体的な改善点としては...

  • CommonJSだった部分をES Modulesに置き換える

  • Vue.jsでデータバインディングのみでしか使用していなかったところを、単一ファイルコンポーネントも活用する

  • 単一ファイルコンポーネントのLintを導入する

  • 最新版パッケージへ継続的なUpdate体制の確立

などがあります。 ここで、それぞれの改善点の詳細なポイントについてみていきましょう。

CommonJSだった部分をES Modulesに置き換える

ES Modules(以下esm)に置き換えるメリットとしては個人的には大きく以下2つだと思います。

  1. 実行前にモジュール読み込みのエラーを検知できる

  2. ツールを使うことでコードの最適化ができるようになる

実行前にモジュール読み込みのエラーを検知できる

esmはCommonJSと違い静的構文であるため、コードの実行前に構文解析が走ります。 これにより、開発者はより早い段階で間違いに気づくことができます。

# CommonJS
## module.js
function hoge() {
  return 1 + 1;
}
exports.module = hoge;

## main.js
const fuga = require('./module').fuga
console.log(fuga); // undefined


# esm
## module.js
export function hoge() {
  return 1 + 1;
}

## main.js
import { fuga } from '.module' // <= ここでSyntax Errorが発生


ツールを使うことでコードの最適化ができるようになる

esmを使うことで、コードの最適化ができるようになります。 例えばWebpackにはTree Shakingという機能があります。 ESモジュール形式で書かれたコードをbundleして一つのファイルにする時に、exportしているけどどこからもimportされていない、使われていないコードを削除する機能のことです。

https://webpack.js.org/guides/tree-shaking/

Edgeでもexport/import時にコードの最適化がされるようです。

Previewing ES6 Modules and more from ES2015, ES2016 and beyond - Microsoft Edge Dev BlogMicrosoft Edge Dev Blog

またCommonJSはいわゆるサードパーティなのに対し、esmはECMAScriptで定義されるJavaScriptの標準であるということも置き換えの理由です。

単一ファイルコンポーネントの活用

メドピアではJavaScriptのフレームワークとしてVue.jsを使用しています。Vue.jsの機能として、Vue.jsを適用させるテンプレート(HTML)とJavaScriptを同じファイル内に記述する単一ファイルコンポーネントというものがあります。 単一ファイルコンポーネントを使用するメリットはいくつかありますが、最も大きな理由は可読性の向上です。 可読性の向上については3つのポイントがあります。

  1. シンタックスハイライト

  2. 同一ファイル内にテンプレートとテンプレートに適用するJavaScriptを書くことができる

  3. テンプレート内でES6が使用できる

シンタックスハイライト

単一ファイルコンポーネントを使用しない場合、HTMLファイル内にVue.jsのコードを書く必要があります。 メドピアではViewテンプレートにHamlを採用しているため、Hamlファイル内にVue.jsのコードを書いています。 しかし、Hamlファイル内にVue.jsのコードを書いてもシンタックスハイライトがHamlのコードにしか適用されないため非常に見辛いです。

[単一ファイルコンポーネントのテンプレート] f:id:ec0156hx39:20171027113801p:plain

[Haml内に書いたテンプレート] f:id:ec0156hx39:20171027123647p:plain

上のように、単一ファイルコンポーネント内のテンプレートは見やすくハイライトされ(※お使いのエディタでハイライトのプラグインを入れる必要があります)、一方Haml内に書いたテンプレートはHamlのシンタックスしかハイライトされないため見づらくなってしまいます。

同一ファイル内にテンプレートとテンプレートに適用するJavaScriptを書くことができる

単一ファイルコンポーネントの最大の特徴です。 1ファイル内にHTML, JavaScript(CSSも)が記述できるのでどのJavaScriptがどのHTMLに適用されているかを容易に知ることができます。

f:id:ec0156hx39:20171027124642p:plain

テンプレート内でES6が使用できる

Hamlファイル内ではES6のJavaScriptのコードがトランスパイルされないので古いブラウザのサポートをする必要がある時は使用することができません。 ES6を使いたい場面(特にv-bind時の文字列テンプレートの使用)で使えないことにより、可読性が落ちる場合があります。 以下はinputタグに動的なclassをつける時の例になります。

[単一ファイルコンポーネントのテンプレート] f:id:ec0156hx39:20171027134313p:plain

[Haml内に書いたテンプレート] f:id:ec0156hx39:20171027134321p:plain

単一ファイルコンポーネントのLintを導入する

単一ファイルコンポーネントはHTMLやJavaScriptのLinterが使用できないため、新たに専用のLinterが必要です。 これの導入により、コードの質の担保・コーディングルールの統一を実現します。

最新版パッケージへ継続的なUpdate体制の確立

パッケージは定期的にアップデートしないと差分が大きくなり、いざアップデートしようとすると大怪我をする恐れがあります。 幸いにメドピアではRubyのGemを定期的にアップデートする体制ができているため、それと合わせてパッケージをアップデートしていく体制にしていきたいです。



などなどWebpacker導入を機に、よりJavaScriptの開発をモダンにしていきたい野望があります。

そこで、その野望を実現するために移行計画を立てました。

  1. Webpackerの導入
  2. 一部BrowserifyでビルドしているコードをWebpackerに移植すると同時に単一ファイルコンポーネントのLintを導入する
  3. 徐々にBrowserifyでビルドしているコードをWebpackerに移していくかつCommonJSをES Modulesに置き換え
  4. Webpackerへの完全移行とともにBrowserifyのアンインストール
  5. yarn upgrade体制の確立

Webpackerの導入で気をつけたこと

最後にメドピアにおけるWebpackerの導入で気をつけた部分を共有します。

Docker用の設定

メドピアでは開発環境にDockerを利用しています。 しかし、デフォルトのWebpackerの設定ではhostがlocalhostに設定されているため、ホストOSのブラウザからDocker上の開発環境にアクセスできないため以下のような設定が必要です。

https://github.com/rails/webpacker#development


# webpacker.yml

  dev_server:
     host: localhost # <= ここを0.0.0.0に変更
     port: 3035
     hmr: false
     https: false

config/webpack/environment.jsの拡張

Webpackerの2系ではplugin, loader, aliasの設定はconfig/webpack/shared.jsに記述すればよかったのですが、3系からshared.jsが廃止され、この辺りの設定がnode_modules以下の@rails/webpackerに格納されています。 もちろんalias等の設定を直接node_modules以下に追記することはできません。そのため、今回はwebpack-mergeを使ってconfig/webpack/environment.jsにそれらの設定しました。

# environment.js

const { environment } = require('@rails/webpacker')
const merge = require('webpack-merge')

module.exports = merge(environment.toWebpackConfig(), {
  resolve: {
    alias: {
      vue: 'vue/dist/vue.js'
    },
  },
});

今回はaliasだけですが、plugin, loaderなどの設定もここに追加していけばwebpackの設定がうまいことできそうです。

終わりに

まだまだモダンな環境への移行は道半ばですが、今後も変化の激しいフロントエンドの技術に追従できるように環境の改善及び技術力の向上に努めていきたいと考えています。 その一貫として、メドピアでは毎週火曜日の19:30〜Vue.jsのもくもく会を開催しています。 是非ともご参加あれ!

またRailsエンジニア、フロントエンドエンジニアを絶賛募集中ですので少しでも興味を持った方は一度メドピアに遊びに来てください!


是非読者になってください(☝︎ ՞ਊ ՞)☝︎


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

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

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

■開発環境はこちら

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

生きて腸まで届くマイグレーションツール Phinx

こんにちは。メドピアCTO室 @kenzo0107 です。

Join して4ヶ月、
日々新たな技術に触れ、赤い実弾かせながら過ごしております。

今回は開発環境 DB をコンテナ化した際に使用した Phinx についてです。

Phinx って何?

f:id:kenzo0107:20170829132120p:plain phinx.org

  • PHP (>=5.4) でコーディングできるDBマイグレーション&シードツール
  • composer 管理
  • CakePHP 2.x 系の schema.php の様なファイルを作る必要がない
  • DB の向け先は yml で複数管理可能
  • F/W に依存しない
  • MySQL, PostgreSQL, SQL Server, SQLite に対応


Phinx 採用経緯

メドピアでは以下の様な課題を鑑みてマイグレーションツールを模索していました。

  • PHP 5.x系 で運用している独自F/Wがある*1
  • 既に DB が大規模 (スキーマ数 50程度)
  • シード機能も欲しい
  • マイグレーション/シード機能のないF/Wで運用しているプロジェクトにも適用可能であればしたい
  • PostgreSQL, SQL Server で運用しているプロジェクトにも適用可能であればしたい

元々、
個々人用の開発 DB を本番 DB からセキュリティ上データをマスクした上で
同期する様な機能も検討していましたが
必要最低限で開発ができる状態であれば良いという総意から
シード機能も合わせて求める様になりました。

Phinx はこれらの課題を網羅しており、軽量で使い勝手がよかった為採用に至りました。


Phinx は初めてという方、
既に比較検討されている方、
といらっしゃると思いますので簡単に使い勝手を試していただく意味でも
弊社の利用方法と合わせて実践チュートリアルとして git を用意しました。

検証環境

  • OSX 10.12.5
  • Vagrant 1.9.3
  • VirtualBox 5.1.18
  • Docker version 17.03.1-ce, build c6d412e
  • docker-compose version 1.11.2, build dfed245

やること

  • 複数の DB スキーマへのマイグレーション実行
  • 複数の DB スキーマへのシード実行

システム概要図

Docker on Vagrant で動作確認していきます。

f:id:kenzo0107:20170829161035p:plain

無事 Moby Dock 達の抱える DB 達にデータが届くか試してみたいと思います。

事前準備

macOS%$ git clone https://github.com/medpeer-inc/phinx
macOS%$ cd phinx
macOS%$ vagrant up
macOS%$ vagrant ssh
vagrant%$ cd /vagrant

Adminer, Phinx, DB コンテナ起動

vagrant%$ docker-compose up --build -d
vagrant%$ docker-compose ps

        Name                      Command               State            Ports
---------------------------------------------------------------------------------------
      Name               Command              State               Ports
-----------------------------------------------------------------------------
        Name                      Command               State            Ports
---------------------------------------------------------------------------------------
vagrant_adminer_1      entrypoint.sh docker-php-e ...   Up       0.0.0.0:80->8080/tcp
vagrant_db-migrate_1   phinx --help                     Exit 0
vagrant_mysql_1        docker-entrypoint.sh mysqld      Up       0.0.0.0:3306->3306/tcp
vagrant_pgsql_1        docker-entrypoint.sh postgres    Up       0.0.0.0:5432->5432/tcp
vagrant_sqlsvr_1       /bin/sh -c /opt/mssql/bin/ ...   Up       0.0.0.0:1433->1433/tcp


  • db-migrate コンテナは one-off コンテナとして利用する為、Exit 0 で問題ありません。ビルドするのが目的です。
  • DB は MySQL, PostgreSQL, MSSQL を用意しました。


今回は MySQL をメインに進めたいと思います。

0. DB作成

MySQL, PostgreSQL コンテナではコンテナ起動時に
hogehoge, mogemoge の 2つの DB Schema を作成する様設定しています。

  • docker-compose.yml
...
  mysql:
    image: mysql:5.7
    environment:
    - MYSQL_ROOT_PASSWORD=rootpass
    - MYSQL_DATABASE=hogehoge
    - MYSQL_USER=developer
    - MYSQL_PASSWORD=pass
    volumes:
    - db-data:/var/lib/mysql
    - ./db/conf.d/my.cnf:/etc/mysql/conf.d/my.cnf
    - ./db/initdb.d:/docker-entrypoint-initdb.d
    ports:
    - 3306:3306


  • db/initdb.d/01_structure.sql
CREATE DATABASE `mogemoge`;

MySQL, PostgreSQL 公式 Docker コンテナでは
/docker-entrypoint-initdb.d 以下の SQL を起動時に実行する為
そちらに DB mogemoge を作成するよう設定しました。

これより 各 DB にテーブルを作成していきます。

1. テーブル作成

テーブル定義ファイル作成

DB hogehogeusers テーブルを
DB mogemogemembers テーブルを
作成する Phinx 定義ファイルを作成します。

定義ファイルのクラス名はキャメル形式限定です。

$ make migrate_create DB=hogehoge CLASS=CreateTableUsers
$ make migrate_create DB=mogemoge CLASS=CreateTableMembers
...
...
created db/migrations/hogehoge/20170724065658_create_table_users.php
created db/migrations/mogemoge/20170724065738_create_table_members.php

db/migrations 内に各 DB 毎のディレクトリが作成され、その配下に Phinx 定義ファイルが作成されているのが確認できます。

テーブル定義ファイル編集

  • db/migrations/hogehoge/20170724065658_create_table_users.php
<?php

use Phinx\Migration\AbstractMigration;
use Phinx\Db\Adapter\MysqlAdapter;

class CreateTableUsers extends AbstractMigration
{
    public function up()
    {
        // 自動生成される id を排除し、primary key を user_id とする
        $t = $this->table('users', ['id' => 'user_id']);

        $t->addColumn('last_name',       'string',     ['limit' => 10,  'comment' => '姓'])        // string 型 20文字制限
          ->addColumn('first_name',      'string',     ['limit' => 10,  'comment' => '名'])        // string 型 20文字制限
          ->addColumn('last_kana_name',  'string',     ['null' => true, 'limit' => 10,  'comment' => '姓(カナ)']) // string 型 NULL許可 10文字制限
          ->addColumn('first_kana_name', 'string',     ['null' => true, 'limit' => 10,  'comment' => '名(カナ)']) // string 型 NULL許可 10文字制限
          ->addColumn('username',        'string',     ['limit' => 20,  'comment' => 'ユーザ名'])   // string 型 20文字制限
          ->addColumn('password',        'string',     ['limit' => 40,  'comment' => 'パスワード']) // string 型 40文字制限
          ->addColumn('email',           'string',     ['limit' => 100, 'comment' => 'Email'])    // string 型 100文字制限
          ->addColumn('postcode',        'string',     ['limit' => 10,  'comment' => '郵便番号'])   // string 型 10文字制限
          ->addColumn('birthday',        'date',       ['comment' => '誕生日'])                    // date 型
          ->addColumn('gender',          'integer',    ['limit' => MysqlAdapter::INT_TINY, 'comment' => '性別(1:男 2:女 3:その他)']) // tinyint 型
          ->addColumn('card_number',     'string',     ['null' => true, 'limit' => 20,  'comment' =>'クレジットカードNo'])  // string 型 20文字制限 NULL許可
          ->addColumn('description',     'string',       ['null' => true, 'limit' => 255, 'comment' =>'説明'])  // string 型 255文字制限 NULL許可
          ->addColumn('created',         'timestamp',  ['default' => 'CURRENT_TIMESTAMP'])        // timestamp 型 default: CURRENT_TIMESTAMP
          ->addColumn('updated',         'datetime',   ['null' => true])                          // datetime 型 NULL 許可
          ->addIndex(['username', 'email'],     ['unique' => true])                               // username, email にユニークキー設定
          ->create();
    }

    public function down()
    {
        $this->dropTable('users');
    }
}


  • db/migrations/mogemoge/20170724065738_create_table_members.php
<?php

use Phinx\Migration\AbstractMigration;

class CreateTableMembers extends AbstractMigration
{
    public function up()
    {
        $t = $this->table('members');
        $t->addColumn('member_code', 'string',    ['limit' => 20,  'comment' => '会員コード'])   // string 型 20文字制限
          ->addColumn('created',     'timestamp', ['default' => 'CURRENT_TIMESTAMP'])        // timestamp 型 default: CURRENT_TIMESTAMP
          ->addColumn('updated',     'datetime',  ['null' => true])                          // datetime 型 NULL 許可
          ->addIndex(['member_code'], ['unique' => true])                                    // member_code にユニークキー設定
          ->create();
    }

    public function down()
    {
        $this->dropTable('members');
    }
}

2. カラム追加

テーブル定義ファイル作成

DB hogehogeusers テーブルにカラムを追加したいと思います。

$ make migrate_create DB=hogehoge CLASS=AddTableUsersColumnsCity
...
...
created db/migrations/hogehoge/20170724065838_add_table_users_columns_city.php

テーブル定義ファイル編集

カラム postcode の後にカラム city 追加します。

<?php

use Phinx\Migration\AbstractMigration;

class AddTableUsersColumnsCity extends AbstractMigration
{
    public function up()
    {
        $t = $this->table('users');
        $t->addColumn('city', 'string', ['limit' => 10, 'comment' => '都市', 'after' => 'postcode'])
          ->update();
    }

    public function down()
    {
        $t = $this->table('users');
        $t->removeColumn('city')
          ->save();
    }
}

マイグレーション実施

$ make migrate

Point !

ちなみにマイグレーションの実行順序は
配置されているファイルの数字・アルファベット順です。
以下の様な仕様となっています。

Aogehoge
H001gehoge
H01gehoge
H0gehoge
H1gehoge
H2gehoge
Hogehoge

テーブル確認

http://192.168.35.102/ へアクセスすると
Adminer のログインページが表示されます。

f:id:kenzo0107:20170829132009p:plain

※ docker-compose.yml で定義されているログイン情報

Key Value
System MySQL
Server mysql
User root
Password rootpass
Database hogehoge

サーバ情報を入力しログインすると作成されたテーブルを確認することができます。

f:id:kenzo0107:20170829132531p:plain

users テーブルをクリックし詳細を確認します。

f:id:kenzo0107:20170829132633p:plain

問題なく定義通りに作成されたことがわかります。

では、DB mogemoge はどうでしょうか。

DB mogemoge にも members テーブルが作成されていることが確認できます。

f:id:kenzo0107:20170829133145p:plain

Point !

各 DB に phinxlog テーブルが作成されています。

マイグレーション実行状況のステータスを管理しています。

  • DB hogehoge.phinxlog

f:id:kenzo0107:20170829133614p:plain

  • DB mogemoge.phinxlog

f:id:kenzo0107:20170829133606p:plain

DB スキーマ毎に phinxlog テーブルを分けた理由としては
将来的に DB スキーマごとお引越しする、もしくは、ドロップするとういう時に
都合が良い為です。


また、以下の様に DB スキーマを指定しテーブル作成はできますが

$t = $this->table('hogehoge.users');
$t->addColumn(...
  ->create();

カラム追加時には以下の様に DB スキーマを指定した場合には実行できない為、
複数 DBスキーマの場合は 1つの phinxlog での管理は現実的でないと考えました。

$t = $this->table('hogehoge.users');
$t->addColumn('city', 'string', ['limit' => 10, 'comment' => '都市', 'after' => 'postcode'])
  ->update();

シード作成

シード定義ファイル作成

$ make seed_create DB=hogehoge CLASS=UserSeeder
$ make seed_create DB=mogemoge CLASS=MembersSeeder
...
...
created ./db/seeds/hogehoge/UsersSeeder.php
created ./db/seeds/mogemoge/MembersSeeder.php

シード定義ファイル編集

  • ./db/seeds/hogehoge/UsersSeeder.php
<?php

use Phinx\Seed\AbstractSeed;

class UsersSeeder extends AbstractSeed
{
    public function run()
    {
        $t = $this->table('users');
        $t->truncate();

        $genders = [1,2,3];

        $faker = Faker\Factory::create('ja_JP');
        $d = [];
        for ($i = 0; $i < 10; $i++) {
            $d[] = [
                'last_name'        => $faker->lastName(10),
                'first_name'       => $faker->firstName(10),
                'last_kana_name'   => $faker->lastKanaName(10),
                'first_kana_name'  => $faker->firstKanaName(10),
                'username'         => $faker->userName(20),
                'password'         => sha1($faker->password),
                'email'            => $faker->email,
                'postcode'         => $faker->postcode,
                'city'             => $faker->city,
                'birthday'         => $faker->date($format='Y-m-d',$max='now'),
                'gender'           => $faker->randomElement($genders),
                'card_number'      => $faker->creditCardNumber,
                'description'      => $faker->text(200),
                'created'          => date('Y-m-d H:i:s'),
                'updated'          => date('Y-m-d H:i:s'),
            ];
        }

        $this->insert('users', $d);
    }
}


  • ./db/seeds/hogehoge/MembersSeeder.php
<?php

use Phinx\Seed\AbstractSeed;

class MembersSeeder extends AbstractSeed
{
    public function run()
    {
        $t = $this->table('members');
        $t->truncate();

        $faker = Faker\Factory::create('ja_JP');
        $d = [];
        for ($i = 0; $i < 10; $i++) {
            $d[] = [
                'member_code'  => $faker->regexify('[0-9]{20}'),
                'created'   => date('Y-m-d H:i:s'),
                'updated'   => date('Y-m-d H:i:s'),
            ];
        }

        $this->insert('members', $d);
    }
}

Faker というライブラリを利用することで 日本人の名前や住所、正規表現を使ったデータを作成できます。

シード実行

$ make seed

無事データが登録されました。

f:id:kenzo0107:20170829175823p:plain

おまけ 1

ここで Phinx の seed のデータ INSERT 方法が非常に気になりました。

...
...
 -- insert('members')
    -> 0.0023s
 -- insert('members')
    -> 0.0016s
 -- insert('members')
    -> 0.0019s
 -- insert('members')
    -> 0.0022s
...
...

1件ずつ INSERT してる...?

本家 Phinx github のソースを確認してみました。

  • src/Phinx/Db/Table.php
    /**
     * Commit the pending data waiting for insertion.
     *
     * @return void
     */
    public function saveData()
    {
        foreach ($this->getData() as $row) {
            $this->getAdapter()->insert($this, $row);
        }
    }

データを foreach して 1件ずつ登録している!
なんて日だ!

数十件ならまだ良いですが
シードデータも増えてくると待ち時間が増えてくるのは宜しくない。

と言うことで
バルクインサートする様修正しプルリクした所無事マージされました*2

https://github.com/cakephp/phinx/pull/1148/files

おまけ 2

SQL を直接実行することも可能です。

    public function up()
    {
        $q = <<<EOF
CREATE TABLE `users` (
  `user_id` int(11) NOT NULL AUTO_INCREMENT,
  `last_name` varchar(10) NOT NULL COMMENT '姓',
  `first_name` varchar(10) NOT NULL COMMENT '名',
  `last_kana_name` varchar(10) DEFAULT NULL COMMENT '姓(カナ)',
  `first_kana_name` varchar(10) DEFAULT NULL COMMENT '名(カナ)',
  `username` varchar(20) NOT NULL COMMENT 'ユーザ名',
  `password` varchar(40) NOT NULL COMMENT 'パスワード',
  `email` varchar(100) NOT NULL COMMENT 'Email',
  `postcode` varchar(10) NOT NULL COMMENT '郵便番号',
  `birthday` date NOT NULL COMMENT '誕生日',
  `gender` tinyint(4) NOT NULL COMMENT '性別(1:男 2:女 3:その他)',
  `card_number` varchar(20) DEFAULT NULL COMMENT 'クレジットカードNo',
  `description` longtext COMMENT '説明',
  `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated` datetime DEFAULT NULL,
  PRIMARY KEY (`user_id`),
  UNIQUE KEY `username` (`username`,`email`)
) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8;
        $this->execute($q);
    }

Phinx のお作法に則らないパワープレイではありますが
結局頭の中で実クエリに変換している脳内工数を考えると
これもアリかなと♪

議論の分かれる所かと思います。

もう一踏ん張りな所

  • TINY INT(3) の様なタイプ指定ができない (?)
  • ユニーク制御しているカラムへの Faker でランダムデータ生成では Duplicate Error 発生の懸念あり (←Fakerの話)

よかった所

  • 様々なタイプの DB へ適合
  • Faker 利用で日本語対応のデータ生成可
  • 比較的学習コスト低

まとめ

あらゆる DB への適合し今後とも善玉マイグレーションツールとして
期待される Phinx、如何でしたでしょうか?

PostgreSQL, MSSQL も同じ定義ファイルからマイグレーション・シードが実行でき、
無事 Moby Dock の腸までデータが届くことを確認しております。

是非お試しください♪

ご参考になれば幸いです。

参照


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

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

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

■開発環境はこちら

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

*1:Rails 移行中

*2:1000 件程度のデータのシード実行では 6倍以上パフォーマンスが向上していることを確認しています。

ノンデザイナーだから使おう Adobe XD

f:id:akinorifukumura:20170808112301p:plain

こんにちは。MedPeerデザイナーの松村です。

突然ですがみなさんAdobe XD使ってますか?

私は最近もっぱらデザイン作業をXDで行なっており、作業スピードが1.8倍くらいになったと感じてます。

そしてXDを使ううちに気づいたのです。

XDはノンデザイナーこそ使うべきなのではないか…?🤔

というわけで、以下のようなスライドを社内LTで発表してみました。

簡単にまとめると、デザイナー以外もXDを使うことで

  1. 自分のイメージを伝えやすくなる
  2. テキスト情報などをそのまま使えるのでデザイナー工数が(少し)減る
  3. なんせ使いやすい

というメリットがあるよという内容です。 (PPTやエクセルでも1はクリアできますが、テキストのコピペが地味にめんどくさい…というデザイナーさんは多いのではないでしょうか)

反応が良かったのでワークショップをやってみた

LTを行ったのち、社内メンバーから使ってみたいという声が上がったのでワークショップを行ってみました。

まずはワイヤーフレームの概要説明を。 f:id:umeccco:20170802123147p:plain f:id:umeccco:20170802123156p:plain これ以上は言葉で説明するより作るのが早いだろう、ということで簡単なワイヤーフレームを個人の端末上で作ってみました。 (ちなみにFORUM Q&Aとは医師同士の質問サービスで、MedPeerのメインコンテンツです。) f:id:umeccco:20170802123211p:plain

ノンデザイナーが驚いたXDの機能

リピートグリッド

f:id:umeccco:20170802132901p:plain

選択したオブジェクトを繰り返し配置できるリピートグリッド。XDのキモとも言えるこの機能には参加者からも「おお〜」という声が上がりました。

helpx.adobe.com

画像のマスク

f:id:umeccco:20170802133507p:plain

ドロップインで画像が指定サイズに収まる点も評判が良かったです。

ただし、切り取り箇所は細かく制御できないのでデザイン時は以下のように「シェイプでマスク」を使うと便利です。

f:id:umeccco:20170802133046p:plain

オブジェクトの結合・切り抜き

f:id:umeccco:20170802133753p:plain

Adobe製品ではおなじみの機能ですが、デザイナー以外の方はあまり知らないとあって良い反応を得られました。

基本機能はIllustratorのオブジェクトの結合・切り抜きと似ていますが、ダブルクリックすると結合・切り抜きを行ったオブジェクトの形を再編集できるので便利です。 f:id:umeccco:20170802133803p:plain

オブジェクトの整列

f:id:umeccco:20170802133451p:plain PPTやエクセルだとあとちょっとが揃わなくてやきもき…ということが多いですが、XDだと気持ちよく揃ってくれる上にガイドまで表示されるところが評価されていました。

困ったこと

  • XDはWindows版もリリースされているが対応機種はWindows 10 Anniversary Updateのみ(Windows7ユーザーは導入できず困りました。)
  • WindowsとMacでインターフェイスが微妙に違う(メニューの出し方などが異なる)
  • なんだかんだで依頼形式は変わらない(小一時間触ったところで新しいツールを使おうとはならない(慣れたツールに戻ってしまう)ので、継続して啓蒙が必要)

良かったこと

  • ノンデザイナーでも1時間もかからないうちにワイヤーフレームが作れた
  • デザインの手順や必要な情報が何かということがふんわり伝わった

相互理解を深めるためにも、ノンデザイナーにもデザイナー用のツールに触れてもらう機会は重要だなと感じました。

今期は以下の内容で勉強会を開催していますが、タッチアンドトライやアイデア共有の機会ももっと設けたいと思っています。

f:id:umeccco:20170802140947p:plain

MedPeerのディレクターやエンジニアはこういった勉強会にも積極的に関わってくれるので、デザイナーには良い環境なのです😋

最後に

MedPeerのコンセプトは「医師の集合知」。

社内メンバー自ら職種の垣根を超えて知識を共有することで、そのコンセプトを体現できればと考え行動しています。

社内には医師メンバーもたくさんいるので、医療に貢献できるサービスを作るべく、医師も加わって知恵を出し合ってサービス開発しています。今後は、「医師を知る」ワークショップなども行う予定です。

少しでも気になったそこのデザイナーさん。是非一度会社見学に来てみませんか?

MedPeerではサービスを通じて社会に貢献したいデザイナーを募集しています。


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


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

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

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

■開発環境はこちら

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

2泊3日の開発合宿 in 千葉県金谷 まるもに行って来ました!!!!!!

こんにちは!!メドピアの栢割(カヤワリ)です。
メドピアでは定期的にエンジニアが集まって、普段の業務ではなかなか出来ないタスクの消化や新しい技術研鑽などを目的とした開発合宿を開催しています。

毎回開催場所には迷うのですが、今回は夏の海を一望できる南房総の方へ!!!千葉県富津市金谷にあるコワーキングスペース「まるも」さんにお邪魔しました。

まるも|都会と金谷を繋ぐコミュニティスペース | 南房総の金谷にあるコミュニティスペース「まるも」。開発合宿や経営合宿、イベント貸切など様々な用途で活用できるスペースです。

いざっ!

最寄りの浜金谷駅に到着。

今回はグループ会社のFitsPlus(フィッツプラス) *1の開発メンバーも参加してくれました。 f:id:takayukikayawari:20170712233217j:plain

駅から歩いて5分ほどの所にコワーキングスペース「まるも」さんがあります。 f:id:takayukikayawari:20170712233550j:plainf:id:takayukikayawari:20170712233600j:plain 中は結構広くてDIY感が溢れたおしゃれなスペースです。テーブル、ソファ、電源タップ、クーラー(←大事)、ホワイトボードなど備品も揃ってます。近くにはスーパーもあるのでお菓子や飲み物にも困りません。(ただ営業時間が夜19時までなので、早めに買い出しをしておかないと、遠くのコンビニまで歩く羽目になります(焦))

f:id:takayukikayawari:20170712233710j:plain

ちなみにまるもさんは年季の入ったトイレの改修をご検討中だそうです↓↓↓↓↓近いうちに行ったら新トイレを拝借できるかも…。

camp-fire.jp

まるもさんは宿泊スペースはなく、鍵なども閉められないので無人状態で貴重品などは置いていけません。荷物置きや寝泊まりはまるもさんのすぐ近くにある「かぢや旅館」さんにしました。

www.kajiyaryokan.com

開発の様子

開発合宿のメリットの一つは普段の職場から離れて、リラックスしやすい環境で開発ができることかと思います。しかしリラックスできるからといってダラダラと開発を進めて、いつもより進捗が悪かったら本末転倒です。
まるもに到着したら、まず各自の目標を共有してから開発を始めます。

(あ…別に普段の開発環境がリラックスできないというわけではないですよ笑。弊社ではエンジニアが出来るだけ快適に開発に集中できるように色々と制度や環境が整えられています。 弊社の開発環境・オフィスにご興味ある方は是非こちらをご参考くださいませ↓↓↓↓↓ )

開発環境 - 採用情報 - メドピア株式会社

弊社の合宿は各自が取り組みたいテーマを決めて開発することが多かったのですが、今回は合宿前にMedPeerのディレクターや部長陣に開発して欲しい新サービスの要件をヒアリングして開発する人もいました。合宿中に新サービスを作って、正式にメドピアのサービスとしてリリースしたり業務を効率化に繋げられれば、合宿を開催する意義も大いにあると思います。最終日の成果発表が楽しみです。

宣言後は開発に没頭します カタカタ(*^-^)ヘ_/ f:id:takayukikayawari:20170713000512j:plainf:id:takayukikayawari:20170713000533j:plain 外にテーブルと椅子があり、気分転換に場所を移動して開発ができます。こんな感じで自然あふれる郊外の空気を吸いながら開発に集中できるのも合宿の醍醐味。 f:id:takayukikayawari:20170713000746j:plainf:id:takayukikayawari:20170713000818j:plain

お腹が空いてきたら地元のご飯を堪能できるのも合宿のいいところ。海が近いこともあって新鮮な魚料理を連日頂きました。絶品!!!!!(ちなみに宿の周りに食事処があるかどうか事前に調べておいた方が良いです…。定休日だったり、営業時間短かったりで、お店を探し回る事になるので笑) f:id:takayukikayawari:20170713001609j:plainf:id:takayukikayawari:20170713001623j:plainf:id:takayukikayawari:20170713003103j:plain 2日目のお昼にはまるもさんスペースをお借りしてBBQを実施。メドピアの職場は部署間の座席が近いので他部署でも話した事がない人はいないのですが、フィッツプラスは職場が本郷にあるのでなかなかお話する機会がありません。BBQをキッカケに普段話さないメンバーともお話ができました(^-^) f:id:takayukikayawari:20170713002039j:plainf:id:takayukikayawari:20170713002123j:plain

コワーキングスペースは24時間使用できるので夜遅くまで開発に没頭しますカタカタ(*^-^)ヘ_/ f:id:takayukikayawari:20170713000838j:plain





成果発表

最終日の午後は成果発表。

  • 機械学習APIを使ったLINE BotとMessanger Botの開発

  • webpackの導入

  • fat controller / model抑制を意識した新しいgemの設計

  • 業務で使う管理画面のブラッシュアップ

  • Vue.jsや導入検討中のgemを使った新サービス開発

  • API-blueprintを利用したツール開発 などなど・・・・・・・

f:id:takayukikayawari:20170713003732j:plainf:id:takayukikayawari:20170713003744j:plainf:id:takayukikayawari:20170713003802j:plainf:id:takayukikayawari:20170713003820j:plain

最後は皆でパシャリ!!!お疲れ様でした!!! f:id:takayukikayawari:20170719102739j:plain

まとめ

今回は事前に社内から要望をヒアリングしてきた方が多かったので、今後正式に新サービスとしてリリースされるかもしれない成果が多く、期待感のある結果となりました。やはり合宿前に準備や目標を決めておくことが開発合宿の成功の秘訣かと思います。
個人的には各自が別々の目標で開発するという流れだったので、次回以降はチームを組んで開発するのも面白いのではないかと思っています。
また次の機会がありましたら、合宿の様子をお伝え出来ればと思います。

それではごきげんよう!!

(過去の合宿の様子が気になる方は以下をご参考ください↓) tech.medpeer.co.jp

tech.medpeer.co.jp


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


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

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

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

■開発環境はこちら

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

*1:FitsPlusは管理栄養士によるパーソナルダイエット指導サービス「ダイエットプラス」を運営しています。ダイエットプラスでは「正しく食べて変わる」を目的に、食事を中心としたバランスのよい健康ライフスタイルを栄養学・ダイエット指導の専門家がサポートしてくれます。

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