メドピア開発者ブログ

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

Ruby2.7の(実験的)新機能「パターンマッチ」で遊ぶ

はじめまして、メドピアのサーバサイドエンジニアの草分です。

RubyKaigi2019 1日目のセッションにてRubyのexperimental(実験的)な新機能「パターンマッチ」(Pattern Matching)が発表されましたね。

speakerdeck.com

この記事では発表で紹介されたパターンマッチの各種機能を確認し、遊んでみます。

概要

Rubyのパターンマッチとは?

Rubyist向けの説明として以下のような説明がなされていました。

  • データ構造の評価によるcase/when条件分岐の提供
  • 変数への多重代入

実際に例文を見てみましょう。

例文

case [0, [1, 2, 3]]
in [a, [b, *c]]
  p a #=> 0
  p b #=> 1
  p c #=> [2, 3]
end

case/whenではなくcase/inが追加され、caseで指定したオブジェクトとinに記載したデータ構造(パターン)を比較し、マッチした場合処理が行われます。

また、マッチした場合はパターンに記載した各変数に値がバインドされ、内部の処理で利用することが可能となります。


通常の多重代入とは異なり、オブジェクトのデータ構造とパターンが一致しない場合は処理されません。

case [0, [1, 2, 3]]
in [a]
  :unreachable # 配列構造が異なるため処理されない
in [0, [a, 2, b]]
  p a #=> 1
  p b #=> 3
end

その他、Hashもサポートされています。

case {a: 0, b: 1}
in {a: 0, x: 1}
  :unreachable # Hashの構造が異なるため処理されない
in {a: 0, b: var}
  p var #=> 1
end

それではこの機能が実装されているRubyを導入し、実際に試してみましょう。

準備

最新のRubyを導入する

  • まずは最新のRubyのソースコードをcloneし、trunkをチェックアウトします。
$ git clone https://github.com/ruby/ruby.git
$ cd ruby
$ git checkout origin/trunk
$ autoconf
$ ./configure
$ make
$ make install
  • しばらく待てば最新Rubyの完成です。

  • rbenvをお使いの場合、2019年5月時点では2.7.0-devバージョンを用いるとパターンマッチが利用可能なrubyをインストールすることができます。
$ rbenv install 2.7.0-dev

解説

基本文法/仕様

試す前に、パターンマッチの基本文法を確認してみましょう。

case expr
in pattern [if|unless condition]
 ...
in pattern [if|unless condition]
 ...
else
 ...
end

case 句に記載したものを検査対象として、以下のように動作します。

  • 記載したパターンを上から順番に評価し、最初にマッチしたものが処理される。
  • 1つもマッチしない場合はelse句の内容が処理される。
  • 1つもマッチせず、else句も存在しなかった場合は NoMatchingPatternError例外が発生する。

また、if/unlessによるguardも可能となっています。

case [1, 1]
in [a, b] unless a == b
  :unreachable # ここは処理されない
end

パターンマッチの基本文法は以上ですが、実行するとwarning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!という警告が表示されます。
警告の通り、パターンマッチは実験的な機能であり将来振る舞いが変更される可能性があります。ご注意ください。

次は、現時点で利用可能なパターンの記法を1つずつ確認してみましょう。

Value pattern

case 0
in 0
in -1..1
in Integer
end
  • ===で比較して一致した対象にマッチします。
  • そのため、上記3パターンはいずれもマッチさせることができます。
    • ※処理が実行されるのは最初にマッチしたパターンのみです。

Variable pattern

case 0
in a
  puts a #=> 0
end
  • 任意の値にマッチし、マッチした値はローカル変数としてバインドされます。
case [0, 1]
in [_, _]
  :reachable
end
  • また、変数として利用しない場合は_を使うことができます。

Alternative pattern

case 0
in 0 | 1 | 2
  :reachable
end
  • | を用いることで複数のパターンにマッチさせることができます。

As pattern

case 0
in Integer => a
  a #=> 0
end
  • =>を用いることでマッチした値を任意の変数にバインドすることができます。
  • rescueの使い方と似た記法ですね。
    • rescue StandardError => e

Array pattern

ここから少し複雑になってきます。

Array patternでは以下のいずれかの記法がパターンとして利用できます。

pat: Constant(pat, ..., *var, pat, ...)
   | Constant[pat, ..., *var, pat, ...]
   | [pat, ..., *var, pat, ...] # BasicObject(...)のシンタックスシュガー

そして以下の条件を満たすとマッチします。

  • Constant(何らかのclassを指定) === 検査対象objectがtrueを返す
  • 検査対象objectの#deconstruct メソッドが配列を返す
  • そこで返した配列と指定したパターンがマッチする

この#deconstructメソッドの使い方が鍵になります。
まずはArrayを対象としたArray patternのパターンマッチから見ていきましょう。

class Array
  def deconstruct
    self
  end
end

case [0, 1, 2] # ↓4種のいずれの記法でもマッチ可能
in Array(0, *a, 2)
in Object[0, *a, 2]
in [0, *a, 2] 
in 0, *a, 2   # []は省略可能
end

次はStructを対象としたArray patternのパターンマッチを見てみましょう。

class Struct
  alias deconstruct to_a
end

Color = Struct.new(:r, :g, :b)
color = Color[255, 0, 0]

case color
in [0, 0, 0]
  puts "Black"
in [255, 0, 0]
  puts "Red"
in [r, g ,b]
  puts "#{r}, #{g}, #{b}"
end

このように、独自に#deconstructを定義することで任意のobjectに対してArray patternによるパターンマッチを行うことができるようになります。
これがRubyのパターンマッチの大きな特徴の1つとなっています。

Hash pattern

Hash patternでは以下のいずれかの記法がパターンとして利用できます。

pat: Constant(id: pat, id:, ..., **var)
   | Constant[id: pat, id:, ..., **var]
   | {id:, id: pat, **var} # BasicObject(...)のシンタックスシュガー

そして以下の条件を満たすとマッチします。

  • Constant(何らかのclassを指定) === 検査対象objectがtrueを返す
  • 検査対象objectの#deconstruct_keys メソッドがHashを返す
  • そこで返したHashと指定したパターンがマッチする

前述のArray patternと似た形式で、Hashのようにkey名を指定してパターンマッチを行うことができます。

class Hash
  def deconstruct_keys(keys)
    self
  end
end

case {a: 0, b: 1} # ↓4種のいずれの記法でもマッチ可能
in Hash(a: a, b: 1)
in Object[a: a]
in {a: a}
in {a: a, **rest}
  p rest #=> {b: 1}
end

こちらも独自に#deconstruct_keysを定義することで任意のobjectに対してパターンマッチを行うことができるようになるため、Rubyのパターンマッチの大きな特徴の1つとなっています。

また、#deconstruct_keysの引数keysには処理の効率化のためのヒントとして、パターンの処理に必要なkey名の配列が渡されます。 その配列に含まれないkeyについては返却せず無視してよく、処理コストの削減を行うことができます。

遊ぶ

クイックソートしてみる

「クイックソートはパターンマッチを用いると書きやすい」という情報を社内で耳にしたため試してみます。

処理対象は数値の配列であることを前提として、ソート処理を書いてみました。

def qsort(ary) 
  case ary
  in [piv, *xs] # 要素が2個以上
    lt, rt = xs.partition{|x| x < piv}
    qsort(lt) + [piv] + qsort(rt)
  else # 要素が0個または1個
    ary
  end
end

ピボットの位置は先頭で決め打ちですが、かなりシンプルに書けますね!

逆にパターンマッチを使用せずに書くとすればこんな感じでしょうか。

def qsort2(ary)
  return ary if ary.length <= 1
  piv = ary[0]

  lt, rt = ary[1..].partition{|x| x < piv}
  qsort2(lt) + [piv] + qsort2(rt)
end

比べてみると、配列長の確認や配列の分割が冗長に見えてきますね。 オブジェクト構造による条件分岐と変数へのバインドが同時に行えるパターンマッチは、かなり強力な文法なのではないでしょうか。

独自拡張クラスを作ってみる

Hash patternをHash以外で使ってみたかったので、Timeクラスを独自拡張し、令和表記で年号を返すメソッドを作ってみました。
As patternと組み合わせて利用しています。

class Time
  def deconstruct_keys(keys)
    { year: year, month: month, day: day }
  end

  def to_reiwa
    case self
    in year: 2019, month: (5..)
      "令和元年"
    in year: (2020..) => year
      "令和#{year - 2018}"
    else
      "not令和"
    end
  end
end

実行してみます。ちゃんと動きましたね。

p Time.local(2019, 5, 1).to_reiwa  # => 令和元年
p Time.local(2019, 8, 1).to_reiwa  # => 令和元年
p Time.local(2020, 1, 1).to_reiwa  # => 令和2年
p Time.local(2019, 4, 30).to_reiwa # => not令和

引数の型指定をしてみる

当日の発表では「引数に対してパターンマッチを実行可能とするのは技術的には可能だが、型の指定に使われることが想定されるので実装しなかった。」という内容の発言がありました。

ということで言いつけを破ってTryしてみましょう。

def arg(num, str)
  case [num, str]
  in [Integer, String]
    :ok
  end
end

このメソッドを呼び出してみると…

p arg(3, "3")     # => :ok
p arg("2", 2)     # => NoMatchingPatternError
p arg(:a, "a")    # => NoMatchingPatternError
p arg(1, :a.to_s) # => :ok

引数の型を間違えると例外をraiseするメソッドになりました! 全くRubyらしくありませんね。

まとめ

RubyKaigi2019で紹介されたパターンマッチの機能を一通り触ってみました。まだ実験的な機能であるとのことで仕様変更の可能性はありますが、正式リリースが楽しみな機能です。

この他にもRuby3で実装が予定されている静的解析や、逆に搭載が見送られた仕様など、将来のRubyに関する具体的な情報が次々発表されたRubyKaigi2019でした。今後のRubyの動向から目が離せませんね!


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


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

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

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

■開発環境はこちら

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

Railsで処理を別クラスに切り出す方法について

こんにちは。メドピアのRuby(Rails)化をお手伝いしている@willnetです。最近はエンジニアが増えた影響か、Railsの質問に答えていることが多いです。

以前、Railsの太ったモデルをダイエットさせる方法についてというタイトルでPOROを使っていこうという話を書きました。その際にコード例などもなるべく多く載せるようにしたのですが、このエントリだけを読んだ状態では、いざ「POROを使ってみよう!」としたときにまだ悩む余地がありそうです。

POROはその名の通り普通のRubyオブジェクトなので、いろんな書き方ができてしまいます。それなりに経験がある人でないと、どのように書いたらいいんだろう…と悩んで時間を使ってしまいそうですね。さらに、複数人で開発しているチームだと書き方のバラツキも気になるところです。きっと、POROを書くときのお作法が決まっている方が開発しやすいはず。

そこで、お作法を決める手助けをするために例を出してみます。

コード例

slackのようなサービスを作っていると想像してみてください。Messageモデルをsaveしたときに、それがhereメンションのときはチャンネル内のアクティブなメンバーのみ、channelメンションのときはチャンネル内のすべてのメンバーに対してMentionを作るという処理をMessageモデルに定義しています。

class Message < ApplicationRecord
  has_many :mentions
  belongs_to :creator, class_name: 'User'
  belongs_to :channel

  after_create :create_here_mention, if: :here?
  after_create :create_channel_mention, if: :channel?

  def here?; end # 省略
  def channel?; end #省略

  def create_here_mention
    members = channel.members.active - [creator]
    create_mentions(members)
  end

  def create_channel_mention
    members = channel.members - [creator]
    create_mentions(members)
  end

  private

  def create_mentions(members)
    members.each do |member|
      mentions.create!(to: member, chennel: channel)
    end
  end
end

そもそもコールバック使うのどうなの?など議論の余地があるコードですが、そこまで考え出すとこのエントリで取り上げる範囲が広がりすぎてしまうためそのあたりは無視してください*1。このコードからcreate_here_mentioncreate_channel_mentioncreate_mentionsを別クラスに切り出してみるとします。さてどう切り出すのが良いでしょうか。

切り出し方にいろいろな選択肢が存在します。

  • クラスやメソッドの名前はどのような観点で決めると良いでしょうか?
  • メソッドはクラスメソッドにすべきでしょうか。インスタンスメソッドにしたほうが良いでしょうか?
  • クラスは一つでいいでしょうか?切り出すメソッドごとにクラスを作ったほうがよいでしょうか?

これらの点について、僕は自分なりの意見を持っています。それが正しいかはさておき、他の人がどういう観点で判断をしているのかを知ることで、みなさんがPOROを書くときに迷うことが減るのではないかと思います。

POROに切り出した後のコード

説明の前に、切り出した後のコードを載せます。このコードを参考にしつつ、どういう観点で切り出しているのか書いていきます。

class Message < ApplicationRecord
  has_many :mentions
  belongs_to :creator, class_name: 'User'
  belongs_to :channel

  after_create :create_here_mention, if: :here?
  after_create :create_channel_mention, if: :channel?

  def here?; end # 省略
  def channel?; end #省略

  def create_here_mention
    HereMentionCreator.call(message: self)
  end

  def create_channel_mention
    ChannelMentionCreator.call(message: self)
  end
end
class HereMentionCreator
  delegate :channel, :creator, to: :message

  def self.call(message:)
    new(message: message).call
  end

  def initialize(message:)
    @message = message
  end

  def call
    members.each do |member|
      message.mentions.create!(to: member, chennel: channel)
    end
  end

  private

  attr_reader :message

  def members
    @members ||= channel.members.active - [creator]
  end
end
class ChannelMentionCreator
  delegate :channel, :creator, to: :message

  def self.call(message:)
    new(message: message).call
  end

  def initialize(message:)
    @message = message
  end

  def call
    members.each do |member|
      message.mentions.create!(to: member, chennel: channel)
    end
  end

  private

  attr_reader :message

  def members
    @members ||= channel.members - [creator]
  end
end

メソッド名は統一する

POROに切り出したとき、publicなインターフェースはcallもしくはnew(つまりinitialize)で統一するようにしています。基本的にはcallで、インスタンス化したオブジェクトを返すだけでよいときのみnewという使い分けをしています。

まずメソッド名を考え、それから属するクラスを決めるものだ、という言説があるのは知っていて(要出典)以前はそのように実装していました。しかしHereMentionCreatorのようなクラス名をつけることで、callメソッドがhereメンションを作るのだな、と十分推測可能です。またメソッド名が統一されていると「このクラスのメソッド名ってなんだっけ?」とならずに便利なので最近は統一するようにしています。

処理の実態はインスタンスメソッドに書く

今回やろうとしていることは、「hereメンションを作る」と「channelメンションを作る」という手続きを切り出すことです。なので次のようにクラスメソッドで実装する人も時々見かけます。

class HereMentionCreator
  def self.call(message:)  
    channel = message.channel
    members = channel.members.active - [message.creator]
    
    members.each do |member|
      message.mentions.create!(to: member, chennel: channel)
    end
  end
end

サンプルコードが簡単なので、なんだかこれでも問題なさそうに見えますね。しかし手続きがもっと多くなるとどうでしょうか。

チャンネルのミュートの概念を追加し、さらにプッシュ通知もするように機能追加したコードを書いてみます。

class HereMentionCreator
  PUSH_NOTIFICATION_LIMIT = 100

  def self.call(message:)
    channel = message.channel
    members = channel.members.includes(:mute_channels).active - [message.creator]

    members.each do |member|
      message.mentions.create!(to: member, chennel: channel, mute: member.mute?(channel))
    end

    not_mute_members = members.reject { |member| member.mute?(channel) }
    not_mute_members.map(&:id).each_slice(PUSH_NOTIFICATION_LIMIT).with_index do |ids, index|
      PushNotificationWorker.perform_in(index.minutes, message.id, uids)
    end
  end
end

これでも読める人は問題なく読めると思いますが、さっきよりも概要を掴みづらくなったのは間違いないはず。

インスタンスメソッドで実装すると次のように書くことができます。

class HereMentionCreator
  PUSH_NOTIFICATION_LIMIT = 100

  delegate :channel, :creator, to: :message

  def self.call(message:)
    new(message: message).call
  end

  def initialize(message:)
    @message = message
  end

  def call
    create_notifications
    create_push_notifications
  end

  private

  attr_reader :message

  def create_notifications
    members.each do |member|
      message.mentions.create!(to: member, chennel: channel, mute: member.mute?(channel))
    end
  end

  def create_push_notifications
    not_mute_members.map(&:id).each_slice(PUSH_NOTIFICATION_LIMIT).with_index do |ids, index|
      PushNotificationWorker.perform_in(index.minutes, message.id, uids)
    end
  end

  def members
    @members ||= channel.members.includes(:mute_channels).active - [message.creator]
  end

  def not_mute_members
    @not_mute_members ||= members.reject { |member| member.mute?(channel) }
  end
end

callメソッドがcreate_notificationsメソッドとcreate_push_notificationsメソッドを呼ぶだけになり、処理の概要がつかみやすくなりました。また、membersnot_mute_membersもローカル変数からインスタンスメソッドに切り出されたことで、それぞれのメソッドの行数が減り、処理の内容を把握しやすくなっています。

このように、メソッド分割することで抽象化がしやすくなるのがインスタンスメソッドを利用する主な理由です。

こう書くとクラスメソッドでもメソッド分割できるのでは?という意見がでてきそうですが、クラスメソッドで同様のことをやろうとするとクラスインスタンス変数を更新するコードになり、結果としてスレッドセーフではないコードになってしまいます。

一つのクラスには一つの公開インターフェース

今回はHereMentionCreatorとChannelMentionCreatorのように2つのクラスに切り出しましたが、次のように単一のクラスにhereメンションをするメソッドとchannelメンションをするメソッドを定義する人もいるのではないでしょうか。

class MentionCreator
  delegate :channel, :creator, to: :message

  def self.here(message:)
    new(message: message, type: :here).call
  end

  def self.channel(message:)
    new(message: message, type: :channel).call
  end

  def initialize(message:, type:)
    @message = message
    @type = type
  end

  def call
    members.each do |member|
      message.mentions.create!(to: member, chennel: channel)
    end
  end

  private

  attr_reader :message, :type

  def members
    @members ||= if type == :here
      channel.members.active - [creator]
    else
      channel.members - [creator]
    end
  end
end

一見これでも問題なさそうに見えます。しかし仕様が変更されるについてメンテナンスが難しくなってきます。例えば新しくeveryoneメンションもMentionCreatorで扱うようにするとどうなるでしょうか。everyoneメンションは基本的にchannelメンションと同じですが、すべての人が参加しているチャンネル(generalチャンネル)以外ではメンションとして扱われないという仕様です。

素直にMentionCreatorクラスを拡張してみます。

class MentionCreator
  delegate :channel, :creator, to: :message

  def self.here(message:)
    new(message: message, type: :here).call
  end

  def self.channel(message:)
    new(message: message, type: :channel).call
  end

  # 追加
  def self.everyone(message:)
    new(message: message, type: :everyone).call
  end

  def initialize(message:, type:)
    @message = message
    @type = type
  end

  def call
    return if type == :everyone && !channel.general? # 追加

    members.each do |member|
      message.mentions.create!(to: member, chennel: channel)
    end
  end

  private

  attr_reader :message, :type

  def members
    @members ||= if type == :here
      channel.members.active - [creator]
    else
      channel.members - [creator]
    end
  end
end

結果として、callメソッドに分岐が一つ増えることになりました。このように分岐が増えていくと、1つのユースケース(例えばhereメンションのとき)だけについて考えたい状況でも別のユースケース(channelやeveryoneメンションのとき)のコードについて理解しなければいけなくなり、そのぶん可読性が落ちます。また、コードを修正したときに想定していない箇所でバグを仕込んでしまう、というケースも次第に増えていくことでしょう。

最初の例のように1つの処理ごとにクラスを作り、できるかぎり分岐を避けることでメンテナンスしやすくなります。

まとめ

僕がPOROを書くときの書き方について、それぞれ根拠を添えて説明しました。他にも良いやり方はあると思うので「俺はもっと良い書きかたを採用している!」という人がいたらどのように書いているのか教えていただけると嬉しいです(\( ⁰⊖⁰)/)


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


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

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

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

■開発環境はこちら

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

*1:コールバックについてはまた別のエントリでとりあげるかもしれません

RubyKaigi2019にプラチナスポンサーとして参加しました!👉🍻

f:id:chmv:20190424190137j:plain

サーバーサイドエンジニアのhirapi(@chmv71)です。
普段は、スギ薬局さんと共同で提供している、食事制限が必要な方に向けた栄養管理食宅配サービス「スギサポdeli」の開発を担当しています🍱
4/18〜4/20の3日間、福岡は博多で開催されたRubyKaigi2019に参加してきましたので、今回はそのレポートをお送りします🎶

アルコールパッチテストスポンサー🍻

もといプラチナスポンサー💎ということで、3階メインホール前にブースを展開しておりました。

f:id:chmv:20190424214318p:plain
こんなブースでした!

メドピアグループが医療業界やセルフケア領域でサービスを提供していることにちなんで、お越しくださった方々にアルコールパッチテスト*1を試していただきました。

健康管理はまず自分の体質を知るところから。推測するな、計測せよ👀

f:id:chmv:20190424191625p:plain
赤くなるのは弱い体質です

特殊なジェルのついたシールを貼ったまま20分待つと皮膚の色が変わります。
写真のように赤くなると耐性低いので注意、変わらない方は平気な気持ちでつい飲み過ぎてしまうので注意です。

こちらが予想外の大好評を頂き、ご用意していた400名分がなんと完売!🎊
Twitterでも話題にしていだいて嬉しい限りです。
(投稿してくださった方から抽選で20名様にTシャツをプレゼントいたします。お楽しみに😉)
これを機に医療の世界と人々の健康を支援するメドピアという会社の存在を知っていただけたらいいなと思います。
パッチテストをお試しくださった皆様、ご自分の体質に合わせたペースでお酒を楽しんでください!🍻

他にも、我が社の誇るかわいいくまのお医者さん「メドベアちゃん」のチロルチョコをお配りしていました。
いくつかバージョンがある中で、一番人気はしれっと官房長官ごっこをしているこちらでした😍

f:id:chmv:20190424183130j:plain
初春の令月にして 気淑く風和ぎ……

私含めスポンサーとして初めてブースに立つエンジニアメンバーも多くおりましたが、足を運んでくださった皆様とお話しできてとても刺激になりました。
Rubyコミュニティの一員として、Rubyでものづくりをするエンジニアとして、これからも皆様と関わっていけたらなと思います。
お越しくださった皆様、改めてありがとうございました!✨

会議 in RubyKaigi

タイムテーブルを改めて見てみると、3日間で計66ものセッションが行われていたことがわかります。
貴重な知見を惜しげもなく発表してくださった登壇者の皆様、トラブルひとつ無く運営してくださったスタッフの皆様、本当にありがとうございました。おつかれさまでした。

個人的に印象的なのは、やはり最終日の開発者会議です。バージョンアップの度にわくわくさせてくれるRubyの新仕様はこのような議論を経て作られているんだ、と胸が熱くなりました。
大盛り上がりだった👉絵文字演算子👉*2も、壇上・会場・タイムラインがエンジニアらしい遊び心で一体になっていてとても素敵でした。Ruby 3.0の目玉になりそうですね!(笑)

メドピアチームとして

さて、前回の記事にもあった通り、今回メドピアからは16名のエンジニアがRubyKaigiに参加しました。
サービス全体の開発を引っ張るテックリードもいれば、つい最近Rubyを書き始めたジュニア層もいます。興味のある分野も様々です🌈
せっかくこの多様なメンバーで参加したので、今回のRubyKaigiで得た学びを皆で深めつつ、チームの力にしていきたいと考えています。

もちろん、その学びの過程や成果は当ブログで広く発信していく所存です💪
メドピアチームとしてRubyコミュニティ・Rubyist仲間の皆様に貢献できるよう準備を進めておりますので、お楽しみに!

また、Rubyコミュニティをさらに盛り上げていくため、イベントもどんどん開催していく予定です🍺
Rubyistの皆様におかれましてはRubyKaigi2020 in 松本に向けてアップを始めているところかと思いますが、こちらも併せてチェックしていただけると嬉しいです!

5/23(木)イベントやります!

そして早速、5月に新規Railsプロジェクト開発のノウハウについてのイベントを開催します💡 詳細はこちら💨
弊社事例を存分にご紹介いたしますので、東京近郊にお住まいの方ぜひお越しください! 軽食をご用意してお待ちしております😋

---

最後に、参加エンジニア全員で撮った1枚を。また皆様にお会いできることを一同楽しみにしております!

f:id:chmv:20190424181111j:plain
また来年、松本でお会いしましょうー!


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


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

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

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

■開発環境はこちら

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

*1:※ 体質的なアルコール耐性を判定する簡易検査キットのことです。

*2:弊社エンジニア・ぱんのTwitterより↓↓

メドベアと一緒にお待ちしています at RubyKaigi 2019

こんにちは! エンジニアの宮原です。
明日から、RubyKaigi 2019が開催されますね。

rubykaigi.org

メドピアとRuby

メドピアは、現役医師が経営するヘルステックカンパニーとして、国内医師の3人に1人が参加する国内有数のドクタープラットフォーム「MedPeer」や、遠隔医療サービスなどを運営・開発しています。2016年頃から、Ruby on Railsへ移行を行っており、新規プロダクトには積極的にRubyを採用しています。
Rubyのコミュニティを盛り上げるべく、今年はプラチナスポンサーとして協賛しています。

ブースへ遊びに来てください

技術研鑽のためのイベントや勉強会への参加は会社が積極的にサポートしており、RubyKaigiは参加希望の全エンジニアに参加費(交通/宿泊費含む)を支給しています。
今年は、ベテラン・若手含め総勢16名、Rubyエンジニアの80%が参加します。   (留守を守ってくれるエンジニアのみんなに大変感謝です...!) f:id:nyagato_00_miya:20190417073021j:plain

メドピアブースでは、開発中プロダクトのGemfileやRails Statsを公開します。

f:id:nyagato_00_miya:20190415175442p:plain
会場ではモザイク無しの実物を公開します

当日は、ヘルスケアカンパニーとして皆さんの健康をサポートできるよう、400名限定でアルコールパッチテストを実施します。 アルコール体質を「ぜんぜん飲めない族」、「ほんとは飲めない族」、「危ない族」の3種類に判定し飲酒のリスクを知るパッチテスト。 ご参加頂いた方全員へメドベアデザインのチロルチョコをプレゼントします。 さらに、限定20名(抽選)でメドベアTシャツも当たりますよ!

f:id:nyagato_00_miya:20190416203504j:plainf:id:nyagato_00_miya:20190416204112p:plain

f:id:nyagato_00_miya:20190415183424p:plain
3Fメインホールホワイエ メドピアブースでお待ちしております♪

「これは!」と感じたセッションについて、ベテランエンジニアと若手エンジニアが対談形式に考察する内容など公開予定です。

それでは、当日会場で皆さんにお会いできることを楽しみにしています。
是非、一緒にRubyKaigiを盛り上げましょう!
ブースでメドベアと一緒にお待ちしています。


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


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

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

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

■開発環境はこちら

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

Rails初学者からみたRailsdm2019の景色

こんにちは、メドピアフットサル部の櫻井です。
Rails歴3ヶ月の自分が贅沢にも「Rails Developers Meetup 2019」に参加してきましたので、そのレポートをお届けしたいと思います。 ちなみに、メドピアでは今回のようなスポンサードイベントへの参加費は会社が全額サポートしてくれちゃいます。

railsdm.github.io

第一線で活躍するRails開発者の技術的知見を広げることに主眼を置く本イベントは、DHH氏の基調講演に代表されるように本当に魅力的なセッションだらけでした。
ただ、それぞれの具体的な内容はRails Developers MeetupのYouTubeチャンネル*1や各登壇者さんのスライドから追うことができるため、本記事ではイベントの様子とそこで駆け出しエンジニアである自分が感じたことをお伝え致します。

f:id:shibadog39:20190326130753p:plainf:id:shibadog39:20190326095106j:plain

ドリンクスポンサーやってました!

メドピアはドリンクスポンサーとして、1日目は特製のPeerWaterを、2日目はメドベアの可愛すぎるカップでコーヒーを提供させていただきました。 特にメドベアコーヒーは好評をいただき、スポンサー枠の350杯が昼過ぎにはなくなってしまうほど大盛況でした。ありがとうございました!

f:id:shibadog39:20190326130923p:plainf:id:shibadog39:20190326131030p:plainf:id:shibadog39:20190326095652j:plain

f:id:shibadog39:20190326095640j:plain
大人気だったメドベアコーヒー

スポンサー企業によるノベルティグッズはどれも個性的でついつい手にとってしまいました。ノベルティはこういったイベントの醍醐味の一つですよね。 また、2日間とも最後には懇親会が開催されました。お寿司からスイーツまで、さらには日本酒からワインまで美味しいものを口にしながらワイワイできる最高の時間でした。

f:id:shibadog39:20190326131510j:plainf:id:shibadog39:20190326100659j:plain

f:id:shibadog39:20190326125419j:plain
大盛り上がりの懇親会

メドピアからの登壇

今回メドピアからは@pipopotamasuさんと@purunkaoruさんがそれぞれセッションとPRセッションで登壇させていただきました!

f:id:shibadog39:20190326100901j:plainf:id:shibadog39:20190326100912j:plain

お二方の登壇の様子

Railsフロントエンドボイラープレート「medpacker」の紹介 by @pipopotamasu

本ブログの記事でも投稿させていただいているmedpackerを紹介させていただきました。
どのような背景からmedpackerを作成する必要が生じたのか、medpackerを使えば何ができるようになるのかわかりやすく説明されています。webpackerからの脱却を考えている場合は、ぜひスライド及び記事の方を参考にmedpackerを使ってみてください!本当に3分かからずにセットアップできます。

MedPeerの取り組みの「失敗」の話をしよう by @purunkaoru

メドピア社内における様々な取り組みとそれにともなう失敗事例を紹介しました。
具体的には

  • 技術的負債をなくしていく整地部
  • Railsのバージョンアップ
  • 懸垂棒

に関するお話を紹介しています。我々と同じ失敗をしないためにも参考にしてください! また、自慢の取り組み・失敗話がありましたら共有していただけると嬉しいです。

初学者から見たRailsdm

冒頭にも書きましたが自分はRails歴3ヶ月で、もちろん今回のRailsdmが初参加でした。
正直に言うと今の自分が参加してなにか得るものがあるのか戦々恐々としていましたが、今となっては参加してよかったと心から思っています。

具体的には以下の3つがその理由です。

手軽に挫折することができる

セッションで前提とされている事柄・技術自体を知らないということが頻発します。比較することもおこがましい方々が登壇者ということを理解しながらも、Railsdmの2日間の間「このままじゃいけない」と何度考えたかわかりません。

手軽に視座を引き上げることができる、世界を広げることができる

第一線のエンジニア・事業責任者の方の問題解決の軌跡や課題意識を直接聞く機会はなかなかないと思います。また、日々の業務では扱う機会がなかったRailsをとりまく周辺技術であったりコミュニティに関する話題に触れることは、自分の興味関心の領域を広げることになると思います。

手軽に自分が何に「おもしろい」と感じるのか観測できる

セッションの内容は本当に多様で、技術にとことん向き合ったものからプロダクトの設計の話、Rubyコミュニティの話までありました。どういう話を聞きたいと思ったか、聞いてどう感じたか改めて振り返ることで自分の志向を再確認できるのではないかと感じました。

まとめると

魔法のようなRailsの技術自体も人の手によって作られているという当たり前のことを認識できたことが最も大きな収穫であったように思います。挫折感もあったのは確かですが、イベント後にはコードを書きたいという前向きな気持ちになっていました。

駆け出しエンジニアこそ、こういったイベントに積極的に参加するべきなのかもしれません。自分はこのRailsdmをきっかけに、各種勉強会に物怖じせずに参加してみようと思うことができました。 嬉しいことにメドピアでは今回のようなイベント・勉強会への参加を推奨する制度があるのでバリバリ活用していきたいと思います。

最後に

Railsdm参加することができてとても良かったです。運営のみなさま本当にありがとうございました!

突然ですが、人間は忘れる生き物です。 ja.wikipedia.org 今回自分が感じた「勉強しなきゃ」とか「あの技術調べてみよう」という気持ちもあっという間にどこかにいってしまうことでしょう。そんなの嫌だということで、メドピアではランチの時間で集まってRailsDM再放送上映会を実施することにしました。
こういった取り組みが自発的に起こるのが弊社の魅力なのかもしれません!楽しみです。

f:id:shibadog39:20190326105654j:plain
Railsdm2019最高でした!

以上、Rails Developers Meetup 2019のレポートをお届けいたしました!

※画像左下にクレジットが入っているものはラブグラフさん*2の撮影によるもので、それ以外は個人で撮影したものになります。


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


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

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

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

■開発環境はこちら

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

GitHub メンションを Slack DM する機能を Serverless Framework で作った話

メドピアマッスル部見習い kenzo0107 です。

今回は GitHub のコミュニケーションを円滑にすべく導入した GitHub 通知の Slack DM 機能になります。

github.com

導入経緯

GitHub.com でイシューコメントやプルリクエスト等でコメントをした、してもらった、時に気づかず放置されてしまうことが度々ありました。

Slack 上で
「あれ、どうなりました?」と聞くと、
「コメントくれてたんですね、すいません、気づきませんでした」と。

この防止策の一環として導入したのが、 GitHub メンションを Slack DM に通知する機能になります。

Slack には既に GitHub アプリあるけど?

Slack には既に GitHub アプリがありますが、個人宛てに DM 通知するには個々が認証する等、一手間あります。

その為、認証等せずとも、GitHub.com 通知を個人宛のメンションとして Slack DM する仕組みが必要と考えました。

新入社員さんに一手間かけてもらうことなく、スムーズに GitHub + Slack のコミュニケーションを体験してもらえるようにしました。

システム構成

f:id:kenzo0107:20190228125632p:plain

  1. mogezo 君が GitHub イシュー or プルリク コメントで hogeko さんへメンション
  2. GitHub Webhook を AWS API Gateway で受け、
  3. Lambda を実行し、
  4. hogeko さんへ Slack DM 通知する

図の点線で囲んだ AWS 内の処理を Serverless Framework で構築しました。

導入手順

以下導入手順の大まかな流れです。

  1. GitHub Webhook 設定
  2. Slack Incoming Webhook URL 発行
  3. serverless framework で API Gateway + Lambda + CloudWatch Logs 構築

GitHub Webhook 設定

Organization の Webhook を作成します。*1

設定項目 内容
Playload URL 仮設定で良いです (ex. https://hoge )。
後ほど設定します。
Content type application/json
Secret 認証の際に必要なパスワード情報です。
適当なランダム値を設定してください。

対象イベントは以下としました。

  • Commit comments
  • Issue comments
  • Pull requests
  • Pull request reviews
  • Pull request review comments

f:id:kenzo0107:20190215182516p:plain

Slack Incoming Webhook 作成

以下リンクから Slack Incoming Webhook URL 作成します。

https://slack.com/services/new/incoming-webhook

Serverless framework で API Gateway + Lambda 構築

事前準備です。

serverless framework プロジェクトを clone します。

git clone https://github.com/medpeer-inc/githubcom2slack
cd githubcom2slack

Nodejs は本プロジェクトでは AWS Lambda 最新の 8.10.0 としています。*2

node -v

v8.10.0

Serverless Framework インストールします。

npm install -g serverless

関連ライブラリをインストールします。

npm install
bundle install -j4 --path vendor/bundle

AWS Access Key ID, AWS Secret Access Key を環境変数設定します。

現状弊社では direnv を利用し設定しています。*3

// direnv インストール
brew install direnv

// 環境変数設定
echo -n 'export AWS_ACCESS_KEY_ID=HOGEFUGA' >> .envrc
echo -n 'export AWS_SECRET_ACCESS_KEY=BUDOUBAR' >> .envrc
direnv allow .

AWS KMS Key 作成

aws kms create-key

{
    "KeyMetadata": {
        "AWSAccountId": "xxxxxxxxxxx",
        "KeyId": "yyyyyyyyyyyyyyyyyyyyyyyy",
        "Arn": "arn:aws:kms:ap-northeast-1:xxxxxxxxxxx:key/yyyyyyyyyyyyyyyyyyyyyyyy",
        "CreationDate": 1549615619.872,
        "Enabled": true,
        "Description": "",
        "KeyUsage": "ENCRYPT_DECRYPT",
        "KeyState": "Enabled",
        "Origin": "AWS_KMS",
        "KeyManager": "CUSTOMER"
    }
}

キーのエイリアス作成

aws kms create-alias \
    --alias-name alias/githubcom2slack \
    --target-key-id yyyyyyyyyyyyyyyyyyyyyyyy

秘密情報の暗号化

今回、秘密情報として扱うのは、以下2 点です。

  • GitHub Webhook Secret
  • Slack Incoming Webhook URL

AWS Lambda コンソール上で暗号化ボタンをポチッと押すのでなく、暗号化した文字列を環境変数として設定します。

GitHub Webhook Secret 暗号化

aws kms encrypt \
  --key-id arn:aws:kms:ap-northeast-1:xxxxxxxxxxx:key/yyyyyyyyyyyyyyyyyyyyyyyy \
  --plaintext "<GITHUB_WEBHOOK_SECRET>" \
  --query 'CiphertextBlob' \
  --output text

Slack Incoming Webhook URL

aws kms encrypt \
  --key-id arn:aws:kms:ap-northeast-1:xxxxxxxxxxx:key/yyyyyyyyyyyyyyyyyyyyyyyy \
  --plaintext "<SLACK_INCONMING_WEBHOOK_URL>" \
  --query 'CiphertextBlob' \
  --output text

<SLACK_INCONMING_WEBHOOK_URL>https:// の後の ドメインからの文字列を指定してください。*4

上記 2 つ生成された値を secrets.yml に設定します。

  • secrets.yml は以下の様になります。
GITHUB_WEBHOOK_SECRET_ENCRYPTED: *****************************
SLACK_INCONMING_WEBHOOK_URL_ENCRYPTED: *********************************
AWS_KMS_KEY_ARN: arn:aws:kms:ap-northeast-1:xxxxxxxxxxx:key/yyyyyyyyyyyyyyyyyyyyyyyy

秘密情報の暗号化

yaml_vault で secrets.yml ファイルを暗号化した secrets.yml.enc をリポジトリで管理します。*5

env \
  AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \
  AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \
bundle exec yaml_vault encrypt \
  --cryptor=aws-kms \
  --aws-region=ap-northeast-1 \
  --aws-kms-key-id=alias/githubcom2slack \
  secrets.yml -o secrets.yml.enc

KMS key を利用した暗号化・復号権限を特定 IAM Group に絞ることで、そのグループに属するユーザのみ暗号化・復号が可能になります。

GitHub.com ユーザと Slack ユーザを紐付け

以下 2 つを紐づける為に git2slackNames という hash 値を設定します。

  • GitHub.com ユーザ名
  • Slack ユーザ名
// github username : slack username
const git2slackNames = {
  'kenzo0107': 'kenzo.tanaka',
}

上記の場合、新たにユーザ追加する度にコード修正が必要となります。

もし GitHub.com ユーザ名 と Slack ユーザ名 に命名規則がある場合、以下メソッドで命名規則に沿った変換を実装すると追加の手間がありません。

弊社では命名規則があるので、変換処理をするようにしています。*6

function extractUsernameFromMessage (message) {
  let usernames = []
  let usernameCandidates = message.match(/@[a-zA-Z0-9-]+/g)
  if (!usernameCandidates) {
    return usernames
  }

  for (let i in usernameCandidates) {
    var u = usernameCandidates[i].replace('@', '')
    if (denySlackDmUsers.indexOf(u) > -1) {
      continue
    }
    if (git2slackNames[u]) {
      usernames.push(`@${git2slackNames[u]}`)
    }

    // github username と slack username に命名規則があれば、
    // ここで変換 git --> slack username へ変更するも良し
  }
  return usernames
}

例)命名規則がある場合

  • GitHub.com のアカウント名: medpeer-<firstname>-<lastname>
  • Slack のアカウント名: <firstname>.<lastname>

以下のような変換が可能です。

for (let i in usernameCandidates) {
  ...
  ...
  if (u.match(/^medpeer-/)) {
    usernames.push(`@${u.replace(/medpeer-/, '').replace(/-/, '.')}`)
  }

いざデプロイ

sls deploy -v

GitHub Webhook URL 設定

GitHub Webhook URL が仮設定状態だったので、生成されたエンドポイントを設定します。

sls info

Service Information
service: githubwebhook2slack
stage: production
region: ap-northeast-1
api keys:
  None
endpoints:
  POST - https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/production/webhook
functions:
  githubWebhookListener: githubwebhook2slack-production-githubWebhookListener

上記の例では、 https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/production/webhook がエンドポイントになります。

こちらを仮設定だった Github Webhook URL に設定します。

では、早速試してみましょう!

GitHub.com のプルリクエストでメンションします。

f:id:kenzo0107:20190227143042p:plain

するとすぐに Slack DM 通知がきます。

f:id:kenzo0107:20190227123309p:plain

通知がこないな、という時に

Lambda のログが CloudWatch Logs に流れてますので、ログを確認してください。

そもそもログが出ていないのであれば、 Webhook の設定忘れや URL に誤りがある等チェックしてみてください。

まとめ

弊社では、非エンジニアも GitHub でコミュニケーションしています。

今回の実装によって、コミュニケーションが円滑になったことに加えて、 GitHub 上で明示的にメンションすることが増えた様に思います。

また、 GitHub から API Gateway + Lambda の認証が確立できたことで、 GitHub イベントをトリガーとした AWS リソースの変更に応用する様にもなりました。

こちらについては今後執筆できればと思います。

以上です。 参考になれば幸いです。


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


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

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

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

■開発環境はこちら

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

*1:リポジトリを絞りたい場合は、個々のリポジトリの Webhook に設定してください。

*2:nodenv でセットアップしています。

*3:特段これでなくてはいけないということではないので、適宜設定してください。

*4:https:// を含めるとエラーになるかと思います。

*5:serverless-secrets-plugin という暗号化する npm 管理可能な package もあるのですが、暗号化時にパスワードを設定する必要があります。そのパスワードの管理をAWS パラメータストアにすると、結局、KMS が必要となり、秘密情報を取得する手間がさらに増えてしまう為、導入経験がある yaml_vault にしました。その他にもあろうかと思いますが、もし良いのあるよ!という場合は是非教えてください!

*6:命名規則がない場合はこの辺りを DB に持たせても良いかもしれません。

MedPeerをrails 5.2へアップデートしてました!🎊

こんにちは、エンジニアの森田です🌴

rails 5.2.0が発表されたのが2018年4月10日🚃

大分遅くなってしまいましたが、2018年10月18日にメドピアが運営する医師専用コミュニティサイト「MedPeer」を、rails 5.1.6からrails 5.2.0にアップグレードしていました🎊

f:id:madogiwa0124:20190105151411p:plain

今回は、アップグレードを行った手順や躓いた部分を今更感もありますが紹介いたします🙌

アップグレードまでの道のり

まずはバージョンアップまでに行ったことの概要を下記にまとめました。

  1. リリースノートとアップグレードガイドを読んで影響を確認
  2. Gemfileのrailsのversionを5.2.0に変更してbundle update
  3. rails app:updateで設定周りを更新
  4. テストを実行し、失敗したor警告が発生した箇所を修正
  5. 社内の検証環境にデプロイして、動作確認
  6. 本番デプロイ
  7. 振り返り会

それぞれについて、実際にやったことを紹介いたします🙇

リリースノートとアップグレードガイドを読んで影響を確認

まずはRails5.2の変更点を知るためにRailsGuideのアップグレードガイドとリリースノートを読みました👀 (私は英語があまり得意ではないので、日本語版もあって非常に助かりました・・・!)

アップグレードガイド

リリースノート

Gemfileのrailsのversionを5.2.0に変更してbundle update

月に1回bundle updateを行っていて、比較的依存Gemのバージョンが上がっている、またマイナーバージョンのアップデートということで影響も少なそうだったので、bundle updateで依存Gemを含めて、アップグレードを行いました。

※月1回のbundle updateの施策については、こちらから👀

speakerdeck.com

rails app:updateで設定周りを更新

次にrails app:updateをとりあえず全てyesで実施して、危なそうな部分を戻す対応を行いました。影響が大きそうだった差分だけ下記に記載しておきます👀

  • bin/railsbin/rakeでspringをロードしないような変更が入っていたので反映しませんでした。
  • active_storagetest_unit関連の設定ファイル追加及び既存ファイルへの更新は、使用していないので反映しませんでした。
  • config/application.rbconfig.load_defaults5.2に変更しました。
  • bootsnapは開発環境のみで動くようにし、本番での使用は一旦様子見しました。

設定ファイル関係はわからないことも多かったのですが、技術顧問の前島さんにレビューしてもらえるので安心・・・🙏

f:id:madogiwa0124:20190105151558p:plain

テストを実行し、失敗したor警告が発生した箇所を修正

とりあえず全体でテストを走らせて落ちた箇所と警告が発生していた部分を修正しました👷
主な修正は下記のようなところでした。

Arelのexistsを使っていた箇所を修正

existsを使っている箇所でDEPRECATION WARNING: Delegating exists to arel is deprecated and will be removed in Rails 6.0.の警告が発生していたため、下記のようにEXISTSではなくNOT INを使って行うように修正しました。

# before
where(Expert.where('users.id = experts.user_id').exists.not)
# after
where.not(id: Expert.select(:user_id)) }

Dangerous query method対応

rails 5.2から下記のようにsqlをハードコーディングするとDangerous query methodの警告が発生するようになりました。

Post.order('RAND()')

MedPeerでは固定の文字列を渡している箇所でのみ発生していたので、Arel.sqlで対応しました。

Post.order(Arel.sql('RAND()'))

テストの無い管理画面に立ち向かう

今回railsのバージョンを上げると同時に管理画面で使っているadministrateというgemのバージョンも大きくあがりました。メドピアでは管理画面ではクリティカルな部分以外でテストコードを書いていません😥
全てを手作業で確認するのは大変なので、今回はRails.application.routesを使ってpathを生成し、各画面遷移時にステータスコード200が返却されることを検証することで、手動テスト前の最低限の品質保証、バージョンアップによるエラーの検知が出来るようにしました🙇

require 'rails_helper'
module AdminRoutingTestHelper
  class << self
    # Admin配下のpath名と、それに合致するcontroller名とaction名を取得
    def admin_routes(actions)
      routes = routes(actions)
      routes.select { |route| route[:name].include?('admin') }
    end

    private

    def routes(actions)
      routes = Rails.application.routes.routes
      routes = routes.map { |route| prepare_route(route) if route.name }.compact
      routes.select { |route| actions.include?(route[:action]) }
    end

    def prepare_route(route)
      { name: route.name, action: route.requirements[:action], controller: route.defaults[:controller] }
    end
  end
end

RSpec.describe '画面に正常に遷移できるか確認' do
  IGNORE_PATH = [
    # 自動テスト対象外のpathを記載 
  ].freeze

  # 前提データが必要な場合は自動テストを行うのが難しいので、一覧と新規に限定
  admin_routes = AdminRoutingTestHelper.admin_routes(['index', 'new'])
  # 対象外のpathを除外
  target_routes = admin_routes.select { |route| IGNORE_PATH.exclude?(route[:name]) }
  url_helper = Rails.application.routes.url_helpers

  # 対象のpathの件数分、遷移しステータスコード200が返ってくることを検証
  target_routes.each do |route|
    describe route[:name] do
      before do
        visit url_helper.url_for(host: Capybara.app_host, controller: route[:controller], action: route[:action])
      end
      it "#{route[:controller]} # #{route[:action]}" do
        expect(page.status_code).to eq 200
      end
    end
  end

社内の検証環境にデプロイして、動作確認

テストのある部分はテストを全て通し、ない部分については先述の仕組みで確認したあと、ディレクターさんにも協力して頂き、業務上クリティカルな箇所について手で検証しました。

今回はマイナーバージョンアップということで、普段使っている本番相当の検証環境にバージョンアップを反映して検証を行いました。

本番デプロイ

通常業務に影響がなさそうという検証が出来たので、2018年10月18日にバージョン対応をmasterブランチにマージし、本番環境にデプロイしました🎊

VersionUp振り返り会

最後にバージョンアップの際に行ったことを整理して、KPTで振り返りを行いました。以下が出てきた内容の抜粋です🙇

Keep

  • 管理画面のテストを最低限ではあるが、自動的に行うことが出来た。

Problem

  • Gemfileで過去に色々な経緯からバージョンが固定されているGemがあり、月1のbundle updateで1年半程更新していないものもあった。railsのバージョンアップによる依存関係の解消時に大きくバージョン上げる必要があったため、検証負荷が高くなってしまった。
  • バージョンアップ用の環境を用意しておらず通常開発と同じ環境でバージョンアップの検証も行っていたため、通常開発業務の検証の際にバージョンアップを切り戻すといった作業が必要でメンテナンス負荷が高く、現状の検証環境のrailsのバージョンを把握しづらかった。

Try

  • Gemfileを読み解く会を開催し、経緯を理解した上で外せるものはバージョン固定を外す。
  • GemfileのVersion固定は、bundle updateのタイミングで見直す等の仕組み化を行う。
  • VersionUpのときは専用の環境を用意する。

Gemfileに記載されているgemがどのように使われているのか、なぜバージョンが固定化されているのかなどを読み解く会をやるといった今後のアクションに繋げることが出てきて、バージョンアップをするだけでなく、その過程で感じた問題等も話せたので振り返りいいのでは?という気持ちになりました🙌

おわりに

大分乗り遅れた感はありますが、今回はMedPeerをrails 5.2にバージョンアップした話でした。 現在6.0に関しても色々と情報が公開されてますが、次回の6.0に関しては乗り遅れずにバージョンアップ出来るように今回の反省を生かしていければと思います!