Quantcast
Channel: メドピア開発者ブログ
Viewing all 215 articles
Browse latest View live

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

$
0
0

こんにちは。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の部分に例えば以下のようにバージョンを付与しました。

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

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

classCreateQuestions< ActiveRecord::Migration[5.1]
   create_table :questions, id: :integerdo |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


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

$
0
0

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

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

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

いざっ!

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

今回はグループ会社の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は管理栄養士によるパーソナルダイエット指導サービス「ダイエットプラス」を運営しています。ダイエットプラスでは「正しく食べて変わる」を目的に、食事を中心としたバランスのよい健康ライフスタイルを栄養学・ダイエット指導の専門家がサポートしてくれます。

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

$
0
0

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:plainf: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

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

$
0
0

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

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

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

Phinx って何?

f:id:kenzo0107:20170829132120p:plainphinx.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 で定義されているログイン情報

KeyValue
SystemMySQL
Servermysql
Userroot
Passwordrootpass
Databasehogehoge

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

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倍以上パフォーマンスが向上していることを確認しています。

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

$
0
0

こんにちは。メドピアでエンジニアをしている村上(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

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

$
0
0

こんにちは。メドピアの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モデルに「ブログを投稿して友達に通知をする」メソッドを書いてみます。

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

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

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

classPostWithNotificationsdefself.create!(creator:, body:)
    new(creator: creator, body: body).create!
  enddefinitialize(creator:, body:)
    @creator = creator
    @body = body
  enddefcreate!ActiveRecord::Base.transaction do
      create_post!
      create_notifications!      
    endendprivateattr_reader:creator, :bodydefcreate_post!
    creator.posts.create!(body: body)
  enddefcreate_notifications!
    creator.friends.each { |friend| create_notification!(friend) }
  enddefcreate_notification!(friend)
    friend.notifications.create!("#{creator.name}さんが投稿しました")
  endend

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

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

classUser< ApplicationRecord# 略defcreate_post_with_notifications!(body)
    PostWithNotifications.create!(creator: self, body: body)
  endend

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

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

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

classMessage< ApplicationRecord
  enum status: %i[unread read]defself.read!(messages)
    messages.unread.each(&:read!)
  end# たくさんのメソッドend

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

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

classMessage::Collectiondefself.read!(messages)
    new(messages).read!
  enddefinitialize(messages)
    @messages = messages
  enddefread!@messages.unread.each(&:read!)
  endend

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

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

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

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

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

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

classUser< ApplicationRecord# ...defself.notify_unread_messagesUser.active.find_each do |user|
      nextunless user.messages.unread.exists?
      UserMailer.not_read_messages(user).deliver_now
    endendend

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

classUnreadMessagesNotificationdefself.notify(user: nil)
    new(user: user).notify
  enddefinitialize(user: nil)
    @user = user || User.active
  enddefnotify
    user.find_each do |user|
      nextunless user.messages.unread.exists?
      UserMailer.not_read_messages(user).deliver_now
    endendprivateattr_reader:userend

メソッドを移動させたことで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タスクはモデルのメソッドを呼び出すだけ」という慣習があります。これにより、処理内容がわかりやすく、かつテストが書きやすくなる効果があります

FukuokaRubyKaigiで登壇してきました!!

$
0
0

FukuokaRubyKaigiで登壇してきました

みなさまこんにちは、メドピアCTOの福村です。

FukuokaRubyKaigi、控えめに言って最高でした! (スケジュールがタイトで食を楽しめませんでしたが次回リベンジ。)

regional.rubykaigi.org

キッカケ

メドピアは長くPHPで開発してきたため、Rubyでも開発していると多くの人に知ってもらいたいということがキッカケで めんたいスポンサーというスポンサー枠で参加させていただきました。
運営スタッフの方々に感謝です。本当に大変だったと思います。

発表内容

朝からほぼ全員?と思うくらい席いっぱいで熱量半端なかったです。 OSSへ恩返しというワードが終始出ていたのが印象的でした。今後も感謝の気持ちやお金、行動をわかりやすく実行していきたい。
島田さんの「A Ruby Programming Episode」がとてもすばらしく、おまけにすごい秀逸な終わり方だったため、次で滑るとFukuokaRubyKaigiの印象わるくなってしまうかもというプレッシャーを密かに感じながら喋りました。
前夜祭もLT大会がすごい盛り上がり、エネルギーをもらいました。

ちょっと補足

  • レビュー振り返り会のオススメ
    • コスト低で実施できるのでよいです。是非やってみてください!
  • ノベルティーグッズにマッサージ器
    • メンバーと良さそうなノベルティーグッズは何かね?と議論を重ねて最終的に選ばれたのがこれでした。
    • もらった人はぜひとも使ってやってくださいm()m
      f:id:akinorifukumura:20171127174821p:plain

まとめ

やりたいこと(やれる)ことは沢山あるもので、だいたいやれそうなものがやれていなかったりで焦りますよね。
一つ一つやれることを着実にやっていくと1年間を振り返ったときいっぱいやってきたなと気づけたりします。
長い道のりなので後悔のない1手を着実に大胆にすすめていきたいと思います。


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


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

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

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

■開発環境はこちら

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

開発合宿!?それならペンションでしょ!! 〜群馬県みなかみ町へのメドピア開発合宿日誌〜

$
0
0

こんにちは。メドピアエンジニアの保立です。
今回は、題名の通り、開発合宿についてです。
メドピアでは、年に2, 3回開発合宿を行なっており、前回の合宿から、僕が合宿の幹事をやらせてもらってます。
今回は、11/29〜12/1の3日間、群馬県のみなかみ町にある谷川くるみ村のペンション木馬さんにお世話になりました。
ペンション木馬さんでは、開発合宿プランが用意されており、今回はこの開発合宿プランで合宿を行いました。
11名での参加でしたが、「貸切」で使わせていただくことができました。
ペンションでの開発合宿が思いのほか良かったので、感想を書いていきます。

ペンションをオススメする理由

合宿先を選定する上で、(1)少人数部屋の用意、 (2)セキュリティー、 (3)予算 の3点で悩まれる方が多いと思います。
合宿をペンションで行うことで、この問題が簡単に解決するんです。

1) 少人数部屋が用意しやすい
エンジニアが中心となる開発合宿では、男性ばかりの合宿になることが多いですが、女性エンジニア・デザイナーも気軽に参加できるように、女性部屋の用意は必須となります。
今回参加表明をした女性社員は2名だったので、2名部屋を用意する必要がありました。
開発合宿の限られた予算内で2名以下の部屋を用意するのは意外と大変なんです。
少人数での利用客が多いペンションでは、女性が少数でも専用部屋を簡単に用意できます。

2) 戸締りがしっかりしている
戸締りの必要性は、会社の文化によってまちまちかもしれませんが、メドピアではPCから離れて食事に行く場合、PCが施錠された場所にあることが必要です。
以前、コワーキングスペースで開発合宿を行なった際は、PCを作業場に置きっぱなしで食事に出かけられず、PCを持って食事に行きました。(つらかった)

3) 宿泊費が抑えられる
普通のホテルでは、朝食・夕食・会議室付きでだいたい1泊1万円くらいかと思います。
今回は、2泊で15,400円(税込)でした!!!

合宿のテーマ

メドピアでは、開発合宿で行うテーマとして、普段の業務中にはできない技術研鑽や実装したい機能を自ら決めます。
初日の午後から、最終日の午前中までひたすら開発を行い、最終日の午後に成果発表を行います。
テーマの一部をここで紹介します。
「ChatBotによるサービス改善」
「Rubyのバグ改善」
「RailsのGem開発」
「ElasticSearchで検索機能をもっとよくする」
「新サービスの企画とモック作成」

合宿中

  • 部屋

部屋はこんな感じです。床暖房が効いてて暖かかったです。
f:id:kaoruhotate:20171203173926j:plain

  • 開発中

もくもくもくもく。 f:id:kaoruhotate:20171201104409j:plain

息抜きにペンションの前の川に来ています。 f:id:kaoruhotate:20171201104017j:plain

  • ご飯

朝食・夕食はペンションで用意してもらいましたが、とても美味しかったです。
f:id:kaoruhotate:20171201104031j:plain

  • 最終発表

宿でプロジェクターを借りて、無事最終発表までできました。
f:id:kaoruhotate:20171203115641j:plain

ペンション木馬の合宿について

ここからは、Q.A.方式で合宿について振り返ります。
- ネット繋がるの?
今回11名での合宿でしたが、問題なく繋がりました。(VPN接続もできました)
ペンション木馬の亭主さんは、水上町の飲食店のホームページを作ったこともあるそうで、ネットワークにも詳しい方のようでした。
さらに、開発中にこんなことが。 ↓↓↓ ありがたいです。 www.facebook.com

  • 電源タップは必要?
    大人数でない限り不要だと思います。
    多人数用の電源タップが5, 6個用意されてました。
    会社から電源タップを持って行きましたが、いらなかった。。。

  • お風呂はどう?
    大浴場といった形でしたが気持ちよかったです。
    午前10時から午後3時を除いて入浴が可能だったため、開発に行き詰まった際や夜寝る前でも、自由にお風呂に入って体を癒すことができました。

  • 昼食をとるところはある?
    朝食・夕食はペンションで用意してもらいました。とても美味しく、量も十分でした。
    外にも、駅前や水上温泉街など、オシャレな飲食店が多く、バリエーションも豊富でした。
    平日の昼間でも思ったより混んでいたので、大人数の際は事前に予約した方がいいかもしれません。
    今回は、水上名物焼きカレー カフェレストラン亜詩麻(アシマ)さんと窯焼きピザの店ラ・ビエールさんで昼食を食べました。
    両方とも、オシャレでご飯も美味しかったです。

  • 注意点は?
    プロジェクターを借りる場合、HDMI対応のコネクターが必要でした。

まとめ

開発合宿のアウトプットは、宿や作業場に大きく左右されると思います。 今回利用した「ペンション木馬」さんでは、ネット環境や電源の問題で不自由されることは少なく、美味しいご飯で精気を養い、夜間常に利用できる大浴場で疲れを癒しながら作業ができたため、いつも以上のアウトプットが出せました。

開発合宿の宿泊先をインターネットで調べると、旅館やホテルがたくさん出てきます。 しかし、ペンションで合宿をすることで、よりたくさんのエンジニアの参加を促せたり、宿泊費を抑えることができるかもしれません。 開発合宿をする際は、ペンションを考えてみてはいかがでしょうか。
f:id:kaoruhotate:20171203115554j:plain


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


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

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

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

■開発環境はこちら

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


2017年に始めたメドピアエンジニアチームの新しい取り組みについて

$
0
0

こんにちは。メドピアにエンジニアとしてJoinして7ヶ月、Ruby/Rails歴≒メドピア歴の小林です。

2017年もあとわずかとなりました。今回のブログでは、1年間を振り返る意味も込めて、今年メドピアのエンジニアチームが始めた新しい取り組み(コードレビュー2段階体制・レビュー振り返り会・bundle update当番)についてご紹介したいと思います。

コードレビュー2段階体制について

メドピアでは、コードのレベルを一定に保つため、コードレビューを行っています。また、Railsの経験が豊富なエンジニア(以下、リードエンジニア)がLGTMを出さないとマージできない体制にしています。

特に、Ruby/Rails歴が短い私のようなエンジニアにとっては、先輩のエンジニアにいただけたレビューで今まで見れていなかった問題点に気づけたり、よりわかりやすいコードを書くためのヒントを頂けたりするので、このような体制で開発できることがとてもありがたいと感じています。

しかし私の入社当時(5月頃)には、レビュー体制で抱えている課題がありました。Rails未経験でJoinしたエンジニアが増えてきた際に、未経験エンジニアの数に対しリードエンジニアが少なかった時期があり、レビューの速度アップが必要になったことがあったのです。

そこで、下記のようなレビュー2段階体制を導入するようになりました。

①ファーストレビュー
…Rails未経験/経験年数の比較的浅いエンジニアが他者のコードをレビューする。
Rails経験が浅くても気づけるコードの問題を指摘してセカンドレビュアーの負担を軽減する。

②セカンドレビュー
…一定以上の年数Rails開発経験のあるリードエンジニアがコードレビューする。
主にRailsWayに則った実装が行われているかを確認する。

※ファーストレビューでLGTMがついた場合でも、
セカンドレビュアーによるレビューでLGTMがつかないと基本的にはマージできない。
※セカンドレビュアーが最初からレビューできる場合はファーストレビューは飛ばして良い。

この体制が導入されたおかげで、私も他の人が書いたコードをレビューする機会ができ、自分でコードを書く時とは違った視点でコードを見る機会も生まれ、そのぶん勉強になったと感じています。

最近はリードエンジニアが増えたため、2段階レビューせず最初からリードエンジニアにレビューして貰えることもあります。スケジュールも加味しつつ時と場合より柔軟な体制をとっています。

未経験エンジニアもレビューに関わることで成長したい/させたいが、その人のレビューだけでマージしてしまうには不安がある場合もあると思います。そのようなチームにはこの2段階体制レビューの取り組みをおすすめしたいと思います。

「レビュー振り返り会」と技術共有の場

コードレビューでは、各プロジェクトに固有の問題ではなく開発全体に影響する問題など、機能開発者とそのレビュアーだけでなくエンジニア全員に周知しておいたほうが良いような問題が上がることがあります。

メドピアでは、その週にレビューで上がった上記のような事項を週に1回エンジニア全体に共有・話し合いをする会として「レビュー振り返り会」を今年から開始しました。

Ruby/Rails中上級者が当たり前に知っていて既にみんなわかっているだろうと思うようなことでも、初心者からすると知らないことが多いです。

振り返り会があるおかげで他のRails未経験だったメンバーがどのようにレビューされているかも知ることができます。

Ruby/Rails以外の開発課題についての振り返りもありますが、その際もそれぞれのエンジニアによって違う範囲の知識に詳しかったりするので、知見を共有することでお互いに勉強になっていることが多いと思います。

また、下記のような開発指針について話し合われたこともありました。

  • Rails5.1から使えるようになったform_withform_forとどのように使い分けるか
  • integerカラムのbigint化の進め方について
  • 類似機能を持ったGemの選定について
  • RubocopやEslintのLintルールの変更について

過去にどのような話題が上がったかは社内ドキュメントにまとめているため、あとからJoinしたメンバーになぜそこがそのような実装になっているのかを説明する時に「○月○日の振り返り会の時に皆でこのような経緯で話し合った」という証跡として使えることがあります。

f:id:marikokobayashi:20171227141221j:plain

bundle update当番

Railsで開発しているチームでどのようにbundle updateを行うかというのは1つの懸念事項として上がってくると思います。

メドピアでは以前はbundle updateの頻度が低かったのですが、Rails5.1に上げたことをきっかけにその頻度を増やしていこうということで、毎週Botで当てられた人がbundle updateを行う、bundle update当番というものを開始しました。

差分のソースを読んで既存の仕組みに影響がないかどうか当番が確認するため、導入されているGemがサイト内でどのように使われているか、しだいに覚えていくようになります。

また、気になる差分があったらマージする前に前述の振り返り会で全員に共有しています。

今週も、ちょうど今年最後のbundle updateがリリースされたところです。これで気持ちよく新年を迎えられそうです。

まとめ

メドピアは、昨年に独自フレームワークからRailsへの移行を開始したこともあって、エンジニアチームのメンバーの半数以上が今年に入ってから加わったメンバーで構成されています。

新しく入ってきたメンバーも既存の仕組みに気になることがあれば気兼ねなく改善のアイディアを出していけるような雰囲気で開発しています。

他にも、週に1回Ruby/Railsに関する書籍やWebサイトの輪読会を行ったり、フロント側ではVue.jsもくもく会を開催したり、2泊3日の開発合宿に定期的に行ったりと、エンジニアが技術研鑽を行える体制が整っており、これから成長したいエンジニアにとってはとてもいい環境だと思います。

現在メドピアでは、Rails未経験/経験年数が2年以内のポテンシャルエンジニアを絶賛募集中です!*1

Rails開発してみたい人、新しい技術に意欲的な人、リードエンジニアに教育されてみたい(?)人、医療に関わるサービス開発を行ってみたい人、その他メドピアに興味を持った方などは、是非、コンタクトを取って頂ければと思います!


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


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

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

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

■開発環境はこちら

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

*1:リードエンジニアも募集中です!

Rails + AWS でモバイルフレンドリーな動画配信サイト構築

$
0
0

あけましておめでとうございます。
メドピアのSRE @kenzo0107です。

2018年もよろしくお願いします。

今回は昨年リニューアルした動画配信システムについてです。

経緯

これまでのメドピアの動画配信は CloudFront 経由で S3 上の mp4 を video タグで参照し配信してました。

この配信方法では CloudFront でキャッシュしづらく
通信状況によってはファーストビューまでに時間が掛かり、サイト離脱へ繋がります。

また、直リンク禁止の動画の場合、
リファラチェック等をするかと思いますが
一部 IE Edge のバージョンで video タグでリファラ参照ができないという仕様があり*1
既存の仕組みをフロントから変える必要がありました。

以上の経緯から動画配信の仕組みを見直し要件を洗い出しました。

要件

  • 動画は mp4 で納品される為、HLS形式へエンコードする機構を用意する。
  • 通信状況に依らずサクサクと見ることができる様にする。
  • 電波状況によってレートを変換する。あくまで見続けられる。
  • 直リンク禁止にする。
  • 今後を見据えて特定のユーザにのみ閲覧を許可する機能を盛り込む。

システム概要

要件を満たすべく環境構築しました。概要は以下となります。

① S3 Bucket transcoder.rawに mp4 ファイルをアップ
② ファイルアップをトリガーに Lambda を起動
③ Lambda が ElasticTranscoder を呼び出し
④ mp4 を HLS(m3u8+ts)形式 へ変換し S3 Bucket transcoder.processedにアップ
⑤ エンコードの成功・失敗をSlackに通知
⑥ ユーザがサイトにアクセス
⑦ Rails から 認証機能を利用し CloudFront にアクセス
⑧ CloudFront がバケットへ対象ファイルにアクセス

f:id:kenzo0107:20180118162230p:plain

理解を深めるべく AWS コンソールで構築手順をお伝えします。

S3 Bucket 作成

mp4 ファイルをアップロード先のバケット(transcoder.raw)、
エンコードされたファイルを格納するバケット(transcoder.processed
を作成します。

transcoder.rawのポリシー作成

mp4 ファイルをアップロード元を許可します。

今回は以下からの全アクションを許可しています。

  • 管理画面URL(https://admin.example.com)
  • 社内IP
{
    "Version": "2012-10-17",
    "Id": "transcoder.raw",
    "Statement": [
        {
            "Sid": "allow-referer",
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::transcoder.raw/*",
            "Condition": {
                "StringLike": {
                    "aws:Referer": [
                        "https://admin.example.com/*"
                    ]
                }
            }
        },
        {
            "Sid": "allow-ad-referer",
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::transcoder.raw/*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": [
                        "<社内 IP>/32"
                    ]
                }
            }
        }
    ]
}

Elastic Transcoder 作成

動画変換の肝の部分です。

以下の様に Elastic Transcoder のパイプラインを作成していきます。

f:id:kenzo0107:20180117133302p:plain

任意(Optional) で 動画エンコード成功可否について通知設定があります。

完了時(On Complete Event)とエラー発生時(On Error Event)にSNS 経由で Slack に通知する様にしました。

f:id:kenzo0107:20180117133342p:plain

CloudFront 作成

S3 bucket transcoder.processedを参照する CloudFront を立てます。

  • delivery methodは Web です。

Origin 設定

  • Restrict Bucket Access : Yes ... アクセス制限を設定します。
  • Origin Access Identity : Create a New Identity ... ID を作成します。
  • Grant Read Permissions on Bucket : Yes, Update Bucket Policy ... transcoder.processedへのアクセスポリシーを更新します。

f:id:kenzo0107:20171224161208p:plain

Behavior 設定

ここで重要なのは Restrict Viewer Access (Use Signed URLs or Signed Cookies)の設定を Yes にすることです。
これにより署名付き URL/Cookie のみ CloudFront へのアクセスが可能となり、直リンクを防止できます。

f:id:kenzo0107:20180120232221p:plain

また、Whitelist Headers で Origin を追加し S3 transcoder.processed の CORS でアクセス許可する URL を絞ることができます。

f:id:kenzo0107:20180120232319p:plain

Distribution 設定

  • Alternate Domain Names : cdn.example.com ... Cookie を有効化させる為、参照元ドメイン (example.com) のサブドメインとします。
  • SSL Certificate : Custom SSL Certificate ... *.cloudfront.netというドメインでなく固有のドメインを利用する為、カスタムSSL証明書を選択
  • Custom SSL Certificate Support : Only Clients that Support Server Name Indication (SNI) 選択します
  • Security Policy : 特に希望なければ recommended を選択します。

f:id:kenzo0107:20180120232946p:plain

  • Supported HTTP Versions : HTTP/2, HTTP/1,1, HTTP/1.0 ... HTTP/2 の恩恵を受けましょう
  • Logging : On ... アクセスログを取るかどうかの設定です。取れるものは取りましょう!
  • Bucket for Logs : transcoder.processed.s3.amazonaws.com ... ログを貯めるバケットです。エンコードされた動画用バケットを使うこととしました。
  • Log Prefix : logs-transcoder-cloudfront ... 保存するログのプリフィックスです。分かり易くしましょう。
  • Cookie Logging : On ... Cookie ログも取っておきます。

f:id:kenzo0107:20171224163250p:plain

以上設定完了後、Create Distributionボタンを押下し作成します。

Lambda 作成

Lambda の役割

  1. mp4 ファイルを transcoder.rawにアップしたことを検知し
  2. ElasticTranscoder のパイプラインに渡し
  3. m3u8, ts ファイルを transcoder.processedに格納します

Lambda 関数作成

Lambda 関数の作成で 一から作成を選択

  • 関数の名前: VideoTranscodingInAWS
  • ランタイム: Node.js 6.10
  • ロール: カスタムロールの作成

新しい IAM ロールの作成

ロール名: lambda_elastictranscoder_execution

  • ポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:ap-northeast-1:xxxxxxxxxxxx:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::transcoder.raw/*",
                "arn:aws:s3:::transcoder.processed/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "elastictranscoder:CreateJob"
            ],
            "Resource": "arn:aws:elastictranscoder:ap-northeast-1:xxxxxxxxxxxx:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "sns:Publish"
            ],
            "Resource": "arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:ElasticTranscoderNotificationToSlack"
        }
    ]
}
  • 信頼ポリシー
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

スクリプト

ハンドラ: index.handler

'use strict';

var AWS = require('aws-sdk');
var s3 = new AWS.S3({
    apiVersion: '2012-09-25'
});

var transcoder = new AWS.ElasticTranscoder({
    apiVersion: '2012-09-25',
    region: 'ap-northeast-1'
});

// return dirname without extensionn
function dirname(path) {
    var p = path.split(path.sep).pop().split('.')[0];
    return decodeURIComponent(p);
}

exports.handler = function(event, context) {

    console.log('Executing Elastic Transcoder Orchestrator');

    var bucket = event.Records[0].s3.bucket.name;
    if (bucket !== 'transcoder.raw') {
        context.fail('Incorrect Video Input Bucket');
        return;
    }
    
    var pipelineId = '<ElasticTranscoder Pipeline ID>';
    var key = event.Records[0].s3.object.key;
    var dkey = dirname(key);

    console.log("(^-^)key");    
    console.log(key);
    console.log(dkey);

    var params = {
        Input: {
          Key: key,
          FrameRate: 'auto',
          Resolution: 'auto',
          AspectRatio: 'auto',
          Interlaced: 'auto',
          Container: 'auto',
        },
        PipelineId: pipelineId,
        Outputs: [
          {
            Key: dkey + '/600k/s',
            PresetId: '1351620000001-200040', // hls 600k
            SegmentDuration: '10'
          }
          ,{
            Key: dkey + '/1M/s',
            PresetId: '1351620000001-200030', // hls 1M
            SegmentDuration: '10'
          }
          ,{
            Key: dkey + '/2M/s',
            PresetId: '1351620000001-200010', // hls 2M
            SegmentDuration: '10'
          }
          ,{
            Key: key,
            PresetId: '1351620000001-000010', //Generic 720p - mp4
            ThumbnailPattern: dkey + '-{count}'
          }
        ],
        Playlists: [
          {
            Name: dkey,
            Format: 'HLSv3',
            OutputKeys: [
                dkey + '/600k/s', 
                dkey + '/1M/s', 
                dkey + '/2M/s'
            ]
          }
        ]
    };

    transcoder.createJob(params, function(err, data){
        if (err) {
            console.log(err, err.stack);
            context.fail();
            return;
        }
        context.succeed('Job well done');
    });
};

<ElasticTranscoder Pipeline ID>を先ほど作成した Pipeline ID を設定してください。

上記スクリプトの要点は以下です。

ビットレート毎 (600k, 1M, 2M) に出力

f:id:kenzo0107:20171224150345p:plain

上位の m3u8 ファイルによってビットレート毎の再生ファイルが管理されています。

帯域に余裕がある場合はプレイヤー側で
高いレート(2M: High)のファイルを選択するようになり
高画質の動画が閲覧できます。

逆に帯域に余裕がなく通信状況が悪い場合は
低いレート(600k: Low)を選択するようになります。

これによってユーザの通信状況にリアルタイムに合わせたレートでのストリーミング配信が可能になります。

ディレクトリ構成を担保したままファイル出力

transcoder.rawにアップしたディレクトリ構成を担保した状態で transcoder.processedに ts ファイルを作成するようにしています。
既存環境からの移行を加味しました。

SLACK_WEBHOOK_URL を変数で設定

環境変数として設定することで Slack 通知先を変えられる様にしました。

transcoder.processed アクセスポリシー確認

Elastic Transcoder での IAM 作成や
CloudFront での Bucket ポリシーをアップデート処理で
以下の様なポリシーが生成されています。

{
    "Version": "2012-10-17",
    "Id": "Policy-CDN",
    "Statement": [
        {
            "Sid": "Stmt1505204403832",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::<aws account ID>:role/Elastic_Transcoder_Default_Role"
            },
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::transcoder.processed/*"
        },
        {
            "Sid": "2",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity <CloudFront Origin Access ID>"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::transcoder.processed/*"
        }
    ]
}

S3 Bucket transcoder.raw で mp4 ファイルアップロード検知

S3 Bucket transcoder.rawの プロパティ > Events 設定します。

  • ObjectCreate (All) にチェックを入れます。
  • サフィックス : mp4
  • 送信先 : Lambda 関数
  • Lambda : VieoTranscodingInAWS

上記設定し保存します。

名前入れなくとも、自動で名前が生成されます。

f:id:kenzo0107:20171224170139p:plain

これで transcoder.rawに mp4 ファイルをアップロードすると Lambda が検知し
HLS ファイルを生成してくれる様になります。

エンコードされるか試してみる

transcoder.rawmedpeer/soreha/sutekina/shokubaとフォルダを作成し mp4 ファイルをアップロードします。

無料動画素材を以下から取得しました。*2

http://www1.nhk.or.jp/archives/creative/material/view.html?m=D0002060315_00000

f:id:kenzo0107:20180117171230p:plain

Slack から通知が届きました。*3

f:id:kenzo0107:20180117171920p:plain

エンコードしたファイルの格納先 transcoder.processedを見てみます。
無事フォルダ構成を担保したまま HLSフォーマット m3u8 ファイルやサムネイル画像が出力されています。

transcoder.processed/medpeer/soreha/sutekina/shokuba/
├── D0002060315_00000_V_000/
│        ├── 1M/
│        │         ├── s.m3u8
│        │         ├── s0000.ts
│        │         ├── ...
│        │         └── s0004.ts
│        ├── 2M/
│        │         ├── s.m3u8
│        │         ├── s0000.ts
│        │         ├── ...
│        │         └── s0004.ts
│        └── 600k/
│        │         ├── s.m3u8
│        │         ├── s0000.ts
│        │         ├── ...
│        │         └── s0004.ts
├── D0002060315_00000_V_000-00001.png
├── D0002060315_00000_V_000.m3u8
└── D0002060315_00000_V_000.mp4

Rails 改修

CloudFront の認証機能によってローカルの Rails on Vagrant から参照できる様にします。
ローカル Rails 環境のドメインを dev.example.comとします。

CloudFront 認証機能には以下 2つの方法があります。

  • 署名付き URL
    • URL に対して CloudFront 認証情報・期限を URL パラメータで渡す
    • CloudFront 上の 1 ファイルに対して認証設定可能*4
  • 署名付き Cookie
    • Browser 上に CloudFront 認証情報・期限を Cookie に保存
    • CloudFront 上の 複数のファイルに対して認証設定可能

上記特性より
署名付き URL を 1ファイルで動画を構成する mp4 で
署名付き Cookie を 複数ファイルで動画を構成する m3u8 で
試験したいと思います。

作成・修正するファイルリストです。

  • app/config/secrets.yml
  • app/controllers/concerns/common.rb
  • app/controllers/hoges_controller.rb (署名付き URL 用)
  • app/views/hoges/index.html.erb (署名付き URL 用)
  • app/controllers/moges_controller.rb (署名付き Cookie 用)
  • app/views/moges/index.html.erb (署名付き Cookie 用)

app/config/secrets.yml

AWS console 上で root 権限で作成した Key Pair IDと Private Key を設定しています。

development:
  secret_key_base: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  cloudfront_key_pair_id: xxxxxxxxxxxxxxxxxx
  cloudfront_private_key: "-----BEGIN RSA PRIVATE KEY-----\nxxxxxxxxxxxxxxxxxxxxx+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n-----END RSA PRIVATE KEY-----"

app/controllers/concerns/common.rb

module Common
  extend ActiveSupport::Concern

  # CloudFront 署名付き Cookie 認証用 Cookie 設定
  def cookie_data(resource, expiry)
    raw_policy = policy(resource, expiry)
    {
      'CloudFront-Policy' => safe_base64(raw_policy),
      'CloudFront-Signature' => sign(raw_policy),
      'CloudFront-Key-Pair-Id' => Rails.application.secrets.cloudfront_key_pair_id,
    }
  end

  # CloudFront 用 署名付き URL 取得
  def get_cloudfront_signed_url(resource, expiry)
    expire      = expiry.utc.to_i
    raw_policy  = policy(resource, expiry)
    signature   = sign(raw_policy)
    key_pair_id = Rails.application.secrets.cloudfront_key_pair_id
    "#{resource}?Expires=#{expire}&Signature=#{signature}&Key-Pair-Id=#{key_pair_id}"
  end

  private

  def policy(url, expiry)
    {
      "Statement" => [
        {
          "Resource" => url,
          "Condition" => {
            "DateLessThan" => { "AWS:EpochTime" => expiry.utc.to_i },
          },
        },
      ],
    }.to_json.gsub(/\s+/, '')
  end

  def safe_base64(data)
    Base64.strict_encode64(data).tr('+=/', '-_~')
  end

  def sign(data)
    digest = OpenSSL::Digest::SHA1.new
    key    = OpenSSL::PKey::RSA.new Rails.application.secrets.cloudfront_private_key
    result = key.sign digest, data
    safe_base64(result)
  end
end

署名付き URL

app/controllers/hoges_controller.rb

署名付き URL を生成します。
mp4 ファイル URL に対して10秒間有効な期限付き動画 URL を生成します。*5

class HogesController < ApplicationController
  include Common

  def index
    # 署名付き URL
    @cloudfront_signed_url = get_cloudfront_signed_url(
      'https://cdn.example.com/medpeer/soreha/sutekina/shokuba/D0002060315_00000_V_000.mp4',
      10.seconds.from_now
    )
  end
end

app/views/hoges/index.html.erb

<video poster="https://cdn.example.com/medpeer/soreha/sutekina/shokuba/D0002060315_00000_V_000-00001.png" preload="auto" controls="controls">
  <source src='<%= @cloudfront_signed_url %>' type='video/mp4'>
</video>

署名付き URL を確認してみる

動画が再生されました!
生成された期限付き動画URLを確認すると非常に長いURLパラメータが付与されていることが確認できます。

f:id:kenzo0107:20180118114358p:plain

そして 10 秒後、ソースから生成された期限付きURLを別途プライベートモードのブラウザで開こうとすると閲覧不可状態となっていることがわかります。

f:id:kenzo0107:20180118114949p:plain

署名付き Cookie

app/controllers/moges_controller.rb

10秒間有効な期限付きの署名付き Cookie を生成します。

class MogesController < ApplicationController
  include Common

  def index
    # 署名付き Cookie
    @cloudfront_url = 'https://cdn.example.com/medpeer/soreha/sutekina/shokuba/D0002060315_00000_V_000.m3u8'
    cookie_data('https://cdn.example.com/*', 10.seconds.from_now).each do |k, v|
      cookies[k] = { value: v, domain: 'example.com', path: '/' }
    end
  end
end

app/views/moges/index.html.erb

簡易的にクラウド上の hls.js 上を利用していますが、実際にはダウンロードして利用しています。

<video id="video" width="600" height="300" class="video-js vjs-default-skin" controls>
<script src="https://cdn.jsdelivr.net/npm/hls.js"></script>
<script>
if(Hls.isSupported()) {
  var video = document.getElementById('video');
  var config = {
    xhrSetup: function(xhr, url) {
      xhr.withCredentials = true;
    }
  }
  var hls = new Hls(config);
  hls.loadSource("<%= @cloudfront_url %>");
  hls.attachMedia(video);
  hls.on(Hls.Events.MANIFEST_PARSED,function() {
    // video.play();
  });
}
</script>

署名付き Cookie を確認してみる

署名付きURLとは異なり、埋め込まれる動画URLは変更ありません。
CloudFront 関連 Cookie が追加されていることがわかります。

  • CloudFront-Signature
  • CloudFront-Policy
  • CloudFront-Key-Pair-Id

f:id:kenzo0107:20180118124520p:plain

そして 10 秒後、Cookie を保存した同一のブラウザ上で 動画URL にアクセスしてみると閲覧不可状態となっていることがわかります。

https://cdn.example.com/medpeer/soreha/sutekina/shokuba/D0002060315_00000_V_000.m3u8

f:id:kenzo0107:20180118114949p:plain

また動画再生中に通信状況を意図的に劣化させることで再生される動画ファイルが変更されている様子がわかります。
是非試してみてください♪

元々の mp4 が ts ファイルに小分けにされキャッシュヒット率も上がり CloudFront 的にもメリットが大きいと感じました。

以上で Rails + AWS で直リンク対策を施した HTTP Live Streaming 動画配信環境が構築できました。

おまけ

既存動画バケットから新規バケットへ移動

新たに今回の HLS 動画閲覧システムを作成した後、
既存の mp4 を格納した S3 Bucket から移行が必要かと思います。

そんな時はコレ

macOSX%$ aws s3 sync --profile <profile> --region <region> \
--exclude "*" \
--include "*.mp4" \
s3://<既存の mp4 バケット名>/ \
s3://transcoder.raw/

初回動画確認前には Invalidation でキャッシュ削除

意外とハマりました。
CloudFront で誤ったキャッシュを保持しアクセスしても期待した動作にならない事象があった為、
問題があった場合はキャッシュを削除しておくと問題の切り分けができます。

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


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


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

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

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

■開発環境はこちら

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

Rails未経験/経験年数が2年以内のポテンシャルエンジニア絶賛募集中です!*6

Rails開発してみたい人、新しい技術に意欲的な人、リードエンジニアに教育されてみたい(?)人、医療に関わるサービス開発を行ってみたい人、その他メドピアに興味を持った方などは、是非、コンタクトを取って頂ければと思います!

*1:Microsoft Edge 40 1506300, Microsoft EdgeHTML 15.15063 でリファラ参照不可であることを確認しています。

*2:NHKクリエイティブ・ライブラリーで無料提供されている動画素材です。試験するのにうってつけでした。

*3:ElasticTranscoder のイベントログを加工してます。

*4:複数の URL に対して署名付きURL生成設定すれば複数設定は可能

*5:試験の為、10秒間としています。

*6:リードエンジニアも募集中です!

UI/UX勉強会、開催しました!

$
0
0

皆様、こんにちは。

ご無沙汰してしまいました、GWはいかがお過ごしでしたか? 連休明けに日焼けで黒くなって来た同僚が居て驚いてしまった、メドピアエンジニアの西澤です。

今日は、先月30日に弊社オフィスで行いました、UI/UX勉強会について報告しようと思います。

これからのデザインに求められるものとは?

何故この勉強会を行う事になったかと言うと、発端は弊社デザイナーの松Mでした。 IoTの本格化もあり、デザイン手法やデザイナーに求められる役割が大きく変わっていく現状に勉強意欲を刺激された松Mはあっという間に社内調整とプレゼンターへの依頼を進め、開催へと漕ぎ着けました。

これから考えるべき事は?

ここからは当日のプレゼンテーションを簡単に紹介させていただこうと思います。 プライバシー・バイ・デザインとこれからのデザイナーの役割
博士(医薬学)笹原 英司さま
特定非営利活動法人ヘルスケアクラウド研究会
一般社団法人日本クラウドセキュリティアライアンス
在日米国商工会議所 ヘルスケアIT小委員会


概要は以下の通りです。
これまで画面の中で完結していた「インターネット」という世界が、IoTによりあらゆるものの中へと広がりを見せることで、プライバシーやアクセシビリティに関してこれまで意識されることのなかった問題が生じてくる。プライバシーについては、問題についての考察、対策等が不可欠になってくると考えられ、またアクセシビリティについては、五感を介して得たインスピレーションをもとに、より直感的にわかりやすく表現、デザインし、誰にでも利用しやすい形にすることが重要になってくる。また、ユーザとのインタラクティブな関係が重要性を増し、デザインの評価にもユーザー自身に参加してもらうことが大事になってくる。 当日の資料がSlideShareで共有されているので紹介させていただきます。
「プライバシー・バイ・デザイン」とこれからのデザイナーの役割

SCQRMについて
常盤 拓司さま
合同会社アライアンス・ポート研究開発担当ディレクター
慶應義塾大学大学院政策・メディア研究科特任講師


PMBOKでは解決できなかった問題を解決する手法として提唱されたSCQRMという考え方についてお話しいただきました。

  • SCQRMとは・・・
    スクラムと読みます。システム開発手法のスクラム(Scrum)とは別の物です。
    Structure-Constructive Qualitative Research Method の頭文字を取った物で、日本語では構造構成的質的研究法と訳せます。

事例数(データ量)を前提にするのではなく、概念化・抽象化できるかに主眼を置き事例として扱うという手法はとても興味深い物です。こちらも共有いただいた資料がありますので、紹介させていただきます。
SCQRMについて

Chlorixについて
Nana Sakisakaさま

Chlorixとは、Nana Sakisakaさんご自身が開発されている、医薬品・化合物・症候・傷病の総合検索エンジンです。
Chlorix
この会のテーマの一つである「誰のためのデザインか?」に対して、「研究者のためのデザインである」という明確な答えをお持ちの上で活動されているところに、ユーザを惹きつけるUXのあり方を見たように思います。
また、官公庁のオープンデータ等をベースに作られ、医師、薬剤師が重視する薬物動態データを掲載するなど、実際の利用シーンを念頭に置いた構成となっているようです。


まとめ

プライバシーやウェブサイト、検索エンジンとデザイン等、それぞれのお立場からデザインについてお話しいただきました。
個人的な感想ですが、UI/UXと一括りで表されることの多いこの言葉、全く別物なのかな、と感じました。 UXを真剣に考え出すと、社会的・文化的背景や利用者個々人のバックグラウンド等、全てを含めて考えた上でベストな解を探すことになってしまうような。

と、少し真面目になってしまいましたが実際は和気藹々と楽しく、刺激的な勉強会でした。勉強会の風景を下に載せておきます。
f:id:MedPeer:20150522100056j:plain今後も誰かの関心事を発端にこのような勉強会も次々開催出来ればと思っております。

だんだん暑くなってきましたね、体調にはお気を付けてお過ごし下さい!

メドピア株式会社 エンジニア 西澤

Browsersyncを利用してお手軽ブラウザ確認環境をつくろう

$
0
0

皆様はじめまして。メドピアエンジニアの中村です。
好きなブラウザは Vivaldiです。

本記事ではWeb開発効率化の為のひとつの手段として、Browsersyncの利用方法と幾つかの機能をご紹介します。

Browsersyncとは

Browsersyncはファイル変更を監視し、自動でブラウザリロードを行ってくれるツールです。
Browsersync - Time-saving synchronised browser testing

同種のツールとしてLiveReloadが有名ですが、

  • ブラウザ側でExtentionや拡張が不要
  • 複数ブラウザで操作の同期もできる
  • その他の機能も豊富

などのメリットから、最近はBrowsersyncの方が人気のようです。

さっそくBrowsersyncを試してみよう

まずはBrowsersyncをインストールしてみましょう。
nodeのパッケージマネージャであるnpmコマンドからインストール可能です。

インストールコマンド

$ npm install browser-sync --save-dev# local環境にインストール
$ npm install browser-sync -g# global環境にインストール

これでインストール完了です。

ではBrowsersyncを実行してみましょう。
なお、サンプルとしてまずは以下のようなシンプルなサイト構成で解説します。

.
├── css
│   └── main.css
└── index.html

index.htmlはmain.cssを参照しているものとします。

ではコマンド実行してみます。

$ browser-sync start--server--files"**/*"[BS] Access URLs:
 -------------------------------------
       Local: http://localhost:3000
    External: http://192.168.0.57:3000-------------------------------------
          UI: http://localhost:3001
 UI External: http://192.168.0.57:3001-------------------------------------[BS] Serving files from: ./
[BS] Watching files...

コマンド実行後、上記のようなログメッセージがconsoleに流れつつ、デフォルトブラウザの新しいタブに http://localhost:3000にすごいサイトが表示されるハズです。

f:id:yuzurunakamura:20150604165654p:plain
※ ここではサンプルとして Bootstrapのexampleを利用しています。

この状態のまま、試しにcssを編集してみましょう。

body{background-color: #ccc;
}

すると自動で更新内容が反映されます。すごいですね。

f:id:yuzurunakamura:20150604165820p:plain

--files オプションにglobバターンで指定したファイル群をwatchし、ファイルに変更があれば自動でリロードしてくれます。
ここはLiveReloadと一緒ですね。

Browsersyncでできるコト

他にも様々な機能があるので幾つかご紹介します。

プロキシ実行

--server オプションの代わりに --proxy オプションを利用するとプロキシとして実行することが可能です。

$ browser-sync start--proxy"sugoi-service.jp"--files"**/*"

ローカル共有

前述の通りBrowsersync実行時に、

$ browser-sync start--server--files"**/*"[BS] Access URLs:
 -------------------------------------
       Local: http://localhost:3000
    External: http://192.168.0.57:3000-------------------------------------
          UI: http://localhost:3001
 UI External: http://192.168.0.57:3001-------------------------------------[BS] Serving files from: ./
[BS] Watching files...

のようなログメッセージが流れます。
ローカルネットワーク上ではこの http://192.168.0.57:3000へアクセスしても閲覧可能です。
キャプチャ画像をSlackでやりとりせずともちゃっちゃっと開発画面共有ができますね。

bs-configファイル(設定ファイル)による実行

毎回細かいオプションを付与して実行するのは面倒です。
bs-config.jsという設定ファイルを用意し、

$ browser-sync start--config bs-config.js

のように--configオプションでbs-config.jsファイルを指定実行すれば毎回同じ設定で起動可能です。
なお、bs-config.jsファイルは、

$ browser-sync init

で生成可能なので、こちらでデフォルト設定のbs-config.jsを生成し、お好みで設定すると便利です。

リロードのウェイト、watch対象のファイル群指定/除外、ログ出力設定など、30個以上のオプションがあります。
全オプションは Browsersync optionsを参考にしてください。

Browesersync管理画面

Browsersyncには管理画面もついています。
デフォルトでは http://localhost:3001が管理画面で、ここからBrowsersyncを起動したまま幾つかのオプションを切り替え可能です。

f:id:yuzurunakamura:20150604165838p:plain

ブラウザ同期

個人的にBrowesersyncの目玉だと思っている機能です。
複数ブラウザでクリック, スクロール, フォーム操作を同期することができます。

f:id:yuzurunakamura:20150604165900p:plain

なにが嬉しいのかというと、
f:id:yuzurunakamura:20150604170029p:plain
のようにPC/スマホそれぞれのウィンドウ幅にした状態で開発すると、レスポンシブデザイン環境でもスムーズに確認可能です。

また、各種イベントが同期されるのでローカルでのクロスブラウザ確認を容易に行うことができますね。
modernIEやiOSシミュレータから確認するのにも有用です。

リモートデバッグ機能

各要素のアウトライン表示や、グリッド表示を行うことが可能です。

f:id:yuzurunakamura:20150604165919p:plainf:id:yuzurunakamura:20150604165955p:plain

ちなみにCSSアウトライン表示機能その他のフィルタリング機能は Vivaldiにはブラウザに最初から組み込まれています。今すぐインストールしましょう。

ネットワーク制限

f:id:yuzurunakamura:20150604165940p:plain
Chromeにも同様の機能がありますが、特定ブラウザでのネットワーク制限下でのテストを試したい場合には有用かもしれません。

GulpにBrowesersyncを組み込む

LESS/SassやTypeScript/CoffeeScriptコンパイルやminifyの為に既にGulp/Gruntを利用しているケースもあるかと思います。
その場合、以下のようなgulpfile.jsを記述すれば gulpコマンドで実行可能です。

var gulp        = require('gulp');
var browserSync = require('browser-sync').create();

// 静的ファイルのみ場合
gulp.task('browser-sync', function() {
    browserSync.init({
        server: {
            baseDir: "./"}});
});

// プロキシ実行の場合
gulp.task('browser-sync', function() {
    browserSync.init({
        proxy: "sugoi-service.jp"});
});

gulp.task('default', ['browser-sync']);

同様にGruntにも組み込み可能です。Browsersync + Grunt.js
また、GulpやGruntで利用できるAPIのドキュメントも公開されています。Browsersync API
gulpfile.jsに組み込んでしまえば、複数サイトを同時に立ちあげることもできますので、gulpfile.jsにすべて寄せてしまった方が便利でしょう。

まとめ

Browsersyncを利用すると、リロードの手間を省きつつブラウザ確認が容易になります。
既存のGulp/Grunt環境への組み込みもカンタンですのでぜひ試してみてください。

Laravel5.0:FormRequestを使ったValidation

$
0
0

はじめまして。 メドピアエンジニアの酒井です。

まだ使い始めてそんなに経っていませんが、Laravelの小ネタを紹介したいと思います。

Laravel5.0で導入されたFormRequestを使用して Controllerをスッキリさせる方法です。

Laravelを触ったのは5.0が始めてですが。

実際に見てみる

まず、最初に通常のValidationの書き方はこんな感じです。

<?phpuse App\Http\Requests\Request;

Class SampleController extends Controller
{publicfunction getIndex(Request $request){$validator= Validator::make($request->all(), ['name'=>'required',
            'email'=>'required|email']);
        if($validator->fails()){}}}

見ても分かるようにContollerに記述していくとForm項目が増えるにつれ、 Controllerに大量のValidation設定を記述する事になります。

次にFormRequestを使用したControllerです。

<?phpuse App\Http\Requests\SampleRequest;

Class SampleController extends Controller
{publicfunction getIndex(SampleRequest $request){}}

すっきりしましたね!

FormRequestって何?

Laravel5.0から導入されたValidation処理を含んだRequestクラスとなります。

詳しくはこちら

リクエストをdispatchした際、Controllerメソッドの依存関係を解決した後に validate()が実行されるようになっています。

つまり、FormRequestを使用するとControllerメソッドの実行前に Validationの実行が完了している事になります。

FormRequestの使い方

<?phpuse App\Http\Requests\Request;

Class SampleRequest extends Request
{//認証ユーザーが権限あるかチェックするpublicfunction authorize(){returntrue;
    }//Validationの設定publicfunction rules(){return['name'=>'required',
            'email'=>'required|email']}//エラー文言の設定publicfunction messages(){return['name.required'=>'The :attribute field is required.',
            'email.required'=>'The :attribute field is required.',
            'email.email'=>'The :attribute must be a valid email address.',
        ];
    }}

public function authorize()

権限チェックする必要ない場合もこれを指定しないとエラーとなりますので 必ず記述するようにします。

あとはviewに以下のように記述すれば、Form画面にエラーを出力できるようになります。

@if (count($errors) > 0)
    @foreach ($errors->all() as $error)
        {{ $error }}
    @endforeach
@endif

エラー時のリダイレクト先は特に指定がなければ、前画面(Form画面)になります。

頻度の高いValidationルール

rule内容
alpha英字チェック
alpha_num英数字チェック
between:min,max最小値と最大値チェック
emailメールアドレスチェック
numeric数字チェック
regex正規表現によるチェック ※1
required入力値の存在チェック
same:field指定されたフィールドとの同値チェック

詳しくはこちら

※1 regexで「|」を使用する場合はrulesの指定を配列にしないと動作しません。

<?phppublicfunction rules(){return['sort'=>['regex:/^(desc)|(asc)$/']]}}

これでしばらくハマりました。。。

まとめ

Illuminate\Foundation\Http\FormRequestを見ると リダイレクト先やエラー時の挙動を変更できるようになっているので、 Validation関連のだいたいの事はFormRequestで行えると思います。

ControllerがValidation設定ばかりになってきたら、 FormRequestの利用を検討してみるのもいいかもしれませんね。

社内LT紹介!

$
0
0

こんにちは。メドピアエンジニアの西澤です。梅雨らしい雨の日が続いていますが皆様いかがお過ごしですか?

今日はメドピアで不定期(ほぼ月1回)で実施しているLightning Talk大会を紹介します。Lightning Talkについてはこちらをご参照下さい。

ライトニングトークとは - はてなキーワード

実際は5分に制限する事もなく、ドラもドラ娘も置かずに実施しているわけですが(^^;)

弊社はオフィス内にオープンなミーティングスペース(セントラルパーク)があり、LTをやるにも適しています。(私のお気に入りのスペースの1つです。) f:id:ikuheinishizawa:20150701181504j:plain

何枚かLT中の写真も。 f:id:ikuheinishizawa:20150701181506j:plainピザをつまみながら。 f:id:ikuheinishizawa:20150701181509j:plain

これまで発表されたテーマは以下のようなものがあります。(本当はもっとあります!)機会があればこのブログでも紹介出来ればと思っています。

  • Go into Golang(Go言語入門)
  • SwiftでUIKit Dynamics
  • Research Kitを触ってみた
  • (元・某ベンチャーCTOによる)シード期の開発現場について
  • (入社直後のメンバーによる)自身のエンジニアキャリア披露

等等・・・

元々このLT大会、個々のエンジニアが持っているスキルや興味を気楽に皆で共有し、研鑽する事を目的に始まりました。 実際やってみると、興味の幅が広がったり、自分が手を付けられていなかった分野を学べたり、業務で役に立ったり、なかなか良い手応えがあります。また、オープンスペースのおかげもあり、エンジニア以外の社員も少し覗いて行ったり出来るので、交流の場にもなっている、と良いな。

今はエンジニアが技術中心の話題で発表していますがいずれ、ディレクターだったり社外の方も交えて開催出来たらと思っています。

それでは、雨にも負けず楽しく夏を迎えましょう!

メドピア株式会社 エンジニア 西澤

LaravelとLumenのパフォーマンスを比較してみた

$
0
0

こんにちは。エンジニアの尾澤です。

先日、酒井がLaravel5.0:FormRequestを使ったValidationというエントリでLaravelのTipsをご紹介しましたが、今回は、そのLaravelと同じコミュニティで開発している別の軽量フレームワークであるLumenとパフォーマンスの比較をしてみたいと思います。バージョンは5.1を使用します。

Lumenのベンチマークとしては、公式サイトで他の軽量フレームワークであるSilexとSlim 3との比較が紹介されていますが、相互にコードの移植性が高いLaravelとの比較がなかったので、調べてみました。

LaravelとLumenの機能面での比較はこちらのエントリーによくまとめられていますので、参考にしてください。

検証環境

検証には、同じくLaravelコミュニティが提供しているVagrant環境であるHomesteadを使用しました。なお、HomesteadはHHVMをサポートしていますが、今回は使用していません。

検証マシンのスペックは以下の通りです。ちなみにメドピアでは、エンジニア・デザイナの全員にMacbook Proを支給していますので、開発作業は快適です(ง ˙ω˙)ว☆

MacBook Pro (Retina, 14-inch, Mid 2014)
Processor 2.8GHz Intel Core i5
Memory 16GB 1600MHz DDR3
Storage 500GB Flash Storage

検証環境には、それぞれのフレームワークで同じ仕様のアプリケーションを実装しました。アプリケーションはシンプルに、GETリクエストに対して常に同じ内容のテキストをレスポンスとして返します。テキストはJSONフォーマットで、Homestead内のMySQLから取得したデータを使用します。LaravelとLumenでは多少書き方は変わるところもありますが、クラス構成やロジックは同じです。フレームワークの以下の機能を利用しました。

  • Routing
  • Controllers
  • Service Providers
  • Eloquent ORM

速度の比較

速度の比較にはApache Benchを使用しました。接続数100で1000リクエストを発行して、1リクエストあたりのレスポンスタイムを比較してみます。

まずはLaravelから。

user:homestead mymac$ ab -n 1000 -c 100 http://laravel.app/sample

〜中略〜

Concurrency Level:      100
Time taken for tests:   113.675 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      12632114 bytes
HTML transferred:       11679000 bytes
Requests per second:    8.80 [#/sec] (mean)
Time per request:       11367.500 [ms] (mean)
Time per request:       113.675 [ms] (mean, across all concurrent requests)
Transfer rate:          108.52 [Kbytes/sec] received

〜後略〜

Time per requestを確認すると、1リクエストあたり113.675 msかかってることが分かります。

Laravelの検証にあたってはphp artisan optimizeコマンドによるコード最適化を行っていません。最適化による速度の改善については別の機会に書きたいと思います。

次にLumen。

user:homestead mymac$ ab -n 1000 -c 100 http://lumen.app/sample

〜中略〜

Concurrency Level:      100
Time taken for tests:   30.072 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      11840000 bytes
HTML transferred:       11679000 bytes
Requests per second:    33.25 [#/sec] (mean)
Time per request:       3007.196 [ms] (mean)
Time per request:       30.072 [ms] (mean, across all concurrent requests)
Transfer rate:          384.49 [Kbytes/sec] received

〜後略〜

Lumenでは、1リクエストあたり30.072 msでした。Laravelの113.675 msと比較すると3.78倍も速いという結果になりました!こんなに差が出ると思わなかったのでビックリ。

負荷の比較

それでは、サーバの負荷についてはどうでしょう?いくら速くてもリソース消費が著しければ意味がないですからね。Apache Benchを実行している状態で、vagrantインスタンスの負荷をvmstatコマンドで記録し、gnuplotでグラフ化してみました。

Laravelの負荷f:id:o205:20150708185806p:plain

Lumenの負荷f:id:o205:20150708185818p:plain

CPU負荷のグラフを比較すると、Lumenの負荷の方が軽いことが分かります。vmstatの実測値からアイドリング率を平均すると、Laravelが9.0%、Lumenが31.4%でしたので、LumenのCPU負荷はLaravelの75.4%ということになります。だいぶCPUに優しいですね!

メモリ負荷については若干Lumenの方が少ないように見えますが、空きメモリ量の実測値を平均すると、Laravelが879MB、Lumenが897MBで、ほとんど変わらないですね。誤差の範囲といってもよさそうです。フレームワーク自体の消費メモリに優劣ありませんでした。

サイズの比較

Laravelのサイズ

vagrant@homestead:~$ du -sk laravel/vendor/laravel/framework/
3416    laravel/vendor/laravel/framework/

Lumenのサイズ

vagrant@homestead:~$ du -sk lumen/vendor/lumen/vendor/illuminate/ lumen/vendor/laravel/lumen-framework/
2588    illuminate
292     laravel/lumen-framework/

フレームワークそのもののサイズはどうでしょう?composerで取得したコアライブラリの容量を比較してみたところ、Laravelが3416 KB、Lumenが292 KB2880KBで、LumenのサイズはLaravelの8.5%84.3%という結果になりました。かなり減量されていますね!自分も、せめて80%くらいにまでダイエットしたいところです…。軽量というには減量具合が中途半端ですね。自分の場合、それくらいダイエットできればBMI値が人並みになるんですが…。

Lumenのサイズに依存パッケージの分が含まれていないというご指摘をいただきました。illuminateパッケージのサイズを加味して計算して修正しました。

まとめ

以上の検証結果をまとめると、以下のような結論になりました。

f:id:o205:20150715113430p:plain

  • LumenはLaravelの約4倍速い
  • LumenはLaravelの約75%CPUに優しい
  • LumenはLaravelと同じくらいメモリを消費する
  • LumenはLaravelの約10%約85%の大きさ

Lumenは拡張性や機能に制限がありますが、その範囲で要求を満たせるものであれば、フルスタックのLaravelではなくLumenを使ってみるという選択肢もありかもしれませんね。

今回使用したソースコードは、https://github.com/medpeer-inc/benchmark-laravel-lumenに置いてありますので、ぜひ参考にしてみてください。


Zenhack(禅ハック)で最優秀賞いただきました&ハッカソンのススメ

$
0
0

こんにちは、メドピアデザイナーの松村です。
最近行き詰まることが多かったので座禅を組んで己に向き合いたい…と思ってZenhackに参加したら最優秀賞いただきましたやったー٩( ᐛ )و

Zenhackとは?

”禅 と IT で世界の課題に挑む”という壮大なコンセプトのもと由緒ある臨済宗建長寺派大本山建長寺に泊まり込みで行われるハッカソンです。すごい。 www.zenhack.jp

当日の様子と成果物イメージは以下からどうぞ。実況まとめありがたい! togetter.comhacklog.jp

私はハッカソンに何度か参加していますが、表彰されると承認欲求の満たされ方がすごいのでフラストレーションの溜まってるエンジニアさんやデザイナーさんにおすすめです。この快感を多くの方に味わって頂きたいので、僭越ながらまだハッカソンに参加されたことのない方に向けて、ハッカソンのススメ的なものを綴ってみます。

ハッカソンのススメ

ハッカソンて何ぞい?とよく聞かれますが、基本的には技術でお題に応える大喜利だと捉えています。ネタに走るもよし、ガチガチのビジネスプランを携えて挑むもよし。楽しみ方は人それぞれです。自分が一番楽しい!と思うことで勝負しましょう。(えらそう)

楽しむ

イデア出しの段階では俺はこれを作りたいんだーー!というのを思いつくまま紙にぶつけます。

こんなかんじ f:id:umeccco:20150708122557j:plain

お題に沿うのも大事ですが、内容を理解しないまま要素だけ貼り付けると薄っぺらくなるので基本的には自分の持ち味を活かすのが大事かなと思います。

尚このときに出すアイデアはチームビルディングのときに淘汰されることが多いので、一人でもやるよという気概のない方はあとでフラストレーションの溜まらないようにこの段階で持てるネタを出し切りましょう。

協力を求める

イデアを出し切ったら次は仲間探しです。

ボーっとしてるとどんどん人が売り切れいくので恥ずかしがらずにナンパしましょう。スーパーのタイムセールの状況が近いです。

この時大事なのがお互いのアイデアに共感できるか、それを一緒に作りたいか、というビジョンの共有です。これがないと実際手を動かしはじめたときにえらいことになります(過去にチームビルディングが上手く行かず途中でリタイアした悲しい思い出)。今回は本当に良いチームに恵まれました。ありがたい。

キョロキョロする

ハッカソンには普段は会えない人がひょっこり参加されてたりします。今回だと電波少年の土屋Pとかカヤックの岩渕さんとかぐるなびのフロントエンドエンジニアさんとかホームズの中の人とか錚々たる面々です。そんな方々と肩を並べて一夜を共に過ごせる…たまらないですね!

トラブルはつきもの

ハッカソンあるあるとしてWi-fi環境の不具合があります。状況を運営に相談したら慌てず騒がずSOYJOYでも食べて休憩しましょう。スケジュールどおり進まない?あるあるです。(今回はスケジュールどおり進んでてさすがカマコンバレーだなーと思いました)

また、出会ったばかりの人と2日間一緒に過ごすのですから、チームメンバーと険悪になる瞬間もあります。でも仕方ないじゃない。人間だもの。とりあえず目の前の自分の作業を終わらせて、それからお茶でも淹れて話し合いましょう。

プレゼンが上手くいかなかった?これは事前準備あるのみです。プレゼン環境を確認した上で3回は通しで練習すべきだなと痛感しました。(プレゼンほんとにやりなおしたい…orz)

讃える

人のアイデアや技術力に嫉妬することも多々ありますが素直にすごい!あんなの思いつかなかった!くやしい!と声に出して讃えることでより楽しくなります。あと人の能力を認めることで人間的に成長できる気がします。

というわけで少しはハッカソンの楽しさが伝わったでしょうか。 ハッカソンに参加する人はものを作りたくてウズウズしてる人たちばかりなので、今ここにない出会い的なものを求めている人は是非参加してみていただきたいです。

ハッカソン情報の収集は以下のサイトが便利です。

www.doorkeeper.jpconnpass.com

後日談

頂いた賞金はチームで山分けしたのち会社へのお土産に鳩サブレーの一番大きいサイズを買って帰りました。めちゃくちゃ重かったです。f:id:umeccco:20150708124608j:plain中に入っていた小分けの袋にも禅の思想を感じて、鎌倉はすごいところだな〜と思いました。 f:id:umeccco:20150708124723j:plain

SOYJOYもたくさん届いて社員一同ハッピーです!大塚製薬さんありがとうございます! f:id:umeccco:20150713151158j:plainf:id:umeccco:20150713151203j:plain

お知らせ

メドピアでは2015年11月4-5日にHealth2.0というイベントを開催します。 ハッカソンの開催はありませんが、医療に関わる様々な情報が集まる場ですので 興味のある方は是非お立ち寄りください!(事前申込制ですが、申込開始は今しばらくお待ちください!) health2.medpeer.co.jp

Vue.jsとRailsの最適な融合を考える

$
0
0

もう新年を迎えて2ヶ月が経ちますね。 多くの人は新年の目標を立てますが、皆さんは何かしら立てましたでしょうか? 英語を毎日勉強するという目標を立てましたが、既に挫折してしまったエンジニアの村上(pipopotamasu (pipopotamasu) · GitHub)です。

本日はその懺悔も込めてVue.jsとRailsの話をお送りします。

f:id:ec0156hx39:20180221120941p:plain

この記事を書く背景

以前ブログで書いた通り、現在Webpackerへの移行を機にフロントエンド周りの改善を進めています。 tech.medpeer.co.jpその中でVue.jsとRailsをいい感じに組み合わせるにはどうしたら良いかについて悩むことがあったので、本記事にて共有させていただきます。 悩んだ内容としては以下になります。
1. Ajax通信時にCSRFトークンをどう設定すればいいか?
2. 単一ファイルコンポーネントで書くHTMLをもっと効率よく書けないか?
3. 単一ファイルコンポーネントとフォームヘルパーの兼ね合い

実行環境

今回の記事ではサンプルコードを多く載せています。主なもののバージョンは以下になります。

  • Rails 5.1.4

  • Webpacker 3.2.1

  • Vue.js 2.5.13

  • axios 0.17.1

  • pug 2.0.0-rc.4

1.Ajax通信時にCSRFトークンをどう設定すればいいか?

RailsではAjax通信時、破壊的なHTTPメソッド(POSTとかDELETEとか)を送る場合はCSRFトークンの検証が必要となります。 jquery-railsのGemで上記のようなAjax通信を行う場合は、Gemの方でCSRFトークンをリクエストヘッダーに自動的に含めてくれるのですが、jQuery以外だとそうはいきません。 自身でCSRFトークンをリクエストヘッダーに含める必要があります。

ではどういった風にVue.jsでCSRFトークンをリクエストヘッダーに含めたら良いのでしょうか? 私自身どうしたらいい感じに実装できるか悩んだため、ここで悩んだ末にたどり着いた実装をご紹介します。

なお、今回のケースではHTTPクライアントとしてaxiosを使用します。

リクエストヘッダーにCSRFトークンを仕込んでみる

通常、Railsが提供しているフォームヘルパーを利用せずにAjax通信を行う場合は明示的にCSRFトークンを設定する必要があります。

設定しない場合

まずはAjaxを送る部分のソースを見てみます。
f:id:ec0156hx39:20180212221613p:plain:w220

上記のようにCSRFトークンを設定しない状態だと...

f:id:ec0156hx39:20180212222555p:plain

このようなエラーが起こるので、ちゃんと通るようにしてみましょう。

設定する場合

今度はCSRFトークンを設定する場合です。
f:id:ec0156hx39:20180212221532p:plain:w700

上記の10行目でCSRFトークンの取得、11行目でリクエストヘッダーにトークンを設定しています。

f:id:ec0156hx39:20180212222617p:plain

今回は上手く処理されました。

この方法はViewのDOM内のCSRFトークンを直接取得し、axiosに設定する方法です。

実はこれと同じ方法を提供してくれるパッケージがあります。 次はそのパッケージ、「rails-ujs」を用いた方法を見てみましょう。

rails-ujsを使ってみる

まずはrails-ujsをインストールします。

yarn add rails-ujs

次にrails-ujsからcsrfTokenを取得するメソッドをimportし、CSRFトークンを設定してみましょう。

f:id:ec0156hx39:20180214161913p:plain:w500

これで先ほどの例のように破壊的なリクエストを送ることができるようになります。

Vue.jsのPluginにしてみる

ここまででCSRFトークンを設定するところまでみてきました。 上記の例だと、1つのエントリーファイルに1つのコンポーネントしかありませんでしたが、実際には複数のコンポーネントを使用することが多いと思います。

その場合、上記の例だとaxiosで破壊的なHTTPメソッドを使用する全てのコンポーネントに...

  • axiosをimport

  • rails-ujsをimport

  • リクエストヘッダーにCSRFトークンを設定

しなければなりません。正直面倒です。

そこで、Vue.jsのプラグインを作成し上記の処理を一纏めにしてしまいましょう。

まずはVue.jsのPluginを実装します。Pluginを実装することにより、Vueオブジェクトからaxiosを呼び出せるようにしてみましょう。

  • app/javascript/plugins/vue-axios.js f:id:ec0156hx39:20180214165844p:plain

次はエントリーファイルにて上記で作成したPluginをVueに組み込みます。

  • app/javascript/packs/chapter1/index.js f:id:ec0156hx39:20180214175736p:plain

最後にコンポーネントでVue経由でaxiosを呼び出してみましょう。

  • app/javascript/components/chapter1/App.vue f:id:ec0156hx39:20180214175851p:plain:w250

これで各々のコンポーネントで設定しなくていいようになりました。


以上のようにCSRFトークンの設定からコンポーネント内でのaxiosの呼び出しについて、メドピア内でどうしているかをみていきました。 皆さんはVue.jsとRailsを組み合わせる時、どのようにしているでしょうか?

2.単一ファイルコンポーネントで書くHTMLをもっと効率よく書けないか?

RailsのViewは独自のテンプレートエンジンを用いることで、効率的にHTMLコーディングをすることが可能になります。 ほとんどの人はHamlやSlimといったテンプレートエンジンを使って効率化しているのではないでしょうか?

Vue.jsの単一ファイルコンポーネントのデフォルトのテンプレートは通常のHTMLです。 せっかくRailsはHamlやSlimを使用しているのに、単一ファイルコンポーネントはHTMLって非効率です。

JSのテンプレートエンジンといえば、Vue.jsに組み込まれているMustachやEJSが有名ですが、今回の目的はHTMLの効率的なコーディングです。どうやらHTMLの省略記法に対応しているのはそれほど多くなく、調べてみた結果「Pug」の一強のようです(もし他に有名なやつがあればこっそり教えてください)。

そこで、メドピアではPugを採用することにしました。

Pugとは

Hamlに影響を受けたテンプレートエンジンです。ただHamlに影響を受けたという割にSlimに近いシンタックスだったりします笑

元々Jadeという名前でしたが、すでにJadeが商標登録?されていたためPugにしたらしいです。 上記でも書いたように、
・HTMLの省略記法に対応
・JSの実行

という特徴があります。 それでは次にPugを使って単一ファイルコンポーネントを書くとどのようになるのかを見ていきましょう。

Pugを使用しない場合

と言いつつも、Pugを使用した場合と使用しない場合のbefore/afterが見れた方が良いので、まずはpugを使用しない場合の単一ファイルコンポーネントを見てみます。 今回の例ではみんな大好きTodoリストです。

  • app/javascript/components/chapter2/TodoApp.vue f:id:ec0156hx39:20180215114443p:plain

まあ普通のHTMLですね笑

次はPugを使用する例を見ていきます。

Pugを使用する場合

まずはPugのパッケージと、WebpackでPugをコンパイルするためのpug-loaderをインストールします。

yarn add pug pug-loader

後は単一ファイルコンポーネントのtemplateタグにlang="pug"とpugの設定をするだけです。 Webpackerの場合はコンフィグファイルに設定を追記する必要はありません。

どうなるかというと...

  • app/javascript/components/chapter2/PugTodoApp.vue f:id:ec0156hx39:20180215120942p:plain

このようにかなりスッキリさせることができました。

みなさんも是非pugを使って見てください!

3.単一ファイルコンポーネントとフォームヘルパーの兼ね合い

Vue.jsの単一ファイルコンポーネントはとても便利です。 以前技術ブログでも書きましたが、単一ファイルコンポーネントは以下のようにとても便利です。 * シンタックスハイライト * 1ファイル内にテンプレートとテンプレートに適用するJavaScript、cssを書くことができる(コンポーネント化できる) * テンプレート内でES6以降のものが使用できる

しかし残念なことに、単一ファイルコンポーネントではRailsのフォームヘルパーを使うことができません。 かといって本来フォームヘルパーで生成されるはずのHTMLを単一ファイルコンポーネント内のテンプレートに直書きするのも効率が悪いです。

比較

そこで、単一ファイルコンポーネントでテンプレートを書くかRailsのViewでテンプレートを書くか比較してみることにしました。 今度はユーザーの新規作成画面を例にしてみます。

単一ファイルコンポーネント内にテンプレートを書く場合
  • app/javascript/components/chapter3/UserRegisterForm.vue f:id:ec0156hx39:20180220122803p:plain

全体としてこのような感じになりました。 このテンプレートを作ってみて面倒だったのはformタグとhidden要素です。

f:id:ec0156hx39:20180220123017p:plain

フォームヘルパーならform_forやform_withで一行で作成できる部分をわざわざ手打ちしてHTMLを書かなければなりません。 またhiddenのinputタグのauthenticity_tokenのvalueですが、フォームヘルパーなら自動的にCSRFトークンが設定されるところ、わざわざ自身で設定しなければなりません。

f:id:ec0156hx39:20180220123451p:plain

結果、テンプレートを作成するのにそこそこ手間がかかってしまいました。

RailsのViewにテンプレートを書く場合

今度はフォームヘルパーを活用する場合です。全体としてはこんな感じになります。

  • app/views/chapter3/new.html.erb f:id:ec0156hx39:20180220123722p:plain

先ほどのformタグとhiddenの件はフォームヘルパーのおかげでかなり楽になりました。 他にもtext_fieldやlabelなどのヘルパーのおかげで、全体的にコード量が減っているのが見て取れます。

しかし、もちろんデメリットもあります。

  • Vue.js部分がシンタックスハイライトが効かないため若干見にくい

  • polyfillが効かない

  • テンプレートとコードが離れる

デメリットこそありますが、メドピア内ではフォームヘルパーを使う時は基本的に単一ファイルコンポーネントを使わず、RailsのViewにテンプレートを書く方法にしています。

まとめ

今回の記事ではメドピア内でRailsとVue.jsを組み合わせる時に悩んだ3つのことと、その対応策を書いてみました。

使用したサンプルコードは以下のリポジトリに置いてあります。

GitHub - medpeer-inc/medpeer-dev-blog2

正直これがベストなソリューションであるという確信はありません。

もし、「もっといい方法があるよ」というアイデアがある方は是非メドピアに遊びに来てそれをご教授ください。 「俺がもっといけてるコードにしてやるぜ!」という方は一緒に働きましょう!

またメドピアでは毎週火曜日にVue.jsのもくもく会を実施しています。そちらも是非ご参加ください!

medpeer.connpass.com


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


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

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

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

■開発環境はこちら

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

トピック型のモバイルPush通知をRails + Amazon SNSで実装する

$
0
0

こんにちは。メドピアにWebエンジニアで入社して約6ヶ月の佐藤です。

メドピアは2/26から銀座に移転しました。 銀座に移転しても花粉からは逃げられませんでしたが 、移転後はなぜか空気清浄機が増えて助かっています。

書いてある事

Amazon SNSの複数ユーザーに一度に通知を送る「トピック」を使ってRailsでPush通知を実装した際の処理フローを主に書いてあります。

f:id:motsat:20180313115508p:plain:w500

トピック機能を使わず、Amazon SNSのプッシュ通知のみを実装した場合はわりとシンプルですが、

  • トピック購読(subscribe)
  • トピック購読の解除(unsubscribe)

等のトピック関連の処理が加わってくると状態管理が複雑になってきます。

また、

  • トピック
  • エンドポイント
  • ARN(Amazon Resource Name)

などAmazon SNSやAWS上での用語も理解する必要があるため、その説明も簡単に入れました。

今後Amazon SNSを使ってpush通知(とトピック機能)を実装する上で何かしら参考になれば幸いです。

また、ブログ内のサンプルコードは下記のリポジトリに格納してあります。

サンプルコード

ブログ内は処理の抽象化された部分が書かれているためaws_sdkの利用部分やDBへの反映部分は出てきませんが、 リポジトリ内の他ファイルにはそこも含めたものが上げてあります。

(ただ、弊社事情部分を削ってあるのでそのままでは動かない参考的なものです)

Amazon SNSのモバイルプッシュ通知

いろいろなデバイス(プラットフォーム)に、送信側はそれほど処理を変えずに簡単にプッシュ通知できるサービスです。

他にもこんな特徴があります。

  • 送信可否状態をAmazon SNS側で管理

 プッシュ通知の送信が失敗すると、状態変更(後述)されるまで送信しない

  • 個別の送信先、またトピック機能を使う事で複数の送信先にも一度に送信できる

 詳細  https://docs.aws.amazon.com/ja_jp/sns/latest/dg/SNSMobilePush.html

Amazon SNSのトピック

トピックは、日本語訳で「話題」や「論題」です。

Amazon SNSのトピック機能は、事前にトピック(Topic)を作成しておき、 ユーザーはそのトピックを購読(subscribe)する事でそのトピックへの通知を受け取ることが出来るようになります。

複数ユーザー(アプリ)を紐付けでおけば、APIの利用側はそのトピックに対して送った通知メッセージが複数ユーザーに届くようになります。

大量ユーザーへのPush通知も、トピックに紐付ければ1回の送信で行えるという事です。

https://docs.aws.amazon.com/ja_jp/sns/latest/dg/CreateTopic.html

アプリ側の機能概要

今回実装したアプリの、ユーザーから見たトピックの選択イメージはこんな感じです。

  • 複数プラットフォーム(iOS+Android)でプッシュ通知を受け取れる
  • ログアウト時にはpush通知が来ないようになる
  • トピックごとに通知のON/OFFができる

アプリ側の設定画面の雰囲気です。

f:id:motsat:20180312205655p:plain

Amazon SNS側作業

AWS マネジメントコンソールで事前に下記の作業を行います。 (APIのユーザー管理やアクセス権限周りも必要あれば設定)

  • アプリケーション作成
  • トピック作成

これらはAPIで作成することもできますが、今回は事前に作成する形にしたのでAWS マネジメントコンソールでの作業を前提とします。

アプリケーション作成

プラットフォームと対になるものです。 iOS、Android分であれば計2つを作成します。

必要なもの

  • iOSのp12ファイルとそのパスワード (Androidは試していませんが、GCMアクセスキーが必要となるはずです)


下記はiOSのアプリケーション作成画面です。

f:id:motsat:20180311214507p:plain

  • アプリケーション名
  • iOSの場合はDevelopmentまたはProductionの選択
  • p12ファイルパスワードを入力

です。(証明書はp12ファイルから勝手に入力されます)

これで、アプリケーションの「ARN」である、「application_arn」が生成されます。(APIでの送信時に必要)

ARN(Amazon Resource Name)は、AWS上に何かリソースを作成した時に与えられる名前です。

この先に出てくる他のリソース(トピック、エンドポイント等)にもARNが設定され、 同じくAPIでそのリソースを指定する時などに使います。

トピック作成

トピックA、トピックBであれば計2つです。 iOS + Androidの2つのアプリケーションがある場合でも、通知単位を分けないのであればトピックは同じ物を利用します。 (逆に、iOSとAndroidで通知単位を分けたければ別に作ります)

f:id:motsat:20180311214515p:plain

これで、トピックの「ARN」である、「topic_arn」が生成されます。(APIでの送信時に必要)

Rails側のモデルのイメージ

今回の実装例に出てくるモデル(DB)については、下記のようなイメージです。

f:id:motsat:20180313113150p:plain

topic_1_…などtopicごとにカラムを持っていますが、topicごとにレコードを持つ形なども良いと思います。

また、実際にはプッシュ通知のメッセージを格納しているモデルなどもいろいろあるのですが、説明用に省きます(実際のものはリポジトリをご覧下さい)

user_push_notification_settings

アプリ(ユーザー)の通知許可フラグを持つテーブルです。 ユーザーに対して1レコード持つ形です。iOS側で反映したらAndroid側も同じ設定となります。

  • user_id

 ユーザーID

  • topic_1_enabled

 トピック1が有効かどうか

  • topic_2_enabled

 トピック2が有効かどうか

user_push_notification_tokens

アプリ(ユーザー)のデバイストークンと、それに関連付けられるARN(Amazon Resource Name)を保存するテーブル。 ユーザーに対し、アプリを利用するプラットフォーム分のレコードを持ちます。

下記の属性を持つイメージです。

  • user_id

 ユーザーID

  • mobile_platform

 プラットフォーム(ios or android)の指定

  • device_token

 アプリから取得したトークン

  • endpoint_arn

 Amazon SNSのendpoint作成時に取得するARN(Amazon Resource Name)

  • topic_1_subscription_arn

 Amazon SNSのトピック1をsubscribeした時に取得するARN(Amazon Resource Name)

アプリサーバー(Rails)の実装

Amazon SNSのモバイルPushとトピック通知を使うためにRails側の実装部分です。

  • アプリからトークンを取得
  • 取得したトークンをAmazon SNSのエンドポイントに紐付ける
  • そのエンドポイントをトピックに紐付ける
  • また、不要になったらエンドポイントやトピックとの紐付けを削除する

という処理です。 主に下記リクエスト時の処理が必要になります。

  • トークンの新規保存、更新時
  • トークンの破棄(ログアウト)時
  • トピックの購読ON/OFF切り替え時

トークン(token)の変更時

f:id:motsat:20180311213737p:plain

1. アプリからtokenを取得

まずはアプリからtokenを取得し、Railsサーバーにリクエストします。

以下、実コードはAmazon SNSへのリクエスト処理はsidekiqで非同期処理で行っていますが、説明上Railsサーバーとしています

endpoint未作成? => YES の時

2. Amazon SNSのcreate_endpointを実行

まずはAmazon SNS側にendpointを作成します。

作成時に、そのendpointを表す 「endpoint_arn」がレスポンスに含まれます。

endpoint_arnはpush通知やトピックの紐付け(subscribe)を実行する時に必要になるのでDB等に保存します。

3. Amazon SNSのsubscribeを実行

2.で取得したendpoint_arnに、トピックの紐付け(subscribe)を行います。

subscribeのレスポンスには、「subscription_arn」が含まれます。 これは、トピックへの送信時には必要ありませんがunsubscribe時に必要になるのでDB等に保存します。

また、一度に複数トピックをsubscribeするAPIは無いため、トピック分subscribeを行う必要があります。

endpoint未作成? => NO の時

2.Amazon SNSのset_endpoint_attributesを実行

すでに作成されたendpoint内の属性を更新する、Amazon SNS のset_endpoint_attributesを実行します。 下記のパラメータを更新します。

  • Token

 アプリから送信された最新のtokenに上書き保存します。

  • Enabled

 送信可否状態です。trueまたはfalseです。  endpoint作成時の初期値はtrueですが、アプリへのPush通知が失敗すると自動的にfalseに更新されます。

アプリがPush受信可能な状態だと信じてtrueを設定します(アプリから送信された最新のtokenなので)。

サンプルコードです。 (メソッド化されているので、Amazon SNS APIの実行や具体的な処理の詳細はリポジトリをご覧ください)

# トークン(token)の変更時defon_updated_token(user_push_notification_token)
    has_device_token = user_push_notification_token.device_token.present?
    has_endpoint_arn = user_push_notification_token.endpoint_arn.present?

    if has_endpoint_arn
      if has_device_token
        set_endpoint_attributes(user_push_notification_token) # b-2.Amazon SNSのset_endpoint_attributesを実行else
        delete_endpoint(user_push_notification_token)
        unsubscribe_all_topics([user_push_notification_token])
      endelsereturnunless has_device_token
      create_platform_endpoint(user_push_notification_token) # a-2. Amazon SNSのcreate_endpointを実行

      on_updated_setting(user_push_notification_token.user) # a-3. Amazon SNSのsubscribeを実行(後述のトピック通知設定の時と同じ処理)endendprivatedefset_endpoint_attributes(user_push_notification_token)
     requester = AwsSnsRequester.new
     requester.set_endpoint_attributes(user_push_notification_token.endpoint_arn,
                                       user_push_notification_token.device_token)
  enddefcreate_platform_endpoint(user_push_notification_token)
    requester = AwsSnsRequester.new
    response = requester.create_platform_endpoint(user_push_notification_token.user_id,
                                                  user_push_notification_token.device_token,
                                                  user_push_notification_token.mobile_platform.value)
    user_push_notification_token.endpoint_arn = response.endpoint_arn
    user_push_notification_token.save
  enddefon_updated_setting(user)
    user_push_notification_setting = user.user_push_notification_setting
    UserPushNotificationSetting::SUBSCRIBE_TOPICS.each do |topic|
      if user_push_notification_setting.enabled_by(topic)
        subscribe_topics(topic, user.user_push_notification_tokens)
      else
        unsubscribe_topics(topic, user.user_push_notification_tokens)
      endendend

トークンの破棄(ログアウト等)時

f:id:motsat:20180311214119p:plain

1.アプリからtoken削除リクエストを送信

アプリからのログアウト時など、Railsサーバーに削除リクエストを送信します。

2.Amazon SNSのdelete_endpointを実行

作成済みのAmazon SNS上のendpointを削除します。 DB上に保存されたendpoint_arnも同時に削除します。

3.Amazon SNSのunsubscribe を実行

delete_endpointでendpointを削除しても、自動でそれに関連付けされたsubscription_arnも削除されるわけではありません(自動でやってほしい…)

なので、subscribeをしていた場合には、unsubscribeをしておく必要があります。

ドキュメントにも注意書きがあります。

https://docs.aws.amazon.com/ja_jp/sns/latest/api/API_DeleteEndpoint.html

When you delete an endpoint that is also subscribed to a topic, then you must also unsubscribe the endpoint from the topic

DB上に保存されたsubscription_arnも同時に削除します。

実装を一部抜粋してコメントを追記したものです。(メソッド化されているので、Amazon SNS APIの実行や具体的な処理の詳細はリポジトリをご覧ください)

※ トークンの更新時とコードと同じです。delete_endpointが追加されています。

defon_updated_token(user_push_notification_token)
    has_device_token = user_push_notification_token.device_token.present?
    has_endpoint_arn = user_push_notification_token.endpoint_arn.present?

    if has_endpoint_arn
      if has_device_token
        refresh_attributes(user_push_notification_token) 
      else
        delete_endpoint(user_push_notification_token) # 2.Amazon SNSのdelete_endpointを実行
        unsubscribe_all_topics([user_push_notification_token]) # 3.Amazon SNSのunsubscribe を実行endelsereturnunless has_device_token
      create_platform_endpoint(user_push_notification_token)

      on_updated_setting(user_push_notification_token.user)
    endendprivatedefdelete_endpoint(user_push_notification_token)
    AwsSnsRequester.new.delete_endpoint(user_push_notification_token.endpoint_arn)
    user_push_notification_token.endpoint_arn = ""
    user_push_notification_token.save
  end

トピックの購読ON/OFF切り替え時

トピック購読の切り替えはかなり単純です。 購読状態を見て、subscribeまたはunsubscribeするだけです。

f:id:motsat:20180311214133p:plain

1.アプリからトピックの選択状態を送信

トピックごとにtrue/falseなど、subscribeまたはunsubscribeするための状態を送信します。

2.Amazon SNSのsubscribe/unsubscribeを実行

アプリから送信されたトピックの選択状態に合わせ、Amazon SNSのsubscribeまたはunsubscribeを実行します。

DB上に保存されたsubscription_arnにも同時に反映します。

サンプルコードです。 (メソッド化されているので、Amazon SNS APIの実行や具体的な処理の詳細はリポジトリをご覧ください)

※トークンの更新時に行うコードと同じものです。

defon_updated_setting(user)
    user_push_notification_setting = user.user_push_notification_setting
    UserPushNotificationSetting::SUBSCRIBE_TOPICS.each do |topic|
      # 2.Amazon SNSのsubscribe/unsubscribeを実行if user_push_notification_setting.enabled_by(topic)
        subscribe_topics(topic, user.user_push_notification_tokens)
      else
        unsubscribe_topics(topic, user.user_push_notification_tokens)
      endendend

その他

テスト(RSpecなど)の実装はClientStubsが便利

外部APIが絡むテストを実行する場合、スタブやモックなどが必要になる事も多いですが、aws_sdkには標準でスタブに関する機能があります。 これを使う事で、テストの実装もかなり楽になりました。 https://docs.aws.amazon.com/sdkforruby/api/Aws/ClientStubs.html

例えば、RSpecでのテスト実行時は全ての処理をスタブとするのであれば、spec/rails_helper.rb等に下記のように記述します。

# テスト実行時、aws_sdkは全体的にスタブにするifRails.env.test?
  Aws.config[:stub_responses] = trueend

aws_sdkでテスト時のレスポンスなどを任意のものにする

また、テスト時に場合によってはレスポンス内容を変えたくなったりする事もあると思います。 その場合は、下記のように一部のレスポンスを指定する事ができます。

aws_sdkのcreate_platform_endpointを実行した時にendpoint_arnが「new_endpoint_arn」としてレスポンスされるようにしたものです。

sns_response = Aws::SNS::Types::CreateEndpointResponse.new(endpoint_arn: "new_endpoint_arn")
Aws.config[:sns][:stub_responses][:create_platform_endpoint] = sns_response

まとめ

実装面やその他ふくめて、良かった所/つらかった所です。

良かった所

  • 大量配信の負荷をあまり気にしなくて良い

 トピック機能を使った場合は、大量配信時にもAPIを使う側は1件送信するだけ。

  • 事前準備が楽

 Amazon SNS側のアプリケーションの設定やトピックの設定も、シンプルで設定しやすく特に迷うこともありませんでした。

  • 料金安い

 Amazon SNS リクエストのうち、毎月最初の 100 万件は無料

 それ以降、Amazon SNS リクエスト 100 万件ごとに 0.50¬USD

 https://aws.amazon.com/jp/sns/pricing/

つらかった所

  • Amazon SNS用語やエンドポイントとトピックの関係などに慣れる必要がある

  • 大量配信先を事前に決められない時がつらい

  Amazon SNSのpush通知送信API(publish)は、1つのエンドポイントARN、もしくはトピックARNを対象するため、トピックに紐付かない複数のエンドポイントへの送信ができないようです。送信数分ループ処理をする事になりますが、送信数によってはサーバー負荷となってしまいそうです。

以上、トピック型のモバイルPush通知をRails + Amazon SNSで同じく実装した時に整理したことなどを纏めてみました。

これからPush通知の運用する中で、新たに問題点や改善点も出てきそうですが何かしらの形で共有できればと思います。


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


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

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

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

■開発環境はこちら

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

メドピアではIT勉強会での会場を提供いたします

$
0
0

こんにちは。メドピアCTOの福村です。
RubyKaigi2018@仙台が横浜開港祭と被っていて家族との調整で頭を抱えている今日このごろです。
開発合宿の計画も進めており今年も(すでに4月ですが)熱い1年になりそうです。
好きな季節は夏です。

さて、メドピアグループ(メドピア、フィッツプラス、Mediplat)は、2月26日から拠点を銀座に移し1フロアでグループシナジーを生みながら事業に邁進しております。 medpeer.co.jp

今回の移転でイベントスペースを作ったのでIT業界への貢献の思いも込め、 ITや医療系の勉強会に積極的にこのスペースを提供していこうという運びになりました。

イベントスペース

https://s3-ap-northeast-1.amazonaws.com/prod.cojp.wp.media.press/press/wp-content/uploads/2018/04/03152607/patio.jpg

アクセス

〒104-0061 東京都中央区銀座6-18-2 野村不動産銀座ビル11階
東銀座駅 徒歩3分

収容人数

50名

設備

  • プロジェクター
  • ゲストWi-Fi
  • 椅子・机

お問い合わせ先

info@medpeer.co.jp

  • イベントの概要
  • ご希望の日時(社員立ち会いが必要な都合上、平日のみとさせていただいております)
  • 参加予定人数

※ 営利を目的とした勉強会については、お断りをさせていただくことがございます

勉強会開催します!

JapanTaxiさんと4/25(水)にRubyをテーマに勉強会を実施します!
Ruby/Rails開発全般で面白いテーマが揃っていると思います。 是非是非新オフィスに遊びに来てください。

medpeer.connpass.com

会場を提供した勉強会

移転して1ヶ月くらいですがいろいろなIT系の勉強会を開催しております!

savanna.connpass.com

ginzarb.doorkeeper.jp

medpeer.connpass.com

medpeer.connpass.com

銀座付近で勉強会したいなぁと思った際はぜひともお気軽にお声がけください。


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


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

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

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

■開発環境はこちら

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

社屋を移転したのでオフィスの壁をデザインした話

$
0
0

f:id:umeccco:20180404142238j:plain

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

この春、メドピアはめでたくオフィスを銀座に移転しました!ワーワー! medpeer.co.jp

今回はオフィス移転をきっかけに会社の沿革をイラストに起こしたやり取りが「ザ・メドピアのものづくり」という感じだったので、デザイナー活動記としてブログにしたためてみました。

ラフを作ろう

広報の藤野女史から、WALLコンセプトの共有を受けラフ案を作成するながれに。 MedPeerの転換期には必ず医療に関する事件や法律改正が絡んでいるので、MedPeerのこれまでの歩みとの相関関係を見せたい、というのがコンセプトでした。

コンセプト時点では「MedPeerの歴史」がフィーチャーされていたのですが、「医師をサポートし、患者を救う」というMedPeerコンセプトから考えても、いっそMedPeerだけじゃなくてもっと広いレンジにしたほうがハマるのではないか、と考えて作ったのがこちらのラフ案。

f:id:umeccco:20180129144708j:plainf:id:umeccco:20180130123132j:plain
f:id:umeccco:20180130123104j:plainf:id:umeccco:20180130123115j:plain

いかがですかこのやる気を引き出してくれる誉め殺しコミュニケーション。

いいもの作って期待に答えるしかない!!て感じですよね。

〜ちなみにメドベアとは、MedPeer内で連載中の4コマ漫画です。〜

医師の声を取り入れよう

ラフ案のプレビューで、代表医師の石見先生に「上医は国を医し、中医は人を医し、下医は病を医す」という言葉に影響を受けたのでそれを入れたい、というフィードバックを受けました。

こうなってくるとせっかくなので医師会員の皆様の声も入れたいなあ……ということになり、 石見代表にMedPeer内の 「FORUM Q&A Life」で募集してもらいました。

f:id:umeccco:20180129144542j:plain

ワクワクしながら投稿を待ちわびる我々。

f:id:umeccco:20180308132217j:plain

おかげさまで、最終的には70以上の案をお出し頂きました。 これには石見代表も感激。

f:id:umeccco:20180308132156j:plain

会員医師の皆様、本当にご協力ありがとうございました。

頂いた名言はほぼ全て今回のデザインに取り入れております。

機会がありましたら、是非MedPeer新オフィスに足をお運びいただければ幸いです。

いよいよ完成!

よーしいよいよ入稿だ〜!と思ったら大きな罠が。 f:id:umeccco:20180130125550p:plain

f:id:umeccco:20180129144427j:plain

アイレベル設定を完全に失念していました。

こんな時にもワハハノリで流してくれるので変にストレスを溜めずに修正することができます。

f:id:umeccco:20180130121903p:plain

というわけで最終的に調整して完成!

f:id:umeccco:20180404142415j:plain

対面には石見セレクションの医学書が。

f:id:umeccco:20180404142335j:plain

こんな感じで弊社では、部署や役職の垣根なく意見を交換してものづくりを行なっています。 (今回のように社内外の医師にご意見を頂き、一緒に作り上げることもしばしばあります。)

MedPeerのデザイン

今回は弊社の思想を表現するため、よりコンセプチュアルなデザインとなりましたが、 webサイトやイベントのデザインも「医師の体温」が伝わるデザインに、と調整を行なっています。

MedPeerのサイトコンセプトはズバリ「臨床の役に立つ」サイト。 今年からはそれに加えて、臨床知識を得られながらも、患者さんに向き合うための英気を養える、医師にとって居心地の良い場の再構築を目指しております。

「自分ならこういうことで実現するかなあ」と思った皆さま。

是非一度、お話をお聞かせください。

MedPeerでは共に働く仲間を募集しています。

おまけ

今回は自動販売機もラッピングできるということで、欲望溢れるデザインにしてみました。(ヘルステックカンパニーの意識とは…)

f:id:umeccco:20180404143127j:plain

MedPeerの会議室 Patioはイベントスペースとしても貸し出しております。 勉強会などの会場でお困りの方はお気軽にお声かけください。

f:id:umeccco:20180404143225j:plain

スカイツリーも見えますよ〜

f:id:umeccco:20180404143149j:plain
Viewing all 215 articles
Browse latest View live