Quantcast
Channel: hachi8833の記事一覧|TechRacho by BPS株式会社
Viewing all 1838 articles
Browse latest View live

Ruby: mallocでマルチスレッドプログラムのメモリが倍増する理由(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

「スレッド単位メモリアリーナ」(per-thread memory arena)は定訳が見当たらないため仮訳を当てています。
楽しい画像はすべて英語記事からの引用です。

Ruby: mallocでマルチスレッドプログラムのメモリが倍増する理由(翻訳)

概要: メモリ断片化は測定や診断が困難ですが、驚くほど簡単に修正できることもあります。マルチスレッドのCRubyプログラム(mallocのスレッド単位メモリアリーナ)におけるメモリ断片化の原因を追ってみましょう。本記事のボリュームは3343語、20分程度です。


シンプルなときはマジこのぐらいシンプル

単純な設定変更だけで問題を完全に解決できることはめったにありません。

私の顧客で、Sidekiqプロセスが大量のメモリを消費していたことがありました(1プロセスあたり1 GB程度)。開始当初の各プロセスは300MB程度でしたが、時間の経過とともにじわじわと肥大化してほぼギガバイトレベルにまで達したところで落ち着き始めました。

私は顧客にMALLOC_ARENA_MAXというたった1つの環境変数の変更を依頼しました。「2に設定してください」と。

プロセス再起動後、「じわじわ肥大化」現象はピタリと止みました。プロセスのメモリ使用量は以前の半分程度(512 MBぐらい)に落ち着いたのです。

「うっそぴょーん」
実際はそんな単純な話ではありません。
フリーランチは存在しませんが、ほぼ無料(10円ランチ程度)だったと言えなくもないぐらいでした。

ここまで読んでこの「魔法の」環境変数をアプリ環境にコピペして回る前に、「この方法にはいくつもの欠点がある」ということを知っておいてください。この方法で解決される問題のことで今のあなたが困っているとも限りません。銀の弾丸はないのです。

Rubyがメモリ使用量の少ない言語であることは、意外に知られていません。しかしRailsアプリの多くは1プロセスあたりのメモリ使用量が1 GBに達する問題に悩まされています。これはJavaレベルに匹敵しつつあります。Rubyのバックグラウンドジョブプロセッサとして有名なSidekiqのプロセスも、同程度かそれより巨大になることがあります。理由はいろいろありますが、特に、断片化の診断とデバッグが極端に難しいこともそのひとつです。

Rubyメモリは対数的に肥大化するのが典型的

この問題は、速度の低下や、Rubyプロセスの不気味なメモリ肥大化という形で顕在化します。そしてよくメモリリークと間違えられます。しかしメモリリークは直線的に増加しますが、断片化によるメモリの肥大化は対数的に増加します。

普通、Rubyプログラムのメモリリークの原因はC拡張のバグによるものです。たとえば、markdownパーサーで呼び出しのたびに10 KBずつリークすると、markdownパーサーの呼び出し頻度は一定になる傾向があるので、メモリ使用量は直線的に青天井で増加し続けます。

メモリ断片化は対数的なメモリ肥大化の原因になります。肥大化は長い曲線を描いて、見えないどこかの上限値に向かいます。メモリ断片化はあらゆるRubyプロセスである程度発生します。これはRubyのメモリ管理方法による、避けがたい結果です。

特に、Rubyではオブジェクトをメモリ内で移動できません。もし移動したら、Rubyオブジェクトへのポインタを持つC言語拡張が残らず破損するかもしれません。オブジェクトをメモリ内で移動できないのだとしたら、断片化は避けられません。これはRubyに限らず、多くのCプログラムで共通の問題です。

顧客の実際のグラフより: 断片化はこのような経過をたどります。
`MALLOC_ARENA_MAX`を`2`にすると激減していることにご注目ください。

しかしRubyプログラムのメモリ使用量は、断片化によって通常の「倍」に、場合によっては4倍以上に達することすらあるのです!

Rubyプログラマーはメモリを意識することに慣れていません。特にmallocレベルではなかなか意識できません。Ruby言語全体が、抽象的なメモリをプログラマーが意識しないで済むように設計されているのですから、もちろんそれ自体は別に問題ありません。しかしRubyはメモリ安全性を保証できるにもかかわらず、完全なメモリ抽象化を提供できていません。メモリを完全に無視するわけにはいかないのです。というのも、Rubyプログラマーはコンピュータ上のメモリの動作についてそれほど経験を積んでいないので、いざ問題が発生すると、どこからデバッグを始めたらよいのか皆目見当がつかなくなることも多く、Rubyのような動的インタプリタ言語の本質的な機能だから仕方がないと諦めることもあります。

「お姫様が4つの敷布団(メモリ抽象化)をめくると、何とその下が断片化し始めているではありませんか」

さらに厄介なのは、メモリが4つの異なる層によって抽象化され、Rubyistから見えなくなっていることです。第1層はRubyの仮想マシン(VM)そのものです。VMは独自の内部構造やメモリトラッキング機能を備えています(これはObjectSpaceと呼ばれることもあります)。第2層はアロケータです。この振る舞いは、用いる特定の実装によって大きく異なります。第3層はOSです。ここでは実際の物理メモリアドレスを仮想メモリアドレスに抽象化します。この抽象化はカーネルごとに大きく異なっています。たとえばMachの抽象化はLinuxとかなり異なります。最後の第4層は実際のハードウェアそのものです。ここでは、アクセスの多いデータが、頻繁にアクセスしやすい「ホット」な場所からなるべく移動しないようにするためにさまざまな戦略を用います。ときには、translation lookaside buffer(TLB)のような特殊なCPU部品まで関わっていることがあります。

これらによって、Rubyistによるメモリ断片化問題の対処が非常に困難になっています。この問題は、仮想マシンやアロケータのレベルで一般的に発生しますが、ここはRubyistの95%にとって馴染みのない部分でもあります。

断片化には避けようのないものもありますが、Rubyプロセスのメモリ使用量が倍増するほど悪化する原因でもあります。今起きているのが避けられない断片化ではなく、メモリ使用量が倍増する断片化かどうかをどうやって知ればよいでしょうか。なお私は、PumaやPassenger Enterprise上で動作するWebアプリや、SidekiqやSucker PunchなどのマルチスレッドジョブプロセッサのマルチスレッドRubyアプリに影響するメモリ断片化の原因について論文を1本書いたことがあります。

glibc malloc内のスレッド単位メモリアリーナ

この問題は、いずれも標準glibcの「スレッド単位メモリアリーナ(per-thread memory arena)」と呼ばれるmalloc実装の特定の機能に集約されます。

理由を理解いただくためには、CRubyのガベージコレクション(GC)が驚くほど高速に実行される様子について説明する必要があります。

Aaron PattersonによるObjectSpaceのビジュアル表示。
ピクセル1個が1つのRVALUEを表す。新しいのが緑、古いのが赤。
詳しくはheapfragを参照。

すべてのオブジェクトは、ObjectSpace内にエントリを1つ持ちます。ObjectSpaceは、プロセス内で動作しているあらゆるRubyオブジェクトのエントリを保持する巨大なリストです。リストの項目はRVALUEの形を取ります。RVALUEはオブジェクトの基本データの一部を保持する40バイトのC言語struct(構造体)です。構造体の正確な中身は、どのクラスのオブジェクトかによって変わります。たとえば、”hello”のような非常に短いStringの場合、文字データを実際に含むビットはRVALUE内に直接埋め込まれます。しかしRVALUEは40バイトしかないので、文字列が23バイト以上になると、RVALUEは、そのオブジェクトデータが実際に配置されるメモリの場所(つまりRVALUEの外)を参照する生のポインタだけを保持します。

複数のRVALUEObjectSpace内で編成されて16 KBの「ページ」になります。1つのページには約408個のRVALUEが含まれます。

この個数は、どのRubyプロセスについてもGC::INTERNAL_CONSTANTS定数で確認できます。

GC::INTERNAL_CONSTANTS
=> {
:RVALUE_SIZE=>40,
:HEAP_PAGE_OBJ_LIMIT=>408,
# ...
}

長い文字列を作成すると(たとえば1000文字のHTTPレスポンス)、次のようになります

  1. ObjectSpaceリストにRVALUEを1つ追加します。ObjectSpaceの空きスロットが不足すると、malloc(16384)を呼び出してリストをヒープページ1つ分増やします。
  2. malloc(1000)を呼び出して、メモリ上1の1000バイトを指すアドレスを1つ受け取ります。

このmalloc呼び出しにご注目ください。私たちが行おうとしているのは、特定サイズのメモリ領域を(場所は問わずに)求めているだけです。実際には、mallocの隣接については定義されていないのです。つまり、メモリ上で実際に配置される場所については何の保証もないということになります。ということは、断片化(根本的にはメモリの場所の問題です)とは、Ruby VMから見ればアロケータの問題だということです2

Ruby自体のObjectSpaceの断片化はある意味で測定可能です。GCモジュールのメソッドGC.statは、現在のメモリやGCのステートに関する情報を豊富に提供してくれます。情報がやや多い上にドキュメントも不十分ですが、次のようなハッシュを出力します。

GC.stat
=> {
:count=>12,
:heap_allocated_pages=>91,
:heap_sorted_length=>91,
# ... 他にも多数のキー ...
}

このハッシュの中でご注目いただきたいのは、GC.stat[:heap_live_slots]GC.stat[:heap_eden_pages]の2つのキーです。

:heap_live_slotsは、生きている(=解放とマーキングされていない)RVALUE構造体が現在専有しているObjectSpace内のスロット数を表します。これはざっくり「現在生きているRubyオブジェクト」の数とみなせます。

edenのヒープ

:heap_eden_pagesは、生きているスロットを1つ以上含んでいるObjectSpaceページの個数を表します。生きているスロットを1つ以上含むObjectSpaceページは、「edenページ」と呼ばれます。生きているスロットを1つも含まないObjectSpaceページは「tombページ」と呼ばれます。tombページはOSに返却される可能性があるので、両者の違いはGC側にとって重要です。また、GCが新しいオブジェクトを最初に置くのはedenページであり、edenページに空きがない場合に初めてtombページに置きます。これによって断片化を軽減しています。

生きているスロット数を全edenページ内のスロット数で割ると、ObjectSpaceで現在発生している断片化の度合いを求められます。私が新しいirbプロセスで取得した例をご覧ください。

5.times { GC.start }
GC.stat[:heap_live_slots] # 24508
GC.stat[:heap_eden_pages] # 83
GC::INTERNAL_CONSTANTS[:HEAP_PAGE_OBJ_LIMIT] # 408

# live_slots / (eden_pages * slots_per_page)
# 24508 / (83 * 408) = 72.3%

私のedenページスロットのうち28%は現在空いています。空きスロットの割合が多いということは、ObjectSpaceのRVALUEが本来よりずっと多くのヒープページにばらまかれているということになります(もし見ることができればですが)。これは内部メモリの断片化の一種です。

Ruby VMの内部断片化を知るもうひとつの測定値はGC.stat[:heap_sorted_length]です。このキーは1つのヒープの「長さ」を表します。ObjectSpaceが3つあり、私が真ん中の2番目をfreeしたとすると、残るヒープページは2つだけになります。しかしヒープページはメモリ内を移動できないので、そのヒープの「長さ」(本質的にはそのヒープページのインデックスの最大値)は3のまま変わりません。

断片化してるけどとってもおいしそうなヒープ

GC.stat[:heap_eden_pages]GC.stat[:heap_sorted_length]で割ると、ObjectSpaceページレベルの内部断片化の測定値を得られます。この割合が低いと、ObjectSpaceリストのあちこちにヒープページ大の穴が開いていることになります。

これらの測定値も興味深いのですが、ほとんどのメモリ断片化(およびアロケーション)はObjectSpaceの中では起きていないのです。断片化が発生するのは、1個のRVALUEに収まりきれないオブジェクトに空き(メモリ)を割り当てるときです。Aaron PattersonとSam Saffronによる実験結果によると、ほとんどがこれに該当することが判明しました。典型的なRailsアプリのメモリ使用量の50%〜80%は、たかが数バイトより大きなオブジェクトに空きメモリを割り当てるmalloc呼び出しによって占められています。

Aaronの言う「managed by the GC」は、実際には「ObjectSpaceリストの内部で」のことです。

それでは「スレッド単位メモリアリーナ」の話題に移りましょう。

スレッド単位メモリアリーナはglibc 2.10(現在はarena.cにある)で導入された最適化であり、メモリアクセス時のスレッド間競合を軽減するよう設計されました。

アロケータの素朴な基本設計では、メインアリーナの1つのメモリチャンクを要求できるのは一度に1つのスレッドに限定されます。これによってメモリの同じチャンクを誤って2つのスレッドが取得することがないようにできます。そのような状況が発生すると、かなりやな感じのマルチスレッドバグの原因になります。しかしスレッドを多数持つプログラムではロックの奪い合いが多数発生して速度が低下することがあります。すべてのスレッドからのすべてのメモリアクセスはこのロックを必ず経由するので、ここがボトルネックになることがおわかりいただけるかと思います。

パフォーマンスへの影響が甚大なため、アロケータの設計ではこのロック機構を撤廃するために多くの努力が注ぎ込まれました。ロックを行わないアロケータすらいくつか開発されたほどです。

スレッド単位メモリアリーナの実装は、次の処理におけるロックの奪い合いを軽減します(Siddhesh Poyarekarの記事を言い換えたものです)。

  1. あるスレッドでmallocを呼び出します。このスレッドは、前回アクセスしたメモリアリーナ(他のアリーナが作成されていない場合はメインのアリーナ)のロック取得を試みます。
  2. そのアリーナを利用できない場合は、次のメモリアリーナのロック取得を試みます(メモリアリーナが他にもある場合)
  3. 利用できるアリーナがない場合は、アリーナを1つ作成してそれを使う。この新しいアリーナは、連結リストの最後のアリーナにリンクされる。

メインアリーナは基本的にこのような形でアリーナやヒープの連結リストで拡張されます。アリーナの個数はmallopt、特にM_ARENA_MAX(ドキュメントはこちらの「environment variables」セクション)で制限されます。デフォルトでは、作成可能なスレッド単位メモリアリーナの個数の上限は、利用可能なコア数の8倍までです。Ruby Webアプリの多くはコアあたりおよそ5スレッドを実行し、Sidekiqクラスタの場合はこれよりずっと多くのスレッドを実行します。Rubyアプリが作成する可能性のあるスレッド単位メモリアリーナの個数は、実際にはこれよりはるかに多くなるということを示します。

これがマルチスレッドRubyアプリにおいて正確にはどのような役割を果たすのかを見てみましょう。

  1. Sidekiqプロセスをデフォルトの25スレッドで実行しています。
  2. Sidekiqが新しい5つのジョブの実行を開始します。このジョブは、外部のクレジットカード処理システムとやりとりするためのものであり、HTTPSでPOSTリクエストを1つ送信し、3秒以内にレスポンスを1つ受け取ります。
  3. 各ジョブ(Rubyland内で個別のスレッドを実行しています)がHTTPリクエストを送信し、IOモジュールを用いてレスポンスを待ちます。一般に、CRubyにおけるほぼすべてのIOはGVM(global VM lock)を解放します。つまり、これらのスレッドは並列(parallel)に動作しており、メインのメモリアリーナロックを奪い合う間柄なので、新しいメモリアリーナがいくつも作成されます。

CRubyでいくつものスレッドが実行されていて、かつI/Oを行っていない場合、2つのRubyスレッドが同時にRubyコードを実行しようとしてもGVMによって阻止されるため、メインのメモリアリーナを奪い合うことは不可能に近くなってしまいます。従って、スレッド単位メモリアリーナは、マルチスレッドかつI/Oを実行するCRubyアプリにしか効きません。

これがどのようにしてメモリ断片化につながるのでしょうか。

ビンパッキングもきっと楽しい

メモリ断片化は、本質的には(数学の)ビンパッキング問題(bin packing problem)です。変な形のピースをいくつもの容器(bin)に振り分けて、隙間を最小にするにはどうしたらよいでしょうか。次の理由から、ビンパッキング問題によってアロケータの難易度が非常に高まります。a)Rubyではメモリ上の場所を決して移動できない(場所をいったん割り当てると、オブジェクトやデータは解放されるまでそこにとどまる)。b)スレッド単位メモリアリーナは、本質的には容器を大量に作成しますが、これらを結合したりまとめたりすることができません。ビンパッキング問題は既にNP困難であり、この制約があるために最適解を得るのがさらに難しくなってしまいます。

時間とともにRSS(Resident Set Size)の値を巨大にするスレッド単位メモリアリーナは、glibc malloc trackerの既知の問題の一部です。実際、MallocInternals wikiには以下の指摘があります。

スレッド衝突の圧力が増加するとともに、圧力を逃がすためにmmap経由で追加アリーナが作成される。このアリーナの最大個数はシステムのCPU個数の8倍までに制限されている(ただし特に指定のない場合: 詳しくはmalloptを参照)ため、多数のスレッドがあるアプリでは引き続きある程度の競合が発生するが、トレードオフとして断片化は減少する。

ここがポイントです。利用可能なメモリアリーナの個数を減らすと、断片化を軽減できるのです。この方法には明確なトレードオフが存在します。アリーナを減らせばメモリ使用量を減らせますが、その代わりロックの競合が増加してプログラムの実行速度が落ちる可能性があります。

HerokuはCedar-14スタックを作成したときに、スレッド単位メモリアリーナにおけるこの副作用を発見しました。Cedar-14スタックでは、glibcのバージョンが2.19にアップグレードされています。

Herokuの顧客から報告された、アプリを新しいスタックにアップグレードするとアプリのメモリ使用量が増加する問題があります。これについてHerokuのTerrence Honeがテストを行い、いくつか興味深い結果を得ています。

設定 メモリ使用量
Base (unlimited arenas) 1.73倍
Base (before arenas introduced) 1倍
MALLOC_ARENA_MAX=1 0.86
MALLOC_ARENA_MAX=2 0.87

基本的に、メモリアリーナのデフォルトの振る舞いはlibc 2.19で実行時間が10%削減されましたが、メモリ使用量は75%も増加したのです!メモリアリーナの最大数を2まで減らすと、スピードアップは得られない代わりに、旧Ceder-10スタックと比較して10%(デフォルトのメモリアリーナの振る舞いと比較すればほぼ2倍!)もメモリ使用量が削減されたのです。

設定 レスポンス時間
Base (unlimited arenas) 0.9倍
Base (before arenas introduced) 1倍
MALLOC_ARENA_MAX=1 1.15倍
MALLOC_ARENA_MAX=2 1.03倍

ほとんどすべてのRubyアプリにとって、メモリ使用量を75%増やすのと引き換えに10%スピードアップするというのは、割に合わないトレードオフです。しかし、次にもう少し現実的な結果をご覧いただくことにします。

プログラムのレプリケーション

私はデモ用のアプリを作成しました。これはランダムなデータを生成してそのレスポンスをデータベースに書き込むSidekiqジョブです。

MALLOC_ARENA_MAXを2に変更すると、24時間後のメモリ使用量は15%低下しました。

ここで私は、この効果が現実の負荷によって大きく拡大されることに気づきました。つまり、この断片化の原因となるアロケーションパターンについて私もまだわかっていない点があるということです。拙著『Complete Guide to Rails Performance』のSlackチャンネルで、本番環境でMALLOC_ARENA_MAX=2を設定するとメモリ使用量を2〜3倍も節約できることを示すメモリのグラフをたくさん目にしました。

問題を修正する

この問題の主な解決方法は2つあります。3つ目は将来可能になるかもしれない解決方法です。

修正1: メモリアリーナを削減する

比較的わかりやすい方法として、利用可能なメモリアリーナの最大個数を減らす方法が考えられます。これはMALLOC_ARENA_MAX環境変数で変更できます。前述のとおり、この変更を行うとアロケータのロック競合が増加し、アプリ全体のパフォーマンスに悪影響を及ぼします

本記事で一般的な設定を推奨するのは無理です。しかしほとんどのRubyアプリではアリーナ数2〜4が適切であるように思えます。MALLOC_ARENA_MAXを1にすると、パフォーマンスに深刻な悪影響が生じ、しかもメモリ使用量はほとんど改善されません(1%〜2%止まり)。(この方法を使うのであれば)これらの設定を実験し、自分のアプリに適したトレードオフの落とし所が見つかるまで、メモリ使用量低下とパフォーマンス低下の両方について結果を測定してください。

修正2: jemallocを使う

もうひとつの解決方法として、単にアロケータを変更する手も考えられます。jemallocもスレッド単位アリーナを実装していますが、mallocで起きる断片化を回避する設計になっているようです。

上のツィートは、私がCodeTriageのバックグラウンドジョブプロセスからjemallocを削除したときのグラフです。ご覧のとおり、かなり劇的な効果を示しています。mallocMALLOC_ARENA_MAX=2を指定した場合についても実験してみましたが、こちらのメモリ使用量はjemallocのほぼ4倍にとどまりました。Rubyでjemallocに変更可能なら、ぜひ変更しましょうmallocよりずっと少ないメモリで、mallocと同等かそれ以上のパフォーマンスを得られるようです。

本記事はjemallocの記事ではありませんが、jemallocをRubyで使うときに押さえておきたい点を以下に示します。

修正3: GCのcompaction

一般的には、メモリ上の場所を移動できれば断片化を削減できます。CRubyでは、C拡張からRubyのメモリをじかに参照する生ポインタが使われる可能性があるため、この手が使えません。メモリ上の場所を移動すれば、segfaultするか読み出すデータがおかしくなる可能性があります。

Aaron Pattersonはここ最近、GCをcompactionすべくがんばっています(以下の動画)。期待が持てますが、おそらく登場は先の話になるでしょう。

まとめ

マルチスレッドRubyプログラムでは、mallocの「スレッド単位メモリアリーナ」で引き起こされる断片化が原因で、実際に必要なメモリ量の2〜4倍ものメモリを消費することがあります。これを修正するには、MALLOC_ARENA_MAX環境変数を設定して最大アリーナ数を減らすか、アロケータをjemallocなどに切り替えることでパフォーマンスを改善します。

これによって多くのメモリを節約でき、ペナルティも大きくありませんので、本番でRubyとPumaまたはSidekiqを使うときは常にjemallocを使うことをおすすめしたいと思います

効果が最も著しいのはCRubyですが、JVMとJRubyでは問題が起きる可能性もあります

関連記事

Rails: Puma/Unicorn/Passengerの効率を最大化する設定(翻訳)

Rubyのメモリ割り当て方法とcopy-on-writeの限界(翻訳)

Rubyのヒープをビジュアル表示する(翻訳)

RailsConf 2017のパフォーマンス関連の話題(1)BootsnapやPumaなど(翻訳)


  1. 実際には、Rubyがリクエストする領域は、文字列の追加やリサイズに備えて少し余分に取ってあります。私たちがHTTPレスポンスを受け取るのはこの場所です。 
  2. ただし、アロケーションのパターンやサイズをアロケータ側で扱おうとすればきっと厄介なことになるでしょう。 

年末特集: TechRachoの2017年度人気記事リスト

$
0
0

こんにちは、hachi8833です。
TechRachoをお読みいただきありがとうございます。

2017年の年末特集として、2017年に公開した記事の中から人気の高かったものをリストアップしました。

2017年度の人気記事一覧(総合)

1位

Linux CUI初心者に早く知っておいて欲しいコマンド操作

BPS Webチームリーダーmorimorihogeさんが多忙な業務の合間を縫って書いた記事です。ここぞというときの爆発力はピカイチです。私も来年は超えるぞっと。

2位

米国から見た日本のRuby事情(翻訳)

RubyKaigi 2017の直前というタイミングも相まって2位にランクインしました。

記事中の「Rubyコアコントリビューターの多くが10分から15分もあればお互いに行き来できるほどの近所に固まって住んでおり」の場所について、この間初めて参加したRails勉強会@東京 第93回でこっそり尋ねてみたところ、私の予想とどんぴしゃりでした。

3位

3年以上かけて培ったRails開発のコツ集大成(翻訳)

特に「2. コントローラにload_resourceを書く」は社内外でも議論になりました。

4位

週刊Railsウォッチ(20170707)Railsの新機能ActiveStorage、高速Rubyフォーマッタrufo gemが超便利、Railscasts全コンテンツが無料公開ほか

ActiveStorageを報じた回がRailsウォッチのトップになりました。

5位

[インタビュー] Aaron Patterson(前編): GitHubとRails、日本語学習、バーベキュー(翻訳)

Aaron Patterson氏インタビュー前後編は、技術的なやりとりのレベルも高く読み応えのある内容でした。RubyKaigi 2017でご本人に直接お礼を申し上げたのもなつかしい思い出です。「ここは日本語で話しかけるべき」と逆の意味で緊張しました。

2017年度の人気記事一覧(はてなブックマーク)

総合にない記事から選びました。

1位

PostgreSQLの機能と便利技トップ10(2016年版)(翻訳)

PostgreSQL記事も多くの方にお読みいただきましたが、その中でも最多でした。今年はBPS社内も新規案件はたいていPostgreSQLでした。Railsウォッチで追いかけてきたRailsの改修もPostgreSQL寄りのものが多く、バージョン10のリリースも相まってPostgreSQL機運が高まっているのを感じました。

2位

Rails開発者のためのPostgreSQLの便利技(翻訳)

こちらもPostgreSQL記事です。ウォッチつっつき会でもPostgreSQLのexplainの使いやすさ・見やすさが何度となく話題になりました。

3位

RailsのCSRF保護を詳しく調べてみた(翻訳)

CSRFの設定そのものは簡単で、Railsガイドでもある程度解説されていますが、詳しいしくみについての関心の高さが伺えました。

2017年度の人気記事一覧(Twitter)

上に含まれない記事から選びました。Twitterの傾向ははてブとはまた違っています。

1位

Ruby: 「マジック」と呼ぶのをやめよう(翻訳)

オピニオン記事だけに反応は大きいものでした。
特に根拠はありませんが、おそらく聖書の影響で、英語圏での「magic」という言葉の奥底にはどこか禍々しいイメージが付きまとっている気がします。日本語だと「コックリさん」とか「犬神憑き」とか「恐山」のような。

2位

Goby: Rubyライクな言語(1)Gobyを動かしてみる

Matzにツイートいただいたおかげもあって、Rubyライクな言語であるGobyちゃんの記事がTwitterで予想外に伸びました。

今年は他にもMatzからTechRacho記事をツイートいただきました。ありがとうございます!

3位

TestProf: Ruby/Railsの遅いテストを診断するgem(翻訳)

CIで待たされてばかりだと開発のリズムが乱れてしまうので、テストの高速化は常に課題です。関心の高さが伺えました。

2017年度の人気記事一覧(Facebook)

上に含まれない記事から選びました。

1位

【漫画翻訳実績】吉田貴司さま「やれたかも委員会」を日本語から英語に翻訳しました(テキスト翻訳のみ)

Facebookの1位はげんきだまCEOのgenkiさんの記事でした。
Facebookの傾向ははてブともTwitterとも大きく違っているのが面白い点です。

2位

[Rails 5] rails newで常に使いたい厳選・定番gemリスト(2017年版)

「これだけは入れておきたいgem」は自分も欲しかったので書いた記事です。

3位

クリスマスだしcanvasで雪を降らせるJS書いてみた

BPSアドベントカレンダー2017でクリスマスイブの夕方に公開した記事です。多忙な中で素敵な記事を書いてくださったスギヤマさん、ありがとうございました!

2017年の終わりに

2016年8月より平日欠かさず記事を公開し続けてはや1年半近くが経ち、徐々にペースが掴めてきたように思います。
元々は翻訳する記事を見繕うつもりで始めた週刊Railsウォッチでしたが、ほどなく毎週公開前にBPS社内メンバーで記事をつっつく通称「つっつき会」に発展し、Railsコミットの読み方を実地で目にできる貴重な機会となっています。今ではappear.inを使って「つっつき会」を社内で共有するようになりました。

皆さまからのTechRacho記事へのご意見やご感想をお待ちしております。こういった皆さまからのフィードバック↓が何よりも励みになります。Twitterの@techrachoまたは@hachi8833、あるいは末尾のフォームまでどうぞ。

お読みいただいている皆さまに改めてお礼を申し上げます。来年もどうぞよろしくお願いいたします。
それでは良いお年をお迎えください!

フォーム

RailsのモデルIDにUUIDを使う(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

RailsのモデルIDにUUIDを使う(翻訳)

UUID(universally unique identifier)は、コンピュータシステムで情報の識別に用いられる128ビットの数字です。GUID(globally unique identifier)と呼ばれることもあります。

PostgreSQLにはネイティブのカラム型があります。PostgreSQLの型についてはRailsガイド(英語)をご覧ください。

Railsのデフォルトであるカウントアップする整数idの代わりに、次のPostgreSQLのUUIDサポートを使います。

Ruby on Rails 5.0以降、ActiveRecordモデルでUUIDをidとして利用できる機能があります。

PostgreSQL拡張を有効にする

bin/rails g migration enable_extension_for_uuidを実行してEnableExtensionForUuidモデルを以下のようにします。

class EnableExtensionForUuid < ActiveRecord::Migration[5.1]
  def change
    enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')
  end
end

config/initializers/generators.rbを作成する

Rails.application.config.generators do |g|
  g.orm :active_record, primary_key_type: :uuid
end

これによって主キーで使われるデフォルトのカラム型が変更され、マイグレーションジェネレータで新規テーブル作成時にid: :uuidが設定されるようになります。

以後のマイグレーション

リレーション作成時にtype: :uuidを使う必要があります。

class AddNewTable < ActiveRecord::Migration[5.1]
  def change
    create_table :related_model do |t|
      t.references :other, type: :uuid, index: true
    end
  end
end

UUIDを使う理由

Railsのモデルで、カウントアップする整数の代わりにUUIDをidとして使うことで、衝突の回避に役立ちます。UUIDの一意性はグローバルなので、異なるモデルが同じidを持つ可能性が発生せず、クライアント側や別のシステムで代入することもできます。

整数idがカウントアップされると、データのサイズを外部から推測可能になってしまいます。たとえばidが5なら5番目に作成されたレコードであることがわかります。UUIDを用いるとデータベーステーブルのサイズを誰も推測できなくなります(テーブルサイズを知られたくない場合)が、外部に公開されるURLでパブリックなidやスラッグ(slug)を使っていれば一応回避できてしまいます。それにしてもRails組み込みのツールを使わない理由はどこにあるのでしょうか?

セキュリティの観点からは、UUIDを用いることで、悪意のある攻撃者がURLからモデルのidを推測してデータにアクセスしようとする事態を防止します。UUIDの推測はきわめて困難です。

UUIDは、それによって少々複雑になっても構わない十分な理由がある場合に向いています。

UUIDを使わない方がよい場合

PostgreSQLを使っている場合はシンプルな変更で済み、パフォーマンス上のコストもほとんど増加しません。MySQLの場合はもっと複雑になります。私は気にしないと思いますが。

UUIDのidでは、ActiveRecordのfirstlastのスコープが期待通りにならなくなります。直近のidの値が最大であるという仮定は使えませんので、新しく参加する開発者がコードベースで混乱するかもしれません。

UUIDは完全に新規のプロジェクトに向いています。しかし現在稼働中のプロジェクトでUUIDに切り替えるのは、よほどの理由がない限り避けるほうが賢明かもしれません。

関連記事

Railsのdefault_scopeは使うな、絶対(翻訳)

Rails: 日付や時刻のカラム名を命名規則に合わせよう(翻訳)

Rubyの単項演算子(+、-)の動作

$
0
0

あけましておめでとうございます。本年もTechRachoをよろしくお願いいたします。

年末年始はインフルエンザで完璧に寝正月になってしまったのですが、Rubyで小ネタを見つけたので記事にしてみました。
チェックにはRuby 2.5を使いました。

Rubyの単項演算子(+、-)の動作

基本の動作

今さらですが、単項のIntegerやFloatの前に付く+-はそのまま数学記号の正符号、負符号とみなせます。この場合+は何もしないと解釈されるため、最終的に数学記号の慣習に沿って+はドロップされます。

1     #=>  1
+1    #=>  1
-1    #=> -1
1.0   #=>  1.0
+1.0  #=>  1.0
-1.0  #=> -1.0

+-が複数ある場合、解釈後に圧縮されます。これは間にスペースがあっても同じです。

-+-- ++- -+ 1  #=> -1

2項演算でも同様です。

-1 -+-- ++- -+ 1 #=> -2

こんな書き方は誰もしませんが、数学記号としての正負符号の挙動が担保されていることがわかります。

この挙動は?

今回気づいたのは以下の挙動でした。

まず、丸かっこ()はあってもなくても単項演算子の解釈に違いはありません。

-(-1) #=>  1
+(-1) #=> -1

classメソッドでクラスを調べることもできます。

-1.class #=> Integer
+1.class #=> Integer

しかし、丸かっこの外に単項演算子がある状態でclassを取ってみると次のようになりました。

-(-1).class #=> NoMethodError: undefined method `-@' for Integer:Class
+(-1).class #=> NoMethodError: undefined method `+@' for Integer:Class

当初はおやっと思いましたが、(-1).classが先に解釈され、続いて戻り値Integerに対して単項演算子-が適用されたことにやっと気づきました。

この挙動を回避するには、丸かっこをさらに追加します。

(-(-1)).class #=> Integer
(+(-1)).class #=> Integer

これは変数などに対しても同様です。

a = 1
-a.class    #=> NoMethodError: undefined method `-@' for Integer:Class
(-a).class  #=> Integer

おまけ: 文字列結合の+

ついでながら、文字列結合演算子としての+も連続して置くことができます。

"a" + + "a" #=> "aa"

なお、文字列結合は原則として+ではなく式展開"#{}"を使いましょう。

Rubyでの文字列出力に「#+」ではなく式展開「#{}」を使うべき理由

関連記事

Ruby: ぼっち演算子`&.`の落とし穴(翻訳)

【保存版】Rubyスタイルガイド(日本語・解説付き)総もくじ

ソフトウェアテストでstubを使うコストを考える(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

ソフトウェアテストでstubを使うコストを考える(翻訳)

本記事では、テストで何かと見かけるstubについて考察します。stubやmockは便利だと思う人もいれば、そう思わない人もいたりします(stubとmockは別物ですが、両者の違いは本記事の範疇ではないため、まとめてstubと呼ぶことにします: どうかご了承ください)。この話題は私が働いているチームではすっかり落ち着いていたのですが、最近になってまた話題にのぼったので、この際私の考えをざっくりここにまとめることにしました。誤りや見落としがありましたらぜひお知らせください。

かつての私は、依存をstubするのが大好きな開発者でした。テストが簡単に書けますし、読みやすく、しかもシンプルです。

class Customer
  def order_fee
    if inherit_fee?
      company.fee
    else
      fee
    end
  end
end

class Order
  def total
    subtotal + customer.order_fee
  end
end

上のコードから、feeは、それに関連する特定の1人のcustomerまたは1つのcompanyについて定義できることがわかります。依存をstubする(実際はメソッドの呼び出しですが)テストは次のようになります。

describe Order do
  let(:customer) { Customer.new }
  let(:order)    { Order.new(subtotal: 100, customer: customer) }

  describe '#total' do
    context 'feeを持つcustomer' do
      before do
        allow(customer).to receive(:order_fee).and_return(21)
      end

      it 'feeを含む合計額を返す' do
        expect(order.total).to eq(121)
      end
    end

    context 'feeを持たないcustomer' do
      before do
        allow(customer).to receive(:order_fee).and_return(0)
      end

      it 'feeを含まない合計額を返す' do
        expect(order.total).to eq(order.subtotal)
      end
    end
  end
end

customerやcompanyのfeeを設定する代わりに、単にCustomer#order_feeの結果をstubで塞いでいます。「そこは本物のcustomerオブジェクトじゃなくてstubオブジェクトを使うんじゃね?」とツッコまれそうですね。もちろん、できます。

describe Order do
  let(:customer) { instance_double(Customer, order_fee: 21) }
  # テストをここに書く
end

しかしあまり変わり映えしません。もうひとつの方法は、本物のメソッドの呼び出しを持つ本物のオブジェクトを使うことです。

describe Order do
  let(:customer) { Customer.new }
  let(:order)    { Order.new(subtotal: 100, customer: customer) }

  describe '#合計' do
    context 'feeを持つ顧客' do
      before do
        customer.fee = 21
      end

      it 'feeを含む合計額を返す' do
        expect(order.total).to eq(121)
      end
    end

    context 'feeを持たない顧客' do
      before do
        customer.fee = 0
      end

      it 'feeを含まない合計額を返す' do
        expect(order.total).to eq(order.subtotal)
      end
    end
  end
end

私が好きなのは最後のアプローチなので、このままこのテストを使おうと思います。もしかすると「テストで網羅できてないケースがあるよね?feeがcompanyから継承されるケースもテストしなきゃ」とツッコまれるかもしれません。おっしゃるとおりです。しかしCustomer#order_feeの部分はCustomerクラス用に書いたテストでカバーされているのですから、同じことを繰り返す理由はありません。もしここで第3のケースがCustomer#order_feeに追加されたら、あなたならOrder#totalのテストに戻って新しいケースのテストをまたひとつ追加しますか?stubを使うテストの方がよいと思うでしょう。見た目にも簡単ですし、Customer#order_feeの戻り値に注目すればよく、依存で何が発生するかを気にする必要もありません。

安全性を高める結合テストとcontractor test

訳注: contractor testに定訳がないため本記事では英ママとしました

stubを使う単体テストで最も用心しなければならないのは、実際のオブジェクトと協調動作する結合テスト(あるいはcontractor test?)も必要になる点です。そうしたテストがないと、単体テストがgreenになっても本番のコードが失敗します。私たちの結合テストで扱う操作に8つのオブジェクトが関わっているとしましょう。これを保証するには、正常に動作するコードでこれらすべてのオブジェクトにアクセスするようになっていなければなりませんが、これらのオブジェクトですべてのケースをカバーするテストが必要ということではありません。たとえば、Customer#order_feeで実際のcompanyオブジェクトにアクセスするような結合テストがない場合、そのcompanyオブジェクトでcustomerオブジェクトが正常に動作するという証拠もないということになります。

したがって、オブジェクトをstubすれば単体テストはシンプルになりますが、その分結合テストが複雑になります。さらにCustomerクラスに新しく依存が追加されたときに、あなたなら結合テストをチェックして新しい依存がcustomerオブジェクトで正常に機能するようにしますか?単体テストで実際のオブジェクトを使っておけば、オブジェクトが依存性と協調して動作することはチェック済みになるので、信頼性が高まります。しかしこれも銀の弾丸ではありません。

依存のケースの取りこぼし

最新のテストセットの「ケースの取りこぼし」の話題に戻りましょう。前述のとおり、Customer#order_feeのすべてのケースをOrder#totalのテストでカバーするのはたぶんおかしいでしょう。そのテストはCustomer#order_feeのケースのテストではなくOrder#totalのテストであり、orderオブジェクトとcustomerオブジェクトの協調動作を確認するためのものだからです。したがって、操作に関連するオブジェクトの一部を結合テストで取りこぼしてしまうと、本番のバグをキャッチできる可能性が下がってしまいます。オブジェクトのやりとりは単体テストで既にカバーされているからです。

次のコードはもっと違う実装にできるのではないかとツッコまれるかもしれませんが、テストはパスします。

class Order
  def total
    subtotal + customer.fee
  end
end

確かにテストはパスしますが、subtotalが121に等しい場合にもテストはパスしてしまいます(テスト対象のオブジェクトの設定で何かしくじったのかもしれませんね)。これはTDD(テスト駆動開発: 先にテストを作成/変更してからコードの作成や変更を行う)を行う理由のひとつです。それもこれも信頼性のためです。Customer#order_fee用に書かれたテストを信頼しないのであれば、ActiveRecord#save!は信頼できるでしょうか?ActiveRecord#save!を使うときに、あなたなら以下のケースをテストしますか?

  • DB接続がない場合にエラーをraiseする
  • テーブルが存在しない場合にエラーをraiseする
  • フィールドが存在しない場合にエラーをraiseする
  • ActiveRecord#save!内部のあらゆる部分

コードを書くときには常にメンテナンスコストにも気を遣う必要があります。そのコストはいずれ誰かが払わなければなりません。結合テストでは多数のオブジェクトが関連するので、関連するいくつかのオブジェクトへのアクセスパスを見落とす可能性がうんと高まります。繰り返しますが、バグの可能性が高まれば誰かがバグ修正のコストを支払わなければならなくなります。バグを見逃せば損害が発生することをどうかお忘れなく。営業チームにとっていい迷惑です(もちろんあなたにとっても)。

stubを使う意味がある場合

stubはどんな場合にも避けるべきであると言いたいのではありません。特定の状況では非常に有用です。

SinonJsをご存知でしょうか。このライブラリはAjaxレスポンスをstub化できます。

this.server.respondWith(
  'GET',
  '/some/article/comments.json',
  [200, { 'Content-Type': 'application/json' }, '[{ "id": 12, "comment": "こんちわ" }]']
);

これはstubのユースケースとして完璧です。最も低レベルな部分をstubしているので、コードのさまざまなレイヤにわたるリクエストのテストを実行すれば、コードが本番でちゃんと動作することを検証できます。

ライブラリを書くときにも、ライブラリの依存をstubできます。Faraday gemはこのアプローチのよい適用例です。

テストを高速にするstub

テストが高速になるという理由でstubを好む人もいます。私はこう思います: 普通ならテストが遅くなったときに「プロジェクトでパフォーマンスの問題が発生してるぞ」と誰かが気づく可能性がありますが、stubを使うとテスト中のパフォーマンス低下が隠蔽されてしまいます。

まとめ

コーディング上のこだわりはこの際抜きにして、作業しているコードの一貫性を保つようにしましょう。さもないと、チームに加わった新メンバーが戸惑い、開発速度も落ちてしまいます。

関連記事

テストを不安定にする5つの残念な書き方(翻訳)

RSpecで役に立ちそうないくつかのヒント(翻訳)

[Rails] RSpecのモックとスタブの使い方

[Rails] RSpecをやる前に知っておきたかったこと

新しいRailsフロントエンド開発(2)コンポーネントベースでアプリを書く(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

新しいRailsフロントエンド開発(2)コンポーネントベースでアプリを書く(翻訳)

前書き

本記事は、フロントエンドのフレームワークに依存しないRailsプレゼンテーションロジックを現代的かつモジュール単位かつコンポーネントベースで扱う方法を独断に基いて解説するガイドです。3部構成のチュートリアルで、例を元に最新のフロントエンド技術の最小限に学習し、Railsフロントエンド周りをすべて理解しましょう。

Part 1のおさらい

こちらもお読みください: Part 1

Hacker NewsReddit数々の議論を呼び起こしたPart 1では、標準的なRailsアプリを現代的なフロントエンドプラクティスに合わせて組み替えました。Webpacker gemを用いてアセットをWebpackでビルドしつつ、CSSをPostCSScssnextで処理しています。BabelAutoprefixerBrowserslistのおかげでクロスブラウザの問題に悩まされずに済むようになりました。git commitのたびにPrettierAirBnB Base ConfigESLintstylelintでコードの文法エラーを自動チェックできるようになりました。

フォルダ構造をわかりやすく変えてコンポーネント指向で考えられるようにし、それでいてReactなどのいかなるフロントエンドフレームワークにも依存しません。昔ながらの.erbパーシャルもこれまでどおり扱えます。開発中はいつものrails sの代わりに弊社が推しているhivemindこちらからどうぞ)やforemanでサーバーを起動します。

チュートリアルPart 2のコードを含むGitHubリポジトリでコードをすぐにご覧いただけます。

ここまでのアプリは「Hello World」メッセージを表示する機能しかなく、まだ体をなしていません。今回は現実のアプリを作りましょう。チュートリアルに沿って私たちと一緒にアプリを作成するときはかなりコピペを繰り返すことになります(もちろんコード例を手入力しても好みに応じて変更しても構いません)。まずはPart 1を完了させておきましょう。

アプリを現実に近づける

前回表示に使った以下のコンポーネントを思い出しましょう。

<!-- app/views/pages/home.html.erb -->
<%= render "components/page/page" do %>
  <p>Hello from our first component!</p>
<% end %>

コンポーネントをレンダリングするヘルパーを導入して少々楽をしましょう。次のような感じです。

<%= c("page") do %>
  <%= c("auth-form") %>
<% end %>

これでフルパスを入力しなくてもコンポーネント名だけを指定するだけで済むようになります。このヘルパーは、同じフォルダ内に置かれた、機能がほんの少し異なる2つのパーシャルを扱うこともできます(_message-form.html.erb_message-form_admin.html.erbなど)。2つのパーシャルを区別しやすくするため、アンダースコア_を慣習として使っています。

application_helper.rbを開いてメソッドを1つ追加します。

module ApplicationHelper
  def component(component_name, locals = {}, &block)
    name = component_name.split("_").first
    render("components/#{name}/#{component_name}", locals, &block)
  end

  alias :c :component
end

次はコントローラです。現時点ではスモークテストで必要だったpages_controller.rbが1つあるだけです。これは削除しても問題ありません(その場合、対応するapp/views/pagesフォルダも削除します)。私たちのチャットアプリには、認証用のAuthControllerと、チャットウィンドウを受け持つChatControllerの2つのコントローラを置くことにします。次のコマンドで2つのコントローラを生成できます。

$ rails g controller auth
$ rails g controller chat

routes.rbも変更しておきます。

Rails.application.routes.draw do
  root to: "chat#show"

  get  "/login", to: "auth#new"
  post "/login", to: "auth#create"
end

認証ページの作成に取り掛かりましょう。

# app/controllers/auth_controller.rb
class AuthController < ApplicationController
  before_action :only_for_anonymous # 既知のユーザーかどうかをチェック

  def new; end

  # paramsからusernameを取得し、sessionに保存してチャットにリダイレクトする
  def create
    session[:username] = params[:username]
    redirect_to root_path
  end

  private

  # ユーザーが以前チャットしたことがある場合はそのままチャットウィンドウにリダイレクト
  def only_for_anonymous
    redirect_to root_path if session[:username]
  end
end

サンプルアプリなのでアクションはかなりシンプルです。初めてのユーザーにはusernameの入力を求め、それをsessionハッシュに保存します。リピーターの場合は認証ページをスキップします。newアクションで必要なビューは1つだけなので、作成してみましょう。設計上、ビューテンプレートにはコンポーネントのパーシャルを呼び出すrender呼び出しのみを含めるべきです。ここでは、Part 1の最後に作成したpageコンポーネントの内部にauthコンポーネントを埋め込みます。

$ touch app/views/auth/new.html.erb
<!-- app/views/auth/new.html.erb -->
<%= c("page") do %>
  <%= c("auth-form") %>
<% end %>

今度は認証フォーム用のコンポーネントを1つ作成しましょう。これには明示的にauth-formという名前を付けます。

$ mkdir -p frontend/components/auth-form
$ touch frontend/components/auth-form/{auth-form.css,auth-form.js,_auth-form.html.erb}

手作業が面倒になってきた方は、本記事の末尾にあるコンポーネント用ジェネレータの導入部分までスキップしてください。

新しいコンポーネントを1つ作成するたびに、これらの2つのコマンドを実行します。手始めに.erbパーシャルからやってみましょう。ここでは標準的なRailsヘルパーを使って標準的なフォームを作ります。

<!-- frontend/components/auth-form/_auth-form.html.erb -->
<div class="auth-form">
  <%= form_tag login_path, method: :post do %>
   <%= text_field_tag :username, "", class: "auth-form--input", placeholder: "Choose your username...", autofocus: true, required: true %>
   <%= submit_tag "Identify me", class: "auth-form--submit" %>
  <% end %>
</div>

最初の時点でCSSの命名ルールを定めておくのも合理的です。

明快な命名法を選ぶことで、共通の名前空間で名前の衝突を避けられますし、コードが自ら語るようになります。


本記事でご覧いただいているアプローチではCSS Modules使っていませんので、名前が衝突しないよう辛抱強くCSSに名前を付けることにします。

参考: CSS Modules所感

「ブロック/要素」アプローチを採用したいので、BEMのハンドブックから拝借することにします(ブロックは私たちのコンポーネント、要素はその論理的なパーツに相当します)。BEMの書式component-name--element-nameを選択します。こうすることで、テキストフィールドや送信ボタンは次のクラスに従う必要があります。auth-form--inputauth-formがコンポーネント、inputが要素になります。auth-form--submitauth-formがコンポーネント、submitが要素になります。BEMの「M」はmodifierの略ですが、このアプリでは簡単のためmodifierは使わないことにします。

もちろん、CSS命名ルールは、コンポーネント間で統一されていれば、各自のこだわりに合わせていただいて構いません。

とりあえずスタイルの下地はできあがりましたが、まだ何も追加されていません。現時点の認証ページ(localhost:5000/login)は次のようになっています。

スタイルなしの認証ページ

スタイルなしの認証ページ

ここで一手間かけて、CSSクラスをネストできるpostcss-nestedプラグインも有効にしておきましょう。ターミナルでyarn add postcss-nestedと入力し、pluginsセクション内の冒頭行に.postcssrc.yml: postcss-nested: {}を追記します。

それではいよいよスタイルをいくつか足してみましょう。スタイルはWebpackからJavaScript経由で取り込まれるので、常にコンポーネントのスタイルシートをimportでコンポーネントのJavaScriptファイルに取り込む必要があります。また、application.jsエントリポイントの内部でコンポーネントを「登録」する必要もあります。

// frontend/packs/application.js
import "init";
import "components/page/page";
import "components/auth-form/auth-form";
// frontend/components/auth-form/auth-form.js
import "./auth-form.css";
/* frontend/components/auth-form/auth-form.css */
.auth-form {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;

  &--input {
    width: 100%;
    padding: 12px 0;
    border: 1px solid rgba(0, 0, 0, 0.1);
    font-size: 18px;
    text-align: center;
    outline: none;
    transition: border-color 150ms;
    box-sizing: border-box;

    &:hover,
    &:focus {
      border: 1px solid #3f94f9;
    }
  }

  &--submit {
    width: 100%;
    margin-top: 6px;
    padding: 12px 0;
    background: #3f94f9;
    border: 1px solid #3f94f9;
    color: white;
    font-size: 18px;
    outline: none;
    transition: opacity 150ms;
    cursor: pointer;

    &:hover,
    &:focus {
      opacity: 0.7;
    }
  }
}

component-name--element-nameという命名ルールのおかげで、ネストした PostCSSをアンパサンド&で簡単に書けるようになったのがわかります。この&は、PostCSSが純粋なCSSに変換されるときに単に「親」クラス名に置き換えられるので、.auth-form { &--input }.auth-form.auth-form--inputの2つの別々のクラスになります。しかし私たちのコードでは、auth-formコンポーネントに関連するものはすべてauth-formクラスのスコープ内に含まれるので、クラス名の衝突を気にする必要はありません。ポイントは、「親」CSSクラス名をプロジェクト内のコンポーネントとそのフォルダに正確に一致させることです。こうしないと、たちまちコードがスパゲッティになってしまうでしょう。

これで、(サーバーが既に動いていれば)ブラウザウィンドウに戻るとログインページにスタイルが追加されていることがわかります。webpack-dev-serverはJavaScriptファイルの変更を検出してバックグラウンドでページを更新します。

スタイル付きの認証ページ

スタイル付きの認証ページ

CSSをこんなに簡単にいじれるようになったのがおわかりでしょうか?ボタンの色を変える必要があるなら、ブラウザとコードエディタをそれぞれ開いて横に並べて作業すれば、変更したファイルを保存するたびにブラウザに即座に反映されます。これでスタイル変更作業が非常にはかどります。

: このフォームを送信して認証ページが表示されなくなった場合(コントローラの現在のロジックでは、ユーザー名がsessionに保存されると戻れなくなります)、ブラウザのcookieを削除してください。

メッセンジャーを撃たないで

訳注: Don’t shoot the messengerはYouTubeのコメディ番組のタイトルで、shooting the messenger(悪い知らせをもたらした人を責める言い回し)のもじりです。Pusciferのアルバムタイトル「Don’t shoot the messenger」でもあります。

認証ページからどこか別のページにユーザーを導く必要がありますが、現時点ではルーティングが少々とからっぽのChatControllerしかありません。メッセージを扱えるようにしたいので、基本的なMessageモデルが必要です。さっそく作ってみましょう。

$ rails g model message author:string text:text
$ rails db:create
$ rails db:migrate

メッセージはActionCableを使って作成されるので、メッセージを表示する何らかの方法がコントローラに必要です。ページを最初に読み込んだときに最新の20件を表示することにします。

# app/controllers/chat_controller.rb
class ChatController < ApplicationController
  before_action :authenticate!

  # 最新メッセージを20件表示
  def show
    @messages = Message.order(created_at: :asc).last(20)
  end

  private

  # ユーザーがusernameを指定しなかった場合/loginにリダイレクト
  def authenticate!
    redirect_to login_path unless session[:username]
  end
end

繰り返しますが、ビューは1つあれば十分です。今回はshow.html.erbを作成します。

$ touch app/views/chat/show.html.erb
<!-- app/views/chat/show.html.erb -->
<%= c("page") do %>
  <%= c("chat", messages: @messages) %>
<% end %>

コンポーネントは単なる純粋なERBパーシャルであり、renderメソッドを使うヘルパーによってレンダリングされるので、いつもと同じようにローカルを渡します。コンポーネントの追加方法は既に学びましたね。

$ mkdir -p frontend/components/chat
$ touch frontend/components/chat/{chat.css,chat.js,_chat.html.erb}

ここからコンポーネントのネストが深くなります。私たちのchatコンポーネントは、ページのコンテンツ全体を参照する方法の1つです。ページには、動的に更新されるメッセージリストと、新しいメッセージを送信するフォームを1つずつ作成するので、messagesmessage-formの2つのコンポーネントに分割できます。また、メッセージが複数あるところにはメッセージが1件あるので、messageコンポーネントも必要です。ターミナルでもう少し作業しましょう。

$ mkdir -p frontend/components/message
$ touch frontend/components/message/{message.css,message.js,_message.html.erb}

$ mkdir -p frontend/components/messages
$ touch frontend/components/messages/{messages.css,messages.js,_messages.html.erb}

$ mkdir -p frontend/components/message-form
$ touch frontend/components/message-form/{message-form.css,message-form.js,_message-form.html.erb}

ファイルとフォルダの作成がすべて終わると、次のような構造になるはずです。

frontend/components
   ├── auth-form
   │   ├── _auth-form.html.erb
   │   ├── auth-form.css
   │   └── auth-form.js
   ├── chat
   │   ├── _chat.html.erb
   │   ├── chat.css
   │   └── chat.js
   ├── message
   │   ├── _message.html.erb
   │   ├── message.css
   │   └── message.js
   ├── message-form
   │   ├── _message-form.html.erb
   │   ├── message-form.css
   │   └── message-form.js
   ├── messages
   │   ├── _messages.html.erb
   │   ├── messages.css
   │   └── messages.js
   └── page
       ├── _page.html.erb
       ├── page.css
       └── page.js

親コンポーネントchatでコードの空白を埋めていきます。

<!-- frontend/components/chat/_chat.html.erb -->
<div class="chat">
 <div class="chat--messages">
   <%= c("messages", messages: messages) %>
 </div>
 <div class="chat--form">
   <%= c("message-form") %>
 </div>
</div>

上のコードから、このコンポーネントはサブコンポーネントもレンダリングすることがわかりますが、サブコンポーネントを個別のエントリポイントにすべて入れたくないので、このままではすぐ手に負えなくなってしまう可能性があります。そこで次の経験則を導入することにします。「あるコンポーネントに子が1つ以上ある場合は、子をcomponent’s .jsファイルでimportすること」。こうすることで、application.jsには階層のトップに位置するコンポーネントだけを登録すれば済むようになります。ここで正しい方法でやっておけば、後々忘れずに済みます。

// 更新後のfrontend/packs/application.js
import "init";
import "components/page/page";
import "components/auth-form/auth-form";
import "components/chat/chat";

続いて、chat内部のネストしたコンポーネントのJSファイルをchat.jsでインポートします。

// frontend/components/chat/chat.js
import "components/messages/messages";
import "components/message-form/message-form";
import "./chat.css";

最後はCSSです。

/* frontend/components/chat/chat.css */
.chat {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: 100%;
  height: 100%;
  overflow: hidden;

  &--messages {
    width: 100%;
    flex: 1 0 0;
  }

  &--form {
    width: 100%;
    background: white;
    flex: 0 0 50px;
  }
}

1つ目のコンポーネントが終わりました。あと3つです!

message-formのERBは次のとおりです。

<!-- frontend/components/message-form/_message-form.html.erb -->
<div class="message-form js-message-form">
  <textarea class="message-form--input js-message-form--input" autofocus></textarea>
  <button class="message-form--submit js-message-form--submit">Send</button>
</div>

ここでは<form>タグを使っていないことにご注意ください。ActionCableを使うために、<textarea>の内容をJavaScriptで送信するからです。

おそらく、ここでクラス名がmessage-formjs-message-formと2回使われている点が気になる方がいらっしゃると思います。この慣習に従っておくことで、設計が変更されてクラス名が変更されたときに、JavaScriptのセレクタが影響を受けずに済みます。つまり、CSSの名前とJavaScriptの名前の2とおりの命名が共存することになります。皆さんのコードでこの通りにする必要はありませんので、単一のセレクタを使ってもかまいません。しかしその場合、CSSクラス名が変更されるたびに、再設計でロジックが壊れないようにするためにDOMを操作するJavaScriptコードも手動で変更しなければならなくなります。

// frontend/components/message-form/message-form.js
import "./message-form.css";
/* frontend/components/message-form/message-form.css */
.message-form {
  display: flex;
  width: 100%;
  height: 100%;

  &--input {
    flex: 1 1 auto;
    padding: 12px;
    border: 1px solid rgba(0, 0, 0, 0.1);
    font-size: 18px;
    outline: none;
    transition: border-color 150ms;
    box-sizing: border-box;
    resize: none;

    &:hover,
    &:focus {
      border: 1px solid #3f94f9;
    }
  }

  &--submit {
    flex: 0 1 auto;
    height: 100%;
    padding: 12px 48px;
    background: #3f94f9;
    border: 1px solid #3f94f9;
    color: white;
    font-size: 18px;
    outline: none;
    transition: opacity 150ms;
    cursor: pointer;

    &:hover,
    &:focus {
      opacity: 0.7;
    }

    &:active {
      transform: translateY(2px);
    }
  }
}

作業中はいつでもlocalhost:5000でチャットウィンドウを表示できます。準備ができていないコンポーネントについてはcレンダリング呼び出しをコメントアウトして止めておくことだけお忘れなく。

先に進みましょう。ここまでで、親コンポーネントとフォームが1つずつできました。次は、メッセージを表示する場所と、各メッセージのテンプレートが必要です。これまでのパターンどおり、ERB、JS、CSSの順に作成します。

<!-- frontend/components/messages/_messages.html.erb -->
<div class="messages js-messages">
  <div class="messages--content js-messages--content">
    <% messages.each do |message| %>
      <%= c("message", message: message) %>
    <% end %>
  </div>
</div>
// frontend/components/messages/messages.js
import "components/message/message"; // メッセージはネストされるので、ここでimportする
import "./messages.css";
/* frontend/components/messages/messages.css */
.messages {
  position: relative;
  width: 100%;
  height: 100%;
  background: white;
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-bottom: 0;
  box-sizing: border-box;

  &--content {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    overflow-x: hidden;
    overflow-y: auto;
  }
}

最後は個別のメッセージのコードです。

<!-- frontend/components/message/_message.html.erb -->
<div class="message">
  <div class="message--header">
    <span class="message--author">
      <%= message.author %>
    </span>
    <span class="message--time">
      <% if message.created_at > Time.now - 24.hours %>
        <%= l(message.created_at, format: :short) %>
      <% else %>
        <%= l(message.created_at, format: :long) %>
      <% end %>
    </span>
  </div>
  <div class="message--text">
    <% message.text.lines.each do |line| %>
      <p><%= line %></p>
    <% end %>
  </div>
</div>
// frontend/components/message/message.js
import "./message.css";
/* frontend/components/message/message.css */
.message {
  margin: 12px 6px;

  &:first-child {
    margin-top: 0;
  }

  &:last-child {
    margin-bottom: 0;
  }

  &--author {
    font-weight: bold;
  }

  &--time {
    color: rgba(0, 0, 0, 0.5);
    font-size: 12px;
  }

  &--text p {
    margin: 0;
  }
}

ここまでの作業がすべてうまくいっているかどうかテストしましょう。まだフォームでメッセージを作成できないので、rails consoleMessageインスタンスをいくつか作成し、正しく表示されるかどうかを実際にチェックします。

# rails consoleで以下を入力する
> Message.create(author: "Evil Martian", text: "Surrender!")

サーバーが実行されていることを確認し、ブラウザを更新します。上のとおりに進めていれば、以下のように表示されるはずです。

チャットウィンドウ

チャットウィンドウ

おまけ

コンポーネントのフォルダやファイルの手動作成ばかり続いて疲れたら、ここでご紹介するRailsジェネレータを使って必要に応じて調整するとよいでしょう。libフォルダの中にgeneratorというフォルダを作成し、そこにcomponent_generator.rbというファイルを置いて以下を記述します。

$ mkdir lib/generators
$ touch lib/generators/component_generator.rb
# lib/generators/component_generator.rb
class ComponentGenerator < Rails::Generators::Base
  argument :component_name, required: true, desc: "Component name, e.g: button"

  def create_view_file
    create_file "#{component_path}/_#{component_name}.html.erb"
  end

  def create_css_file
    create_file "#{component_path}/#{component_name}.css"
  end

  def create_js_file
    create_file "#{component_path}/#{component_name}.js" do
      # コンポーネントのCSSをJS内で自動requireする
      "import \"./#{component_name}.css\";\n"
    end
  end

  protected

  def component_path
    "frontend/components/#{component_name}"
  end
end

これで以下のコマンドラインでコンポーネントを生成できます。

$ rails g component コンポーネント名

チュートリアルPart 2の完了おめでとうございます!もしうまく動かない場合はGitHubリポジトリのコードでチェックしましょう。ここまでお読みいただきありがとうございます。次回Part 3では、いよいよActionCableでアプリをインタラクティブにし、いくつか仕上げ作業を行ってからHerokuにデプロイします。「sprockets抜き」のRailsアプリで生じる問題についても取り上げます。どうぞお楽しみに!


Part 1 | Part 2 | Part 3

スタートアップをワープ速度で成長させられる地球外エンジニアよ!Evil Martiansのフォームにて待つ。

関連記事

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)

Rubyのシンボルをなくせるか考えてみた(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rubyのシンボルをなくせるか考えてみた(翻訳)

この記事を読んでいる皆さまの場合はわかりませんが、私は個人的に文字列とシンボルの区別に由来するバグを余裕で10回以上は踏んでいます。このバグは私の書いたコードでも、他のライブラリを使うときにも起きました。私はコードに表示されるシンボルの外観は好きなのですが、シンボルと文字列の特定の区別のされ方が好きではありません。(こういうことを書くと炎上しそうですが)この区別は問題を解決するよりも作り出す方が多いと思います。

そこで私も考えてみました。いっそシンボルをなくしてしまえばどうだろう。過激な主張でしょうか?かといって、何千というRubyライブラリを書き直して、そこに含まれる:symbolを片っ端から削除して回るという戦略に勝ち目があるとは思えません。おそらくシンボルリテラルはfrozenかつイミュータブルな文字列として使うこともできるでしょう。どのような仕組みになっているのでしょうか。

もしも世界がこうだったら

問題の解決法を長いパラグラフでみっちり記述するのはつらいので、私が空想する性質をデモでお見せして、コードそのものに語らせたいと思います。もしもの世界へようこそ。

:foo == :foo  # true
:foo == "foo" # true

これが私の出発点であり、目指すゴールでもあります。文字列かシンボルかという区別はもううんざりです。もちろんこんな簡単な話ではありません。私の空想する動作を完全に説明するにはもっと多くの性質(テストケース)が必要です。

私のユースケースでは多くの場合、ハッシュからの値の取り出しやハッシュへの値の代入を使います。ハッシュで表してみましょう。

{"foo" => 1}[:foo] == 1 # true
{foo: 1}["foo"]    == 1 # true

こんなふうにできたらどんなに楽だったことでしょう。

要するに、以下が欲しいのです。

:foo.hash == "foo".hash # true

Hash(またはSet)で何かを出し入れすると、Rubyは常に入力をObject#hashでいわゆるハッシュ関数として扱います。2つのオブジェクトが等しい場合、両者は同じhashを返すべきです。さもないとRubyがHashからオブジェクトをうまく見つけられなくなります。次の例をご覧ください。

class Something
  def initialize(val)
    @val == val
  end
  attr_reader :val

  def ==(another)
    val == another.val
  end
end

a = Something.new(1)
b = Something.new(1)

hash = {a => "text"}
hash[a] # => "text"
hash[b] # => nil

これはいわゆるValue Objectを定義しています。あるクラスがその1つ以上の属性によって定義され、それを比較に用いています。しかし私たちはhashメソッドをまだ実装していないので、RubyはそれらがHashのキーとしても利用される可能性を認識しません。

a.hash
# => 2172544926875462254

b.hash
# => 2882203462531734027

2つのオブジェクトが返すhashが同じ場合、両者が等しいとは限りません。利用できるハッシュ値の個数には上限があり、衝突はめったなことでは起きません。しかし2つのオブジェクトが等しい場合は同じhashを返すべきです。

class Something
  BIG_VALUE = 0b111111000100000010010010110011101011000100010101001100100110000
  def hash
    [@val].hash ^ BIG_VALUE
  end
end

普通ハッシュ値の計算では、すべての属性が正確に一致する配列との衝突を回避するために、すべての属性の配列のhashと巨大な乱数値とのXORを取ります。
言い換えると、欲しいのは以下です。

Something.new(1).hash != 1.hash
Something.new(1).hash != [1].hash

しかしこれは脱線でした。メリットの話に戻りましょう。

繰り返しますが、私は以下だったらよいのにと思っています。

{"foo" => 1}[:foo] == 1 # true
{foo: 1}["foo"]    == 1 # true

そのためには以下が必要です。

:foo.hash == "foo".hash # true

しかしここが重要です。現時点ではシンボルのハッシュ値算出は文字列のハッシュ値算出の2〜3倍高速であるようです。私はその理由を知りません。シンボルはイミュータブルですが、おそらくシンボルは事前に算出済みのハッシュ値を持っているか、メモ化されたハッシュ値を持っているのではないでしょうか。ハッシュ値が変わらないのがその根拠ですが、私にはまだよくわかっていません。しかしこれが理由であれば、frozenかつイミュータブルな文字列が遅延算出またはメモ化されたハッシュ値も持てばよいのではないかと想像できます。

世の中には、以下の事実に依存しているライブラリやアプリが多数あると信じています。

:foo.object_id == :foo.object_id

明らかにこの動作は変えるべきではありません。しかし私は、もし仮にRubyのシンボルが文字列であり、かつRuby内部にそれらの一意なリストが保持されるのであれば、上で行ったように何の問題もなく動作すると信じています。

結局、常に同じシンボルを得られるという事実は、Ruby実装のどこかで単に以下の対応付けがなされていることを示しています。

{"foo" => Symbol.new("foo")}

なお、かつてのシンボルはガベージコレクションすら行われていませんでしたが、現在は行われています。

{"foo" => "foo".freeze}

仮にRuby内部のどこかで上のようになっているしたら、:fooを求めたときにも同じオブジェクトを得られるでしょう。

:foo.object_id == :foo.object_id # true
:foo.equal?(:foo)                # true

先を続けましょう。問題があるのはこのあたりです。

foo = "foo"
foo.equal?(foo.to_s) # true

RubyのString#to_sは基本的にselfを返します。したがって、仮にシンボルがfrozenな文字列だったとしたら、以下は動かないでしょう(実際には動きますが)。

foo = :foo
bar = foo.to_s
bar << " baz"

動かないであろう理由は、barは新しい文字列ではなく、(現在のシンボルがそうであるように)fooと同じオブジェクトになるはずだからです。

ここにはもうひとつ問題が潜んでいます。次のようにオブジェクトがシンボルかどうかをチェックしているライブラリが多数ある可能性が考えられます。

if var.is_a?(Symbol)
  # 何かする
else
  # 別のことをするか、何もしない
end

これをどうにか解決できないか考えました。:foo"foo"を本当に区別しなければならなくなった場合に、どうやって区別したらよいのでしょうか。

2つの選択肢が考えられます。ひとつは、Symbolを、Stringに変換せずにStringのように動作させること(そういうメソッドをすべて追加するかSymbol = Stringというエイリアスにすることによって)で、もうひとつは、SymbolStringから継承する、すなわちSymbol < Stringとすることです。

もしそうできれば、以下はtrueになるでしょう。

:foo.is_a?(Symbol)

しかしその場合、以下もtrueになるでしょう。

:foo.is_a?(String)

この違いは、Symbol#to_sが再定義され、(同一の文字列ではなく)新しい一意のfrozenでない文字列を返すことで生じるでしょう。

つまり以下のような感じになるでしょう。

class Symbol < String
  def initialize(val)
    super
    freeze
  end

  def to_s
    "#{self}"
  end

  def hash
    @hash ||= super
  end
end

こんなふうに動くかどうか、私は疑わしく思っています。今の段階でこのような変更を導入すれば、おそらく膨大なエッジケースが発生するでしょう。しかしFixnumBignumをなくせるなら、Symbolだってなくせるのではないでしょうか?

訳注: FixnumBignumはRuby 2.4で既にIntegerのエイリアスに移行しました。

皆さんもSymbolをなくしたいですか?皆さんはどうお考えですか?コードにSymbolクラスがないとだめですか?それとも皆さんはシンボルの記法が好きなだけでしょうか?

締めくくりに、Matzのコメントを引用します。

(Rubyの)シンボルはLispのシンボルを取り入れたもので、Lispのシンボルは文字列とは根本的に異なっていました。(Lispの)シンボルは文字列表現としてはイケてません(し速くもありません)が、Rubyはシンボルに関して独自路線を取ったため、シンボルと文字列の違いは(Rubyの)ユーザーからはそれほど認識されてこなかったのです。

(シンボルをなくすという)アイデアはだめだとお考えの方には、Matzもシンボルを廃止しようとした(が、できなかった)ことを一応申し上げておきます。

私は、オブジェクトがSymbolかどうかのチェックに依存するライブラリが多すぎると思っています。

ついでに申し上げると、Smalltalkのシンボルは文字列を継承しています。

追伸

本記事をお楽しみいただけた方や、大規模なRailsアプリを手がけている方には、Domain-Driven Railsも楽しくお読みいただけるかと思います。ぜひご覧ください。

関連記事

Ruby2.0でnil.object_idの値が4から8に変わった理由

Rubyのクラスメソッドをclass << selfで定義している理由(翻訳)

あまり知られてないRuby/Railsの便利メソッド5つ(翻訳)

Vue.jsサンプルコード(28)文字の大きさをボタンで変更する

$
0
0

28. 文字の大きさをボタンで変更する

  • Vue.jsバージョン: 2.5.2
  • [小]ボタンと[大]ボタンをクリックすると、「文字の大きさ」という文字列のフォントサイズが変更されます。
  • 画面をリロードすると最初の状態に戻ります。

サンプルコード


バックナンバー(Vue.jsサンプルコード)

Vue.jsサンプルコード(01〜03)Hello World・簡単な導入方法・デバッグ・結果の表示とメモ化


Rails: データベースのパフォーマンスを損なう3つの書き方(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: データベースのパフォーマンスを損なう3つの書き方(翻訳)

2005年にRailsのActiveRecordを初めて見たときに、稲妻のような天啓を感じたのを未だに覚えています。当時はPHPアプリで生SQLクエリを書いていましたが、それまで面倒で退屈でたまらなかったデータベースの扱いが、そのときを境に突如として簡単で楽しいものに変身したのです。そう、楽しくなったのです。

…やがて、ActiveRecordのパフォーマンスの問題に気づくようになりました。

ActiveRecordそのものが遅いわけではありませんでした。ちょうどその頃には実際に実行されるクエリに注意を払わなくなっていたのです。やがて、Rails CRUDアプリで用いられる最も定番のデータベースクエリの中に、データセットが巨大化したときのデフォルトのパフォーマンスがかなり見劣りするものがあることがわかってきました。

本記事では、パフォーマンスを損なう3つの主要な犯人について解説します。しかし最初に、データベースクエリが正常にスケールしているかどうかを調べる方法について説明しましょう。

パフォーマンスの測定

データセットが十分小さければ、どんなデータベースクエリでも十分パフォーマンスを発揮できます。したがって、本当のパフォーマンスを実感するには本番のサイズでデータベースのベンチマークを取る必要があります。ここでは22,000レコードを持つfaultsというテーブルを用いることにします。

データベースはPostgreSQLです。PostgreSQLでパフォーマンスを測定するには次のようにexplainを使います。

# explain (analyze) select * from faults where id = 1;
                                     QUERY PLAN
--------------------------------------------------------------------------------------------------
 Index Scan using faults_pkey on faults  (cost=0.29..8.30 rows=1 width=1855) (actual time=0.556..0.556 rows=0 loops=1)
   Index Cond: (id = 1)
 Total runtime: 0.626 ms

クエリ実行の見積もりコスト(cost=0.29..8.30 rows=1 width=1855)と、実際の実行にかかった時間(actual time=0.556..0.556 rows=0 loops=1)の両方が表示されます。

もう少し読みやすくしたいのであれば、次のようにYAML形式で出力することもできます。

# explain (analyze, format yaml) select * from faults where id = 1;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Index Scan"         +
     Scan Direction: "Forward"       +
     Index Name: "faults_pkey"       +
     Relation Name: "faults"         +
     Alias: "faults"                 +
     Startup Cost: 0.29              +
     Total Cost: 8.30                +
     Plan Rows: 1                    +
     Plan Width: 1855                +
     Actual Startup Time: 0.008      +
     Actual Total Time: 0.008        +
     Actual Rows: 0                  +
     Actual Loops: 1                 +
     Index Cond: "(id = 1)"          +
     Rows Removed by Index Recheck: 0+
   Triggers:                         +
   Total Runtime: 0.036
(1 row)

本記事では、「Plain Rows」と「Actual Rows」の2つだけに注目することにします。

  • Plan Rows: クエリに応答するときに、最悪DBがループを何行回すかという予測を示します
  • Actual Rows: クエリの実行時にDBが実際にループを何行回したかを示します

上のように「Plain Rows」が1の場合、このクエリは正常にスケールすると見込まれます。「Plain Rows」がデータベースの行数と等しい場合、クエリが「フルテーブルスキャン」を行っていることが示されます。この場合クエリはうまくスケールできないでしょう。

クエリパフォーマンスの測定方法の説明が終わりましたので、Railsのいくつかの定番コードでどんな問題が起きているかを見てみましょう。

犯人1: count

以下のコードはRailsビューで非常によく見かけます。

Total Faults <%= Fault.count %>

このコードから生成されるSQLは次のような感じになります。

select count(*) from faults;

explainで調べてみましょう。

# explain (analyze, format yaml) select count(*) from faults;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Aggregate"          +
     Strategy: "Plain"               +
     Startup Cost: 1840.31           +
     Total Cost: 1840.32             +
     Plan Rows: 1                    +
     Plan Width: 0                   +
     Actual Startup Time: 24.477     +
     Actual Total Time: 24.477       +
     Actual Rows: 1                  +
     Actual Loops: 1                 +
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Parent Relationship: "Outer"+
         Relation Name: "faults"     +
         Alias: "faults"             +
         Startup Cost: 0.00          +
         Total Cost: 1784.65         +
         Plan Rows: 22265            +
         Plan Width: 0               +
         Actual Startup Time: 0.311  +
         Actual Total Time: 22.839   +
         Actual Rows: 22265          +
         Actual Loops: 1             +
   Triggers:                         +
   Total Runtime: 24.555
(1 row)

これは…シンプルなcountクエリが22,265回もループしているではありませんか。これはテーブルの全行数です。PostgreSQLでは、countは常に全レコードセットをループします。

このレコードセットのサイズを減らすには、クエリにwhere条件を追加します。要件によっては、パフォーマンスが十分受け入れられる程度にサイズを減らすことができるでしょう。

この問題を回避する他の方法として、唯一、count値をキャッシュする方法があります。Railsにはそのための仕組みがあるので、以下のように設定できます。

belongs_to :project, :counter_cache => true

クエリが何らかのレコードを返すかどうかのチェックにも別の方法があります。Users.count > 0をやめて、代わりにUsers.exists?をお試しください。こちらの方がずっとパフォーマンスは上です(情報提供いただいたGerry Shawに感謝いたします)。

訳注: より高度なcounter_culture gemを使う方法もあります。

Rails向け高機能カウンタキャッシュ gem ‘counter_culture’ README(翻訳)

犯人2: ソート

indexページは、ほぼどんなアプリにも1つや2つあるでしょう。indexページでは、データベースから最新の20レコードを取り出して表示します。これをどうやってもっとシンプルにできるのでしょうか?

レコード読み出し部分はおおよそ以下のような感じになっていると思います。

@faults = Fault.order(created_at: :desc)

このときのSQLは次のような感じになります。

select * from faults order by created_at desc;

分析してみましょう。

# explain (analyze, format yaml) select * from faults order by created_at desc;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Sort"               +
     Startup Cost: 39162.46          +
     Total Cost: 39218.12            +
     Plan Rows: 22265                +
     Plan Width: 1855                +
     Actual Startup Time: 75.928     +
     Actual Total Time: 86.460       +
     Actual Rows: 22265              +
     Actual Loops: 1                 +
     Sort Key:                       +
       - "created_at"                +
     Sort Method: "external merge"   +
     Sort Space Used: 10752          +
     Sort Space Type: "Disk"         +
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Parent Relationship: "Outer"+
         Relation Name: "faults"     +
         Alias: "faults"             +
         Startup Cost: 0.00          +
         Total Cost: 1784.65         +
         Plan Rows: 22265            +
         Plan Width: 1855            +
         Actual Startup Time: 0.004  +
         Actual Total Time: 4.653    +
         Actual Rows: 22265          +
         Actual Loops: 1             +
   Triggers:                         +
   Total Runtime: 102.288
(1 row)

このクエリを実行するたびに、データベースが22,265行をソートしていることがわかります。これはあかんやつです。

SQLはデフォルトで、ORDER BY句のたびにレコードセットをその場でソートします。これにはキャッシュも効きませんし、うまいマジックもありません。

解決法は、インデックスを用いることです。この例のようにシンプルであれば、created_atカラムにソート済みインデックスを追加するだけでクエリはかなり高速になります。

Railsのマイグレーションに以下を追加します。

class AddIndexToFaultCreatedAt < ActiveRecord::Migration
  def change
    add_index(:faults, :created_at)
  end
end

このマイグレーションで、以下のSQLが実行されます。

CREATE INDEX index_faults_on_created_at ON faults USING btree (created_at);

末尾の(created_at)はソート順を指定しています。デフォルトは昇順です。

これでソートのクエリを再度実行してみると、ソートが行われなくなることがわかります。インデックスからソート済みのデータを読み出すだけで済むようになりました。

# explain (analyze, format yaml) select * from faults order by created_at desc;
                  QUERY PLAN
----------------------------------------------
 - Plan:                                     +
     Node Type: "Index Scan"                 +
     Scan Direction: "Backward"              +
     Index Name: "index_faults_on_created_at"+
     Relation Name: "faults"                 +
     Alias: "faults"                         +
     Startup Cost: 0.29                      +
     Total Cost: 5288.04                     +
     Plan Rows: 22265                        +
     Plan Width: 1855                        +
     Actual Startup Time: 0.023              +
     Actual Total Time: 8.778                +
     Actual Rows: 22265                      +
     Actual Loops: 1                         +
   Triggers:                                 +
   Total Runtime: 10.080
(1 row)

複数のカラムでソートする場合は、複数カラムでソートしたインデックスを作成する必要があります。Railsマイグレーションでは以下のような感じで記述します。

add_index(:faults, [:priority, :created_at], order: {priority: :asc, created_at: :desc)

より複雑なクエリに対処する場合は、explainで確認するとよいでしょう。なるべく早いうちに頻繁に行うのがポイントです。クエリによっては、わずかな変更をかけただけで、PostgreSQLでソートのインデックスが効かなくなることに気づくかもしれません。

犯人3: limitoffset

データベースの全項目をindexページに表示することはめったにありません。普通はページネーションを使って、一度に10件から30件、50件程度を表示します。このときに最もよく使われるのがlimitoffsetの組み合わせです。Railsでは次のような感じになります。

Fault.limit(10).offset(100)

このときのSQLは次のような感じになります。

select * from faults limit 10 offset 100;

ここでexplainを実行してみると奇妙なことに気づきます。スキャンされた行数は110件で、ちょうどlimitoffsetを足したのと同じです。

# explain (analyze, format yaml) select * from faults limit 10 offset 100;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Limit"              +
     ...
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Actual Rows: 110            +
         ...

ここでoffsetを10,000に増やしてみると、スキャンされた行数も一気に10010件に増加し、クエリの実行時間も64倍に増えます。

# explain (analyze, format yaml) select * from faults limit 10 offset 10000;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Limit"              +
     ...
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Actual Rows: 10010          +
         ...

ここから残念な結論が得られます。ページネーションでは後のページになるほど速度が低下します。上の例では1ページあたり100件を表示するので、100ページ目になると1ページ目より13倍も時間がかかってしまいます。

どうしたらよいでしょうか?

正直に申し上げると、これについて完全な解決法をまだ見つけられていません。私なら、ページ数が100ページや1000ページにならないよう、まずはデータセットのサイズを減らせないか検討するかもしれません。

レコードセットのサイズを減らすのが無理であれば、offsetlimitwhereに置き換える方法が一番有望でしょう。

# データの範囲を指定
Fault.where("created_at > ? and created_at < ?", 100.days.ago, 101.days.ago)

# またはidの範囲を指定
Fault.where("id > ? and id < ?", 100, 200)

まとめ

本記事が、PostgreSQLのexplain関数を利用してデータベースクエリに潜むパフォーマンスの問題を検出するのにお役に立てば幸いです。どんなシンプルなクエリであってもパフォーマンス上の大きな問題の原因となる可能性があるのですから、チェックする値打ちは十分あると思います :)

関連記事

Rails向け高機能カウンタキャッシュ gem ‘counter_culture’ README(翻訳)

[Rails] RubyistのためのPostgreSQL EXPLAINガイド(翻訳)

Rails開発者のためのPostgreSQLの便利技(翻訳)

週刊Railsウォッチ(20180112)update_attributeが修正、ぼっち演算子`&.`は`Object#try`より高速、今年のRubyカンファレンス情報ほか

$
0
0

こんにちは、hachi8833です。インフルエンザA型が身に沁みました。

2018年最初のウォッチ、いってみましょう。年末年始を挟んでだいぶ記事がたまっているのでいつもより多めです。

Rails: 今週の改修

Ruby 2.5をCIに追加

まずは縁起物コミットから。

# travis.yml
   - 2.2.8
   - 2.3.5
   - 2.4.2
+  - 2.5.0
   - ruby-head

 matrix:
   include:
-    - rvm: 2.4.2
+    - rvm: 2.5.0

PostgreSQLでbulk_change_tableをサポート

MySQLでは以前からbulk: trueが使えるそうです。

# activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#367
+        def bulk_change_table(table_name, operations)
+          sql_fragments = []
+          non_combinable_operations = []
+
+          operations.each do |command, args|
+            table, arguments = args.shift, args
+            method = :"#{command}_for_alter"
+
+            if respond_to?(method, true)
+              sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) }
+              sql_fragments << sqls
+              non_combinable_operations << procs if procs.present?
+            else
+              execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+              non_combinable_operations.each(&:call)
+              sql_fragments = []
+              non_combinable_operations = []
+              send(command, table, *arguments)
+            end
+          end
+
+          execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+          non_combinable_operations.each(&:call)
+        end

MiniMagickでcombine_optionsをサポート

# activestorage/app/models/active_storage/variation.rb#48
   def transform(image)
-    transformations.each do |method, argument|
-      image.mogrify do |command|
-        if eligible_argument?(argument)
-          command.public_send(method, argument)
-        else
-          command.public_send(method)
+    transformations.each do |(method, argument)|
+      if method.to_s == "combine_options"
+        image.combine_options do |combination|
+          argument.each do |(method, argument)|
+            pass_transform_argument(combination, method, argument)
+          end
         end
+      else
+        pass_transform_argument(image, method, argument)
       end
     end
   end

つっつきボイス: 「MiniMagickが好きと聞いて」「好きというほどではw: ImageMagickに比べればマシかなぐらい」

[Rails] MiniMagickでPDFのページ数を取得するときはフォントエラーに注意!

PostgreSQLのrange typeでFloat::INFINITYをサポート

rangeが空文字列にならないようFloat::INFINITYに型変換するようになりました。

# activerecord/test/cases/adapters/postgresql/range_test.rb#361
+    def test_infinity_values
+      PostgresqlRange.create!(int4_range: 1..Float::INFINITY,
+                              int8_range: -Float::INFINITY..0,
+                              float_range: -Float::INFINITY..Float::INFINITY)
+
+      record = PostgresqlRange.first
+
+      assert_equal(1...Float::INFINITY, record.int4_range)
+      assert_equal(-Float::INFINITY...1, record.int8_range)
+      assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range)
+    end

つっつきボイス: 「PostgreSQLのrangeって無限が使えるのか」「無限こわい」

参考: PG10マニュアル: 8.17.4. Infinite (Unbounded) Ranges

()[]を使い分けるんですね。Lintに怒られそう。

(lower-bound,upper-bound)
(lower-bound,upper-bound]
[lower-bound,upper-bound)
[lower-bound,upper-bound]
empty

逆関連付けで外部キーが更新されていなかったのを修正

# activerecord/test/cases/associations/has_many_associations_test.rb#2512
+  test "reattach to new objects replaces inverse association and foreign key" do
+    bulb = Bulb.create!(car: Car.create!)
+    assert bulb.car_id
+    car = Car.new
+    car.bulbs << bulb
+    assert_equal car, bulb.car
+    assert_nil bulb.car_id
+  end

つっつきボイス: 「inverse association、この間案件に出てきたナ」「逆関連付け、でいいのかな」

validationコールバックが複数コンテキストで発火しなくなったのを修正

#21069で実装されていたのがいつの間にか動かなくなっていたので修正されたそうです。

class Dog
  include ActiveModel::Validations
  include ActiveModel::Validations::Callbacks

  attr_accessor :history

  def initialize
    @history = []
  end

  before_validation :set_before_validation_on_a, on: :a
  before_validation :set_before_validation_on_b, on: :b
  after_validation :set_after_validation_on_a, on: :a
  after_validation :set_after_validation_on_b, on: :b

  def set_before_validation_on_a; history << "before_validation on a"; end
  def set_before_validation_on_b; history << "before_validation on b"; end
  def set_after_validation_on_a;  history << "after_validation on a" ; end
  def set_after_validation_on_b;  history << "after_validation on b" ; end
end
d = Dog.new
d.valid?([:a, :b])
# 修正前
d.history #=> []
# 修正後
d.history #=> ["before_validation on a", "before_validation on b", "after_validation on a", "after_validation on b"]

つっつきボイス: 「やや、before/afterコールバックのon:オプションって初めて知ったけどこれは?」「on:はコンテキストを限定するのに使うやつですね: その条件が満たされるときだけコールバックされる」「なるほど~: if書きたくないマンにはうれしい機能」「条件が複雑になったらifで書かないと見落とすかもですね」

ActiveStorageで扱う添付ファイルの拡張子を追加

# activestorage/lib/active_storage/engine.rb
+    config.active_storage.content_types_to_serve_as_binary = [
+      "text/html",
+      "text/javascript",
+      "image/svg+xml",
+      "application/postscript",
+      "application/x-shockwave-flash",
+      "text/xml",
+      "application/xml",
+      "application/xhtml+xml"
+    ]

つっつきボイス: 「content dispositionって何でしたっけ」「ファイルをインラインで表示するかダウンロードダイアログを出すかの扱いっすね」「まさにコミットメッセージに書いてあった」

String.blank?のエンコーディングがUTF-16LEでエラーになるのを修正

ActiveSupportでStringクラスを開いて修正しています。

# activesupport/lib/active_support/core_ext/object/blank.rb#104
class String
   BLANK_RE = /\A[[:space:]]*\z/
+  ENCODED_BLANKS = Concurrent::Map.new do |h, enc|
+    h[enc] = Regexp.new(BLANK_RE.source.encode(enc), BLANK_RE.options | Regexp::FIXEDENCODING)
+  end

つっつきボイス: 「UTF-16ってASCII互換じゃないしエンディアンとかBOMとかサロゲートペアとかいろいろ残念で残念で: これを標準にしちゃったWindowsって(略」「出たな文字コード厨w」

参考: Wikipedia-ja: UTF-16

属性が見つからない場合の挙動を修正

# activerecord/lib/active_record/attribute.rb#234
+        def forgetting_assignment
+          dup
+        end

つっつきボイス: 「dirty save周りの修正っすね」

#25503のupdate_attributeの挙動がついに修正

昨年末のRailsウォッチで言及した#25503 update_attribute ignores autosave relationsが2年越しでついに修正されました。

# activerecord/lib/active_record/persistence.rb#405
-      if has_changes_to_save?
-        save(validate: false)
-      else
-        true
-      end
+      save(validate: false)
      end

つっつきボイス: 「例のGobyちゃんの作者のst0012さんが、このバグが直ってないって昨年落ち込んでました」「おお!これが修正されたということは、例のQiitaの定番記事『ActiveRecord の attribute 更新方法まとめ』のupdate_attributeの記述↓も修正してもらわないと」


ActiveRecord の attribute 更新方法まとめより


Goby: Rubyライクな言語(1)Gobyを動かしてみる

Rails

Rails.application.routes.url_helpersを直接呼ぶと遅い

# 直接呼んだ場合
Requests per second:    55.08 [#/sec] (mean)
Time per request:       18.155 [ms] (mean)
# モジュールをクラスにincludeした場合
Requests per second:    117.09 [#/sec] (mean)
Time per request:       8.540 [ms] (mean)

issue #23451 Performance Regression using url_routerとそれを修正するPR#24554 Memoize the RouteSet#url_helpers moduleが前から上がっていますがまだmergeされていません。それまではinclude Rails.application.routes.url_helpersする方が速いそうです。

# app/whatever/url_helper.rb
class UrlHelper
  include Singleton
  include Rails.application.routes.url_helpers
end

Rails 5.2でMySQLの降順インデックスをサポート(RubyFlowより)

# 同記事より
create_table "reports", force: :cascade do |t|
  t.string "name"
  t.integer "user_id"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["user_id", "name"], name: "index_reports_on_user_id_and_name", order: { name: :desc }
end

Railsのsystem testをRSpecから使う

# 同記事より
require 'rails_helper'

describe 'Homepage' do
  before do
    driven_by :selenium_chrome_headless
  end

  it 'shows greeting' do
    visit root_url
    expect(page).to have_content 'Hello World'
  end
end

つっつきボイス: 「この間takanekoさんに教えてもらった@jnchitoさんの記事↓の方がたいてい詳しいんですが、一応」

whatson: 今後開催予定のRubyカンファレンスを一覧表示するgem

もともとRuby Conferences (& Camps) in 2018 – What’s Upcoming?をご紹介しようと思っていたのですが、リンク先がアドベントカレンダーのせいか今日になって消滅していて、その情報源であるこのgemを見つけました。
私はうれしいですがほぼ誰得なgemですね。

$ gem install whatson
$ rubyconf
Upcoming Ruby Conferences:

  in 14d  Ruby on Ice Conference, Fri-Sun Jan/26-28 (3d) @ Tegernsee, Bavaria (near Munich / München) › Germany / Deutschland (de) › Central Europe › Europe
  in 20d  RubyFuza, Thu-Sat Feb/1-3 (3d) @ Cape Town › South Africa (za) › Africa
  in 28d  RubyConf India, Fri+Sat Feb/9+10 (2d) @ Bengaluru › India (in) › Asia
  in 55d  RubyConf Australia, Thu+Fri Mar/8+9 (2d) @ Sydney › Australia (au) › Pacific / Oceania
  in 62d  RubyConf Philippines, Thu-Sat Mar/15-17 (3d) @ Manila › Philippines / Pilipinas (ph) › Asia
  in 63d  wroc_love.rb, Fri-Sun Mar/16-18 (3d) @ Wrocław › Poland (pl) › Central Europe › Europe
  in 69d  Bath Ruby Conference, Thu+Fri Mar/22+23 (2d) @ Bath, Somerset › England (en) › Western Europe › Europe
  in 95d  RailsConf, Tue-Thu Apr/17-19 (3d) @ Pittsburgh, Pennsylvania › United States (us) › North America › America
  in 105d  RubyConf Taiwan, Fri+Sat Apr/27+28 (2d) @ Taipei › Taiwan (tw) › Asia
  in 111d  Rubyhack: High Altitude Coding Konference, Thu+Fri May/3+4 (2d) @ Salt Lake City, Utah › Southwest › United States (us) › North America › America
  in 133d  Balkan Ruby Conference, Fri+Sat May/25+26 (2d) @ Sofia › Bulgaria (bg) › Eastern Europe › Europe
  in 139d  RubyKaigi, Thu-Sat May/31-Jun/2 (3d) @ Sendai › Japan (jp) › Asia
  in 161d  RubyConf Kenya, Fri Jun/22 (1d) @ Nairobi › Kenya (ke) › Africa
  in 167d  Paris.rb XXL Conf, Thu+Fri Jun/28+29 (2d) @ Paris › France (fr) › Western Europe › Europe
  in 175d  Brighton Ruby Conference, Fri Jul/6 (1d) @ Brighton, East Sussex › England (en) › Western Europe › Europe
  in 305d  RubyConf, Tue-Thu Nov/13-15 (3d) @ Los Angeles, California › United States (us) › North America › America

    More @ github.com/planetruby/awesome-events

ついでに、元サイトがhttp://planetruby.herokuapp.com/というRuby情報クローラ的なサイトになっていました。

今年のリストでは、アフリカ大陸(南アフリカ共和国とケニア)でRubyカンファレンスが開催されるのが目につきました。ケニアのはその名もrubyconf.nairuby.orgです。ナイルビー。


http://rubyconf.nairuby.org/2018より

Rails公式ニュースにも掲載されている情報ですが、今年4月開催のピッツバーグのRailsカンファレンスCFPを募集だそうです(CFP: Call for Proposal)。


つっつきボイス:Bath Ruby Conferenceって何だ?っと思ったら、Bathはイギリスの地名なんだそうです」「水上温泉みたいな?」

Rails 5.2のdeprecation情報

ほとんど走り書きなので、5.2.0リリースまでに別途まとめようと思います。

「巨大なプルリク1件と細かいプルリク100件どっちがまし?」を考える(Hacklinesより)

今回たまたま見つけたHacklinesというRuby情報クローラが面白かったのでそこからいくつか記事を拾いました。


つっつきボイス: 「元記事にも貼られているこれほんに↓」「巨大なのがhorse-sizedで、こまいのがduck-sizedということみたいです」「コミットの粒度ってほんと悩みますね」「読まされるレビュアーの立場で考えるしかないかなー」

アセットのプリコンパイルを高速化するには(Hacklinesより)

「CDNを使う」「@importrequire_tree .を避ける」などの地道な方法が紹介されています。

マイグレーションを実行せずにSQLクエリを見たい(Hacklinesより)

同記事で、#31630 Allow to run migrations in check mode (dry run)というつい最近のPRが紹介されています。まだmergeされていません。


つっつきボイス: 「マイグレーションのdry run、たまに欲しくなりますよね」

ぼっち演算子&.の方がObject#tryよりずっと高速(Hacklinesより)

ベンチマークコードと結果はGistにあります。

#同Gistより
       user     system      total        real
      check for nil:  0.040000   0.000000   0.040000 (  0.040230)
   check respond_to:  0.100000   0.000000   0.100000 (  0.101780)
             rescue:  2.080000   0.020000   2.100000 (  2.103482)
 active_support try:  0.150000   0.000000   0.150000 (  0.151765)
    safe navigation:  0.040000   0.000000   0.040000 (  0.040369)

つっつきボイス: 「safe navigation operatorってぼっち演算子のことなのね」「後者はRubyでの俗称というかあだ名っぽいですね」
「ところでぼっち演算子って.&&.のどっちでしたっけw」「わかるーw: ワイもよく迷う」
「そういえばtry!ってどう違うんだったかな」「この記事↓翻訳したときにbabaさんに教えてもらったのを末尾に追加してあります: 『ぼっち演算子が#try!と少し異なるのは、引数付きだとnilのときに引数が評価されないという点です。』」「引数があるかどうかで違う、と」

Railsの`Object#try`がダメな理由と効果的な代替手段(翻訳)

追伸: 体操座りしながら指でいじいじしている形で覚えるとよいそうです。

belongs_to関連付けクエリのリファクタリング(RubyFlowより)

「ここではスコープよりクラスメソッドの方が自分にはわかりやすかったから」だそうです。

# 同記事より
class Job < ActiveRecord::Base
  belongs_to :category

  def self.publishable
    joins(:category).merge(Category.publishable)
  end
end

Job.publishable

他にEncapsulating queries in a Rails Modelという記事もありました。


つっつきボイス: 「スコープかクラスメソッドか」「scopeは最後のリファクタでそれっぽければやればいい気がする: デフォルトはclassメソッドでいーんじゃないかな?」

マイクロサービスはチームを苦しめる(Hacklinesより)


つっつきボイス: 「記事にあったコンウェイの法則ってこれですね↓」「うんうん、官僚組織のシステムはやっぱり官僚っぽい設計になるし」

Conwayの法則とは,“組織の設計するシステムには … その組織のコミュニケーション構造をそのまま反映した設計になるという制約がある”,というものだ。つまり,チームの開発成果がその組織の内部的なコミュニケーションのあり方によって決まる,という意味である。
Conwayの法則に従った組織の成長より

参考: クックパッドとマイクロサービス — Conwayの法則に言及しています

「ところでコンウェイっていうとライフゲームの英語名Conway’s Game of Lifeを思い出しちゃいます(年バレ!)」

flag_shih_tzu: Integerカラムにビットパターンでフラグを追加するgem(Hacklinesより)

# 同記事より
class Spaceship < ActiveRecord::Base
  include FlagShihTzu

  has_flags 1 => :warpdrive,
            2 => :shields,
            3 => :electrolytes
end

shih tzuって何だろうと思ったら、中国産の犬種「西施犬」のようです。フラグとどう関連するのかは謎です。

ついでに、元記事タイトルは「博士の異常な愛情 または私は如何にして心配するのを止めて水爆を愛するようになったか」のもじりですね。

Ruby trunkより

早くもRubyに大量のコミット

PB memoさんのRubyコミット日記です。年明け早々に追いきれないほどのコミット大漁節です。


つっつきボイス: 「みんな冬休み取ったー?w」

そういえば以下の記事で、今後Rubyのリリース日が前倒しになるかもしれないという構想が語られていました。

また、クリスマスリリースはプレゼントという意味があるものの、家族を持つコミッターが増えてきたため、「少し前の22日や23日にしたほうがよいかもしれない」と語った。

Integer#powの法(modulo)が巨大な場合の結果がおかしい->修正

以下は12が正しいそうです。

irb(main):020:0> 12.pow(1, 10000000000)
=> 1
irb(main):021:0> 12.pow(1, 10000000001)
=> 1
irb(main):022:0> 12.pow(1, 10000000002)
=> 1

beginなしでdo-endブロックでrescue

1年前の変更なので2.5には反映済みです。

lambda do
  begin  #<= これがなくてもいいようになった
    raise 'err'
  rescue
    $! # => #<RuntimeError: err>
  end
end.call

つっつきボイス: 「自分もこのbeginなくていいと思う」「matzがためらいがちにacceptしてました↓」

Although I am not a big fan of this syntax, mostly because I don’t like fine grain exception handling.
But I found out many developers prefer the syntax. After some consideration, I decided to accept this.

SymbolとStringの違いに関するRDocを追加

/* 定数名、メソッド名、変数名はシンボルとして返される
*
*     module One
*       Two = 2
*       def three; 3 end
*       @four = 4
*       @@five = 5
*       $six = 6
*     end
*     seven = 7
*
*     One.constants
*     # => [:Two]
*     One.instance_methods(true)
*     # => [:three]
*     One.instance_variables
*     # => [:@four]
*     One.class_variables
*     # => [:@@five]
*     global_variables.last
*     # => :$six
*     local_variables
*     # => [:seven]
*
* Symbolオブジェクトは識別子を表す点がStringオブジェクトと異なる
* Stringオブジェクトはテキストやデータを表す
*/

つっつきボイス: 「この間この記事↓を公開した後の変更なので取り上げてみました」


Rubyのシンボルをなくせるか考えてみた(翻訳)

Ruby

Fukuoka Ruby Awardエントリー募集(1/31まで)(Ruby公式ニュースより)


www.ruby-lang.orgより

Ruby 3とJIT(Ruby Weeklyより)

Noah Gibbsさんの記事です。Optcarrotがoptimized modeで相当速くなっています。


engineering.appfolio.comより

Ruby 2.5のベンチマーク取ってみた

HexaPDFを使っています。


gettalong.orgより

Kernel#itselfにRubyの美学を見た

短い記事です。

# 同記事より
collection.each_with_object({}) { |item, accum| accum[item] = accum[item].to_i + 1 }
# ↓ここまで簡潔に書ける
collection.group_by(&:itself).transform_values(&:count)

RubyにCコード書いてメモリ共有してみた

# 同記事より
require 'inline'
class CHello
  inline do |builder|
    builder.include '<stdio.h>'
    builder.c 'int sumThem() {
      return 2 + 2;
    }'
  end
end

>> CHello.new.sumThem #=> 4

つっつきボイス: 「RubyコードにまるっとCのコードがインラインで埋まっているんですよね」「これマジ凄くない?C拡張より楽チンそう」「rubyinlineでできるみたいです」「メモリ共有にはFiddle::Pointerを使ってるそうです」

「この記事にはネタ画像がいくつか埋まってるんですが、その中でもこれ↓: シャイニングっていう昔のくっそ怖い映画の一番有名なシーンなんですが」「なんか見たことあるっちゃある感じ」


blog.rebased.plより

「この『Here’s Johnny!!』っていうセリフは、実はこの場面までの緊張感を一発で台無しにする、英語圏のこの年代の人じゃないとわからないずっこけネタなんですね」「Tonight Showという米国の長寿テレビ番組のオープニングで司会者が必ず言うセリフなんですが、日本に置き換えるとさしずめ『サザエでございま~す』とか『ぼーくドラえもん』っていう感じ: そこでそれ言うか!みたいな」

JRubyより速い


つっつきボイス: 「今見てみると2.5ががくっと遅くなってますね」「何かつっかえてるのかな?」

卜部さんの「HashDoS脆弱性との戦い」

Ruby実装の命名の由来

これもNoah Gibbsさんの記事です。

RubyBench: RubyやRailsのベンチマークサイト


rubybench.orgより


つっつきボイス:https://speed.python.org/みたいなのがRubyにもないかなと思って探したら見つかりました: 相当細かくベンチ取ってくれて楽しい」


rubybench.orgより

Ruby開発者のための5つの習慣(Ruby Weeklyより)


  1. RuboCopはいつどんなときでもかけろ
  2. git historyを汚すな
  3. お遊びプロジェクトを立ち上げてみろ
  4. Railsのソースコードを読め
  5. Railsガイドを「もう一度」読め

つっつきボイス: 「5…」「5…」

unlessのご利用は控えめに(Hacklinesより)

# 元記事より
# Example 1
unless something?
  # do something
else
  # do other thing
end

# Example 2
unless something? || another_thing?
  # do something
end

つっつきボイス:unless自体はいいけど確かにelseと一緒に使うとか勘弁w」

RubyとPythonのmixinを比較する(Hacklinesより)

みっちり長い記事です。

# 同記事より
class RunnerMixin:
    def max_speed(self):
        return 4


class SortaFastHero(RunnerMixin):
    """This hero can run, which is better than walking."""
    pass


class SortaFastMonster(RunnerMixin):
    """This monster can run, so watch out!"""
    pass

つっつきボイス:endがないと、どうもパンツ履き忘れたような気持ちになってw」「Pythonコードってブラウザからコピペしたはずみでインデント消えちゃったり」「それはコピペするなということかも」

地味すぎて誰も気がついていないCRuby 2.5の新機能

mruby/c1.1 RC2リリース

  • Procクラスの実装
  • sprintfメソッドの実装
  • .classメソッドの実装
  • RangeObjectのリファレンスカウント対応
  • StringObjectのバグ修正
  • 重複した数値処理の排除
  • Rubyによるクラスの定義とインスタンスメソッドの定義を実装

各種言語のハッシュマップ実装を比較

  • Python
  • Ruby
  • Java
  • Scala
  • Golang
  • C#
  • C++

この記事のサイドバーにあったNo Magic: Regular Expressionsという記事もつい気になってしまいました。

Graphql-batchとPromise.rb

Graphql-batchはShopifyのリポジトリですね。内部でPromise.rbを使っているそうです。


つっつきボイス: 「↓こんな感じでRubyでPromiseできるみたいです」

# lgierth/promise.rbより
require 'promise'

Promise.new
  .tap(&:fulfill)
  .then { Promise.new.tap(&:fulfill) }
  .then { Promise.new.tap(&:reject) }
  .then(nil, proc { |reason| p reason })

JavaScript: 5分でわかるPromiseの基礎(翻訳)

asakusa.rb新年会

現時点でまだ空席あるようです。

KitchenCI: 複数プラットフォームをサポートするCIサービス


kitchen.ciより

driver:
  name: vagrant

provisioner:
  name: chef_zero

platforms:
  - name: ubuntu-14.04
  - name: windows-2012r2

suites:
  - name: client
    run_list:
      - recipe[postgresql::client]
  - name: server
    run_list:
      - recipe[postgresql::server]

Chefが前提のようです。

SQL

DB設計カタログサイト


つっつきボイス: 「確かにこれ凄い!」「医療とかホテルとか、よくここまで集めた」「実用に即したDB設計ってなかなか見る機会ないですよね」

PostgreSQLが「DBMS of the year 2017」に輝く(Postgres Weeklyより)

https://db-engines.com/en/rankingというランキングサイトを元にしています。


db-engines.comより


つっつきボイス: 「MongoDBとかってDBMSなんですかね?」

JavaScript

面接で聞かれるES6理論クイズ10問(解答付き)

500人以上の技術面接で使われた問題だそうです。

  1. JavaScriptのスコープを説明し、スコープの例を知っている限り列挙せよ(6点)
  2. ホイスティングを例を挙げて説明せよ(6点)
  3. prototypeの役割を例を挙げて説明せよ(6点)
  4. 3の例を拡張してprototypeの継承を説明せよ(5点)
  5. 3の例をES6構文で書き直せ(6点)
  6. thisの値を説明せよ(6点)
  7. コンテキストバインディングを例を挙げて説明せよ(3点)
  8. =====の一般的な違いを説明せよ(6点)
  9. 変数がarrayかどうかをチェックする方法を述べよ(3点)
  10. 以下のコードのどこがおかしいかを説明し、修正せよ(4点)
if ( typeof x === 'object' ) {
    x.visited = true;
}

rearmed-js: JavaScriptのArrayなどをRuby風に書けるライブラリ

// westonganger/rearmed-jsより
var array = [];
var cb = function(val, i){ };
array.any(cb=null) // returns bool
array.all(cb=null) // returns bool
array.compact(badValues=[null, undefined, '']) // returns array, accepts array or splat arguments
array.dig(*args) // returns value, accepts splat arguments or array
array.each(function(val, i){ })
...

Sinon.js: JavaScriptでmockやstubを使うライブラリ

テスティングフレームワークに依存しないそうです。

// sinonjs.orgより
it("returns the return value from the original function", function () {
    var callback = sinon.stub().returns(42);
    var proxy = once(callback);

    assert.equals(proxy(), 42);
});

NectarJS: JSコードをネイティブバイナリにコンパイル(JavaScript Liveより)

WebAssemblyにも対応しているそうです。

via GIPHY

JavaScriptオブジェクトのrest/spreadプロパティ(JavaScript Liveより)

// 同記事より
const style = {
  width: 300,
  marginLeft: 10,
  marginRight: 30
};

const { width, ...margin } = style;

console.log(width);  // => 300
console.log(margin); // => { marginLeft: 10, marginRight: 30 }

JavaScriptのhoistingを理解する(JavaScript Liveより)


medium.com/@thamizhchelvan2005より

JavaScriptの「obfuscation」とは何か(JavaScript Liveより)

obfuscationは、いわゆるminifyやuglifyより徹底的に変換をかけています。

// 同記事より
function hello(name) {
console.log('Hello, ' + name);
}
hello('New user');

// obfuscation後
eval(function(p,a,c,k,e,d){e=function(c){return c};if(!''.replace(/^/,String)){while(c--){d=k||c}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k)}}return p}('3 0(1){2.4(\'5, \'+1)}0(\'7 6\');',8,8,'hello|name|console|function|log|Hello|user|New'.split('|'),0,{}))

tui.editor: 表やグラフも扱えるMarkdownエディタ(JavaScript Liveより)

表やUML図などを直接扱えるようです。


nhnent/tui.editorより

CSS/HTML/フロントエンド

HTML 5.2の新着情報とポイント(jser.infoより)


  • <dialog>要素
  • Apple製品でのアイコン表示改善
  • <main>要素を複数持てる
  • <body>タグ内にも<style>を書ける(ただしパフォーマンス上おすすめしない)
  • <legend>タグ内に見出しタグを置ける
  • 廃止: keygenmenumenuitem、厳密なDOCTYPE
  • etc

CSSの:notセレクタを導入


つっつきボイス::notときどき使いますヨ: 繰り返し要素の最後のところだけ区切り線入れたくないときとか便利」「そうそう、これないと不便」

その他

技術トークの5つのコツ


reverentgeek.comより

  • その技術を選んだ理由を話す
  • その技術で何ができるかを話す
  • どうやったら動いたかをデモする(しくじったポイントも入れよう)
  • 参考リンクを忘れずに
  • マイクはないものと思え

meltdown: メルトダウン脆弱性の実演コード(GitHub Trendingより)

今旬のネタだけあって、10日ほどで★2400超えです。

これマジで?

Go 1.10 Beta2リリース

番外

闇深そうなフォントかるた

ケンブリッジ大学の脳力測定サイト

いわゆる脳トレ的なやつです。

成功の秘訣は「大学の町の近くで育つこと」?

日本語記事: 元グーグルのデータサイエンティストが発見! 成功者の意外な共通点とは

340刷

ロシアのサーバールームお祓い事情


つっつきボイス: 「サーバールームで水撒くか普通…」

AIで転職情報を勝手にかき集めるのは…


つっつきボイス: 「この人に目をつけられたらもう逃げられないっすね」

闇落ち以外のパターンが思いつかない


今週は以上です。

バックナンバー(2017年後半)

週刊Railsウォッチ(20171222)定番gemまとめサイト、active_record-mtiでテーブル継承、PostgreSQL 10の非互換変更点、Railsガイド攻略法ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやRSSなど)です。

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Github Trending

160928_1701_Q9dJIU

画面デザインpart1: 画面UIの解剖 —構成要素と操作(翻訳)

$
0
0

こんにちは、hachi8833です。本シリーズ(全3回)は主にデザイナーを想定していますが、フロントエンド開発者、フルスタック開発者、HTMLコーダーにも有用な内容です。

概要

原著者の許諾を得て翻訳・公開いたします。

画面デザインpart1: 画面UIの解剖 — 構成要素と操作(翻訳)

本記事は、「デザインシステム」および画面UIのデザインや開発におけるデザインシステムの重要性について解説するシリーズ記事のpart 1です。

解剖学(anatomy)は、生き物を部分に切り分けて構造を記述する学問です。画面UIも生き物と同じように、誕生(プロジェクト発足)、成長(UIデザイナーがユーザーに関する知識を蓄える)、死(もっと新しいパターンへの置換え)というライフサイクルをたどります。そこで、画面UIデザインを生き物に見立てて「解剖」し、主要な構成要素を取り出してみましょう。

ユーザーが目にするデジタル画面は、本質的に(color)、タイポグラフィ(typography: フォントのこと)、空間(spacing)、アイコン(iconography)(ない場合もある)の4つのパーツで構成されます。

訳注: ここで言うアイコンには写真やイラストなどの画像は含めていないと考えられます。

もちろんサウンドや奥行き、動き(モーション)といったパーツもありますが、もっとも重要なのは上の4つであり、UI要素でこれらをまったく使わないことはありえません。したがって、こうしたパーツをデザイン作業の初期段階でしっかり決めておけば、新しいUI/UXを生み出してデザイナー自身のブランディングを高めるより大きなチャンスにもつながります。

Submitボタン

上の図をよくご覧ください。ただのシンプルなボタンです。これをパーツに分解してみましょう。

ボタンの構成

まず、各パーツがどのように協調しているか、じっくりとご注目ください。続いて、画面サイズにかかわらず各要素の配置を柔軟かつレスポンシブに構成するために、要素を空間に正確に配置するシステムを導入します。

私は8ポイントのグリッドシステムを使っているので、ブロック要素やインライン要素のサイズ、パディング、マージンはすべて8の倍数で定義されます。こうすることで、レイアウトが自然にまとまります。詳しくはElliot Dahiの良記事をご覧ください。

次はスマホで何かお気に入りのアプリをひとつ開いてみましょう。上で解説した4つのパーツがどのUIにもあることがわかると思います。デザインがこのように統一されるのは、ページをコーディングする開発者はユーザーが閲覧するすべてのパーツをCSSで宣言しなければならないからです。

デザイナーはこの点を理解しておくことが重要です。というのも、大半のデザイナーがこうした開発上の重大なポイントを認識しておらず、値が不揃いなままのデザイン仕様を開発者に丸投げすることがよくあるからです。このままではUIの一貫性が損なわれ、ユーザーの印象も散漫になってしまいます。

あなたのデザインをコーディングする開発者は、渡されたデザインを100%忠実に再現しようとしていることを常に念頭に起きましょう。画面UIが散漫になってしまったのであれば、それはコーダーの責任ではなく、デザイナーの責任です。

フロントエンド開発者は例外なくpaddingmarginfont-sizefont-weightcolorbackground-colorなどをマークアップで宣言しなくてはなりません。だからこそ、デザイナーがこうした値をシステムに沿って統一的に宣言すれば、フロントエンド開発者にとって非常に大きな助けになります。私はそのために「デザイントークン(Design Token)」と呼ばれる手法をデザイン作業に導入しており、かつすべてのコンポーネントで使われるSassファイルも作成するようにしています。デザイントークンについては後日記事にする予定なので、それまではこちらの記事をご覧ください。

まとめ: デザイナーは、画面UIを外部の作業者に展開できる何らかのフレームワーク(訳注: 本記事では「決めごと」を指します)を導入し、それを用いて画面UIを構成することを始めてみましょう。色相(hue)、フォントスタイル、アイコンなどの値はいつでも自由に変更できますが、そうした変更には常にフレームワークを適用すべきです。このフレームワークを「デザインシステム」と呼びます。

デザインシステム(名): 成果物の1種であり、さまざまなメディア出力の調和を保つために、UXやデザイン上の決定を統一された形式でやりとりする、明確なガイドラインを指す。

Part 2ではデザインシステムについてより深く掘り下げ、デザインシステムをどのように構成するかについて解説します。Part 3では、デザインシステムをデザイナーと開発者で共有・展開する方法について解説します。

新しいRailsフロントエンド開発(3)Webpackの詳細、ActionCableの実装とHerokuへのデプロイ(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

新しいRailsフロントエンド開発(3)Webpackの詳細、ActionCableの実装とHerokuへのデプロイ(翻訳)

前書き

本記事は、フロントエンドのフレームワークに依存しないRailsプレゼンテーションロジックを現代的かつモジュール単位かつコンポーネントベースで扱う方法を独断に基いて解説するガイドです。3部構成のチュートリアルで、例を元に最新のフロントエンド技術の最小限に学習し、Railsフロントエンド周りをすべて理解しましょう。

Part 2までのおさらい

こちらもお読みください:

Part 2までに、「コンポーネント」アプローチを用いてチャットアプリの骨格を組み立てました。各コンポーネントは、アプリのfrontend部分の内部のフォルダとして表現されており、それぞれが.erbパーシャル、.cssスタイルシート、.jsスクリプトの3つのファイルで構成されています。現時点でJavaScriptコードに含まれているのは、ネストしたコンポーネントを読み込むためのimport文だけです。これによってすべてのパーツがapplication.jsのエントリポイントとして含まれるようになり、Webpacker gemでこれらをまとめてCSSやJSのバンドルをビルドできるようになっています。

今回のチュートリアルの最後の章では、JavaScriptを用いてチャットが動くようにする予定です。公式のRailsドキュメントは未だにSprocketsやCoffeeScriptが前提になっているため、ActionCableをES6モジュールから用いる方法についても解説します。

「sprockets抜き」アプリが完成したら、Herokuにデプロイします。

完成版のEvil Chatアプリのコードをすぐにもご覧になりたい場合はGitHubのリポジトリをどうぞ。

ご存知かと思いますが、ActionCableの理解はそれほど簡単ではありませんので、できるだけ手順ごとに動作を明示的に解説してみます。経験豊富な開発者の知性を過小評価する意図はありませんのでご了承ください。途中でActionCableを十分理解できた方は、解説をスキップしてコードスニペットまで進めてください。コードスニペットは通常のSprockets実装と異なっているため、Railsガイド(訳注: 英語版Edgeガイドです)のコード例はWebpackで動作しません。

ActionCableのRuby部分

まずは、チャットのチャンネルの生成が必要です。

$ rails g channel chat

これでapp/channels/の内部にchat_channel.rbというファイルが作成されます。

ActionCableはRailsでWebSocketsと統合されており、サーバー側のロジックをRubyで書き、クライアント側のロジックをJavaScriptで書くことができます。ActionCableのクールな点は、ブラウザ上で実行されるJavaScriptから、サーバー側のRubyメソッドを呼び出せることです。chat_channel.rbはチャット用のメソッドを定義する場所であり、全登録ユーザーのデータのストリーミング(本チュートリアルの場合、新しいメッセージでDOMを更新する少量のHTMLです)も担当します。

チャンネル固有の機能を扱う前に、ActionCableが認証済みユーザーのみをブロードキャストすることを担保する必要があります。アプリ作成時に生成したapp/channels/application_cableフォルダの内部を見ると、WebSockets認証を担当するconnection.rbファイルがあります。Part 2の認証が非常にシンプルだったことを思い出しましょう。sessionハッシュ内に単にusernameキーを作成し、ユーザーがどんなusernameでも使えるようになっていました。以下は今回必要なコードです。

# app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = request.session.fetch("username", nil)
      reject_unauthorized_connection unless current_user
    end
  end
end

ここではセッションからusernameを取り出そうとしています。usernameがない場合、接続を拒否します。実際には、新しいユーザーは「log in」画面を経由するまでActionCableのブロードキャストを受け取りません。

続いてchat_channel.rbに手を加えます。

# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat"
  end

  # サーバーがメッセージ形式のコンテンツを受け取ると呼び出される
  def send_message(payload)
    message = Message.new(author: current_user, text: payload["message"])
    if message.save
      ActionCable.server.broadcast "chat", message: render(message)
    end
  end

  private

  def render(message)
    ApplicationController.new.helpers.c("message", message: message)
  end
end

subscribedメソッドは接続が認証されると呼び出されます。stream_fromは、「chat」チャンネルでブロードキャストされるどんなメッセージでもクライアントに到達できるということを表します。

このsend_messageメソッドは最も興味深い部分です。今はアプリのRuby部分の内部なので、ActiveRecordと直接やり取りできます。私たちのシンプルな例では、「メッセージを1件送信する」というのは、Messageモデルの新しいインスタンスを1つ作成してデータベースに保存し、authortextが正しく設定されたmessageパーシャルをレンダリングして、生成されたHTMLを「chat」チャンネルでブロードキャストするということを意味します。

ここでご注意いただきたいのは、app/channelsの内部からはApplicationControllerrenderメソッドにも、コンポーネントをレンダリングするカスタムcヘルパーにも直接アクセスできないという点です。そこで、ヘルパーを間接的に呼び出す別のrender定義を作成します。そのために、ApplicationControllerのインスタンスを1つ作成して、ApplicationHelperモジュールで定義したヘルパーにアクセスします。今私たちが関心を抱いているのはcヘルパーなので、ApplicationController.new.helpers.cでアクセスします。

ActionCableのJavaScript部分

純粋なrails newで生成したRails 5.1アプリでは、ActionCableのクライアント部分(JavaScriptで記述されている)はアセットパイプラインでインクルードされます。私たちがapp/assetsを削除したときに、この標準的な実装も効率よく取り除かれていますので、ActionCableのJavaScriptライブラリを再度インストールする必要があります。今度はYarn経由でnpmからインストールします。

$ yarn add actioncable

さて、WebpackでActionCable(あるいは別のJavaScriptライブラリ)を用いる場合の特別な点とは何でしょうか?

Sprocketsを使うと、JavaScriptファイルが結合後に共通のスコープで共有されたものを扱うことになるため、this.jsで宣言されたものは何であってもthis.jsが事前に読み込まれていればその後のthat.jsからアクセスできます。Webpackはこの点が違っており、より抑制の効いたアプローチを採用しています。Ross Kaffenbergerの良記事から引用します。

これは、ブラウザでのJavaScriptバンドル方法のパラダイムがSprocketsとWebpackで根底から異なっていることを理解するうえで役立ちます。
この違いは、Webpackの動作の中核部分にあります。Webpackでは、SprocketsのようにJavaScriptコードをグローバルスコープで結合するのではなく、個別のJavaScriptモジュールをクロージャ経由で個別のスコープに仕切っているので、モジュール間のアクセスをimport経由で宣言することが必須になります。これらのJavaScriptモジュールは、デフォルトでは一切グローバルスコープには公開されません。

私たちはES6のexport文やimport文を多用しなければならなくなります。しかし私たちは、最初にfrontend内にclientフォルダを作成しています。ActionCableの(JavaScript)クライアントはここに置きます。

$ mkdir frontend/client
$ touch frontend/client/cable.js

cable.jsは、「cable」コネクションのconsumerインスタンスの作成に使われます。Sprocketsで書かれた標準的なRailsサンプルでは、これはグローバルなAppオブジェクトの一部として作成されるのが普通です。公式のActionCableドキュメントや世にあまたあるチュートリアルでは次のようなコードが使われています。

// これはコピペしてはいけません!
(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer();
}).call(this);

このコード例を、私たちのモジュールベースのシステムに合わせて調整する必要があります。また、consumerが作成済みの場合には既存のコネクションを再利用してcreateConsumer関数の再呼び出しを避けたいと思います。そのためにグローバルなwindow変数を使いたくないので、別のアプローチを採用します。私たちのcable.jsモジュールは、コネクションのインスタンスをconsumer内部変数に保持し、createChannel関数をexportします。この関数は既存のconsumerをchatチャネルにサブスクライブするか、新しいconsumerインスタンスを1つ作成します。それではコードをcable.jsに書いてみましょう。

// frontend/client/cable.js
import cable from "actioncable";

let consumer;

function createChannel(...args) {
  if (!consumer) {
    consumer = cable.createConsumer();
  }

  return consumer.subscriptions.create(...args);
}

export default createChannel;

createChannel関数は汎用なので、consumerを特定のチャンネルにサブスクライブしたいどんな箇所からでも正しい引数を与えて使うことができます。したがって、サーバー側のchat_channel.rbのRubyコードに対応するクライアント側JavaScriptコードとなるファイルが別途必要になります。このファイルをchat.jsと呼ぶことにしましょう。

$ touch frontend/client/chat.js

コードは次のとおりです。

// frontend/client/chat.js
import createChannel from "client/cable";

let callback; // 後で関数を保持するための変数を宣言

const chat = createChannel("ChatChannel", {
  received({ message }) {
    if (callback) callback.call(null, message);
  }
});

// メッセージを1件送信する: `perform`メソッドは、対応するRubyメソッド(chat_channel.rbで定義)を呼び出す
// ここがJavaScriptとRubyをつなぐ架け橋です!
function sendMessage(message) {
  chat.perform("send_message", { message });
}

// メッセージを1件受け取る: ChatChannelで何かを受信すると
// このコールバックが呼び出される
function setCallback(fn) {
  callback = fn;
}

export { sendMessage, setCallback };

この部分は難解なので、説明のテンポを落としてじっくり見てみましょう。
細かな動作は次のようになっています。

  • cable.jsからcreateChannel関数をimportします。
  • この関数に2つの引数を与えて呼び出します。チャンネルの名前(Rubyのsome_channelのような名前はJavaScriptではSomeChannelとし、両者の命名慣習を壊さないようにしなければならない点に注意)と、ActionCableの標準コールバック(connecteddisconnectedreceived)を定義するオブジェクトです。ここで必要なのはreceivedコールバックのみです。このコールバックは、ブロードキャストされたデータをJavaScriptオブジェクトの形式として引数として持つチャンネルブロードキャストをconsumerが受け取ると呼び出されます(RubyとJavaScriptオブジェクトの変換はRails自身が行います)。
  • ここから少々ややこしくなります。messageオブジェクトを受信したら、何らかの関数を呼び出す必要があります。コンポーネントのこの部分は、必要に応じてDOMを扱う方法を責務上知っていなければならないので、この関数をここで定義したくありません。そこで、setCallbackという汎用的な関数を1つ作成します。この関数は、正しいコンポーネントから呼び出されると、メッセージ受信後に呼び出したいコンポーネント固有のあらゆる関数を保存するcallback変数を変更します。
  • sendMessageは、コネクションインスタンスのperformメソッドを呼び出します。ここはActionCableの最も魔術的な部分であり、JavaScriptからRubyのメソッドを呼び出します。これはchat_channel.rbからsend_messageメソッドをトリガして、messageオブジェクトを引数として渡します。この{ message }という記法は、ES6の{ message: message }のショートハンドです。ここではペイロードがmessageキーの下にあることを前提としています。このコンテキストにおける「message」は、メッセージフォームに含まれるユーザー(visitor)の種類を表す単なるテキストです。
  • 最後に、モジュールからsendMessagesetCallbackを両方ともexportし、後でコンポーネントで使えるようにします。

明確なメッセージを1件送信する

それでは最初にメッセージの送信を扱いましょう。この責務を引き受けるべきコンポーネントはどれでしょうか?Part 2では、個別のメッセージ用にmessageコンポーネントを、メッセージのリスト用にmessagesコンポーネントを、テキストの送信にはmessage-formを使いました。ブルーの大きな「Send」ボタンはmessage-formの内部にあるので、ここに置くのが正解です。frontend/components/message-form/message-form.jsのコードを変更しましょう。

// frontend/components/message-form/message-form.js

// client/chat.jsからsendMessageをimportする必要がある
import { sendMessage } from "client/chat";
import "./message-form.css";

const form = document.querySelector(".js-message-form");
const input = form.querySelector(".js-message-form--input");
const submit = form.querySelector(".js-message-form--submit");

function submitForm() {
  // sendMessageを呼び出し、その結果Rubyのsend_messageメソッドが呼ばれて
// ActiveRecordでMessageインスタンスが作成される
  sendMessage(input.value);
  input.value = "";
  input.focus();
}

// コマンドキー(またはCtrlキー)+Enterでメッセージを送信できる
input.addEventListener("keydown", event => {
  if (event.keyCode === 13 && event.metaKey) {
    event.preventDefault();
    submitForm();
  }
});

// ボタンをクリックして送信してもよい
submit.addEventListener("click", event => {
  event.preventDefault();
  submitForm();
});

動作を確認しましょう。もう一度サーバーを起動して認証し、メッセージボックスに適当なテキストを入力してコマンド+Enterキーを押し、Railsログを調べると次のように表示されます。

chat_channelの最初のブロードキャスト

chat_channelの最初のブロードキャスト

これで、フォームを送信すると、バックエンドでMessageインスタンスが新たに1つ作成され、メッセージのパーシャルが生成されてActionCableですべての登録ユーザーにブロードキャストされます。残るは、HTMLで受け取った文字列をDOMに挿入してページに表示するだけです。

受信したメッセージ

新しいメッセージをその都度動的にページに挿入する責務を負うのはmessagesコンポーネントです。元々このコンポーネントはデータベース内のすべてのメッセージをレンダリングする責務を負っていることがその理由です。

ここで行う必要があるのは、chat.jsモジュールのsetCallback関数を呼び出して、ブロードキャストされたメッセージを引数として受け取る別の関数に渡すことだけです。もう一度おさらいしましょう。chat.jsモジュールは、chatチャンネルで何かがブロードキャストされると、常にreceivedイベントに対して何か操作を行える状態になりますが、正確な操作については(明示的に示すまでは)関知しません。これを行うには、実行したい関数をsetCallbackに渡します。

messages.jsの新しいコードは次のとおりです。

// frontend/components/messages/messages.js
import { setCallback } from "client/chat";
import "components/message/message";
import "./messages.css";

const messages = document.querySelector(".js-messages");
const content = messages.querySelector(".js-messages--content");

function scrollToBottom() {
  content.scrollTop = content.scrollHeight;
}

scrollToBottom();

// ActionCableで新しいメッセージを1件受け取るたびに
// このコード片を呼び出すよう`chat.js`に伝える
setCallback(message => {
  content.insertAdjacentHTML("beforeend", message);
  scrollToBottom();
});

ここでchat.jsモジュールに渡しているのは、メッセージのリストを上にスクロールして、新しいメッセージのHTMLを下に追加するだけのシンプルな関数です。これで、2種類の異なるブラウザを立ち上げて、それぞれ別のニックネームでログインしてチャットしてみると、以下のようにすべて正常に動作していることがわかります。

異なるブラウザで動作するチャット

異なるブラウザで動作するチャット

Herokuにデプロイする

いよいよアプリをHerokuにデプロイして、本番環境でもチャットできることを確認しましょう。最初にHerokuアカウントを用意し、自分のPCにHeroku CLIがインストールされていることを確認します。これでターミナルでherokuコマンドが使えるようになります。

アプリのデプロイを準備するうえで必要な点がいくつかあります。

最初に、既存のProcfilerails serverwebpack-dev-serverの実行に使われる)をProcfile.devに変更します。devなしのProcfileはHerokuで使います。また、本番環境ではwebpack-dev-serverが実行されないようにしたいと思います。

Procfile.devは次のようになります。

server: bin/rails server
assets: bin/webpack-dev-server

メインのProcfileにはserver行だけを残します。

server: bin/rails server

注意: この変更を行った後でアプリをlocalhostで実行したい場合は、hivemind Procfile.dev(使っているプロセスマネージャによってはovermind s -f Procfile.devforeman run -f Procfile.devなど)で起動する必要があります。

次に、ビルドタスクがHeroku側で認識されるようにする必要があります。

RubyアプリをプッシュしていることがHeroku側で認識されると、assets:precompileを起動しようとします。これはアセットパイプラインでアセットをビルドするのに昔から使われているタスクです。しかしWebpackerを使う場合は、別のyarn:installタスクとwebpacker:compileタスクを呼び出す必要があります。

最新バージョンのRailsとWebpacker(3.2.0)は、Sprocketsを無効にしてあってもassets:precompileでSprocketsを起動できます(試しにローカルでbundle exec rails assets:precompileを実行してみると、パッケージがビルドされてpublicフォルダに置かれる様子を見ることができます)。

ただし本記事執筆時点では、Rails 5.1.4とWebpacker 3.2.0による「Sprockets抜き」アプリのHerokuでのビルドは失敗しました。Vladimir Dementyevのおかげで回避方法がわかりました。Rakefileで明示的にassets:precompileを定義する必要があります。

# Rakefile
require_relative 'config/application'

# この行を追加
Rake::Task.define_task("assets:precompile" => ["yarn:install", "webpacker:compile"])

Rails.application.load_tasks

RailsとWebpackerのコントリビューターは現在も本番環境でのアセットのビルドをできるだけ楽にする最善の方法を模索中なので、この部分は将来変更される可能性があります。すべてが落ち着いて、追加のハックなしでHerokuでアプリをビルドできるようになれば理想です。

また、HerokuでActionCableを動かすためには本番でRedisを有効にする必要もあります。Gemfileのgem 'redis', '~> 3.0'のコメントを解除してください(注意: バージョン4はRails 5.1のActionCableで認識されません: 5.2で修正予定)。

config/cable.ymlproductionに、urlの正しい設定が含まれていることを確認します。

development:
  adapter: async

test:
  adapter: async

production:
  adapter: redis
  url: <%= ENV["REDIS_URL"] %>
  channel_prefix: evil_chat_production

REDIS_URL環境変数に正しいRedisサーバーのURLを設定するために、Heroku Redisアドオンを使います。

そして最後に、config/environments/production.rbに以下の行を追加してください。

config.secret_key_base = ENV["SECRET_KEY_BASE"]

secrets.ymlをソースコントロールにコミットしない場合は、この行が必要です(ただしRailsの「encrypted secrets」を設定していない場合はこの行を追加すべきではありません)。

ついにデプロイ準備ができました。

$ heroku create YOUR_APP_NAME
$ heroku addons:create heroku-redis:hobby-dev

数分後にHeroku Redisアドオンが作成されたら(heroku addons:infoでステータスを確認できます)、次を実行します。

$ git add . && git commit -m "prepare for deploy"
$ git push heroku master

アプリのビルドが完了したら、heroku run rails db:migrateを実行してproductionのデータベースを準備します。すべてうまくいけば、デプロイしたアプリをheroku openでブラウザに表示できます。

うまく動いた方、おめでとうございます!

補足: 静的なアセットについて

今回ビルドしたアプリでは静的なアセットを使っていませんが、Webpackerで静的なアセットを扱う方法についても触れておく価値があると思います。ここでは画像を扱いたいとしましょう。最初に、画像の置き場所を決める必要があります。frontendフォルダの下のimagesフォルダにまとめて置くか、画面の表示を担当するコンポーネントの下に個別の画像を置きます。画像をどこに置くとしても、画像がWebpack manifestに現れるようにするには、画像をJavaScriptにimportして最終的にapplication.jsのエントリポイントに含まれるようにする必要があります。

app/assets/imagesの下にある既存の画像をすべてfrontend/staticに素早く移動してstatic.jsエントリポイントにリンクする方法については、Gistをご覧ください。

画像の数が多すぎて、ヘルパーモジュールのバンドル項目を増やしたくない場合(Webpackのfile-loaderは、ファイルごとにパスを返す責任だけを持つモジュールを1つ生成します)、packsの下に個別のエントリポイントを作成して(static.jsなどのように)呼び出すこともできます。

そして、asset_pack_pathヘルパーimage_tagを組み合わせると、正しい<img src="">を生成できます。

画像とコンポーネントをまとめる方法は次のような感じになります。

  • フォルダ構造:
frontend/components/header
├── _header.html.erb
├── header.css
├── header.js
└── static
    └── logo.png

header.jsは次のようになります。

import "./header.css";
import "./static/logo.png"

これで次のようにERBパーシャルに書けます。

<%= image_tag asset_pack_path('./static/logo.png') %>

別の方法としては、image_tagを使うのを我慢し、代わりにCSSでurlヘルパーを用いてWebpackのcss-loaderがデフォルトでプロジェクトに含める画像を直接読み込む方法もあります。これで、次のようにCSSのbackground-プロパティとして要素に画像を割り当てることができます。

.header {
  &--logo {
    width: 100px;
    height: 100px;
    margin-bottom: 25px;
    background-image: url("./static/logo.png");
    background-size: 100%;
  }
}

この方法にする場合、JavaScriptファイルで画像をimportする必要も生じません。なお、url()はフォントにも使えます。

プロジェクトのリポジトリには、SVGアイコンをCSSから読み込む例も含まれています。インラインSVGを使いたい場合は、postcss-inline-svgモジュールを使うこともできます。

「Sprockets抜き」をやってみてわかったこと

ActionCableを使った場合とまったく同様に、RailsでSprocketを無効にすると他のいくつかの部分についてもnpmで再インストールする必要が生じます。

  • Turbolinks

プロジェクトでTurbolinksを再度有効にするには以下のようにします。

$ yarn add turbolinks
// frontend/packs/application.js
import Turbolinks from "turbolinks";
Turbolinks.start();
  • UJS

RailsにSprocketsがない場合、次のようにnpmrails-ujsを再インストールしないとUnobtrusive JavaScriptを理解できなくなります(link_tomethod: :deleteの設定など)。

$ yarn add rails-ujs
// frontend/packs/application.js
import Rails from "rails-ujs";
Rails.start();

本チュートリアルからヒントを得たプロジェクトの紹介

  • Komponentは、本記事で解説した「コンポーネントベースのアプローチ」をRailsプロジェクトに取り入れやすくするgemです。このgemに含まれるジェネレーターは、frontendフォルダの作成、Webpacker configの変更、コンポーネント作成を単一のコマンドで行なえます。また、パーシャルにふさわしいテンプレートエンジンを検出したり、コンポーネントごとの「プロパティ」やヘルパーの設定に使える.rbファイルでコンポーネントを拡張したりします。

Komponent gemの作成とメンテナンスは、フランスの開発会社OuvragesEtamin Studioが、Evil Martiansとは独立に行っています。


お読みいただきありがとうございました!

本チュートリアル3部作(全貌を理解するにはすべてお読みください)では、Webpackerを完全に採り入れてアセットパイプラインを取り除き、Reactなどのフロントエンドフレームワークについて学ばずに、できるだけRailsの組み込みツールを用いて「コンポーネント」のコンセプトに基づいてRailsのフロントエンドコードを編成する方法を学びました。本チュートリアルで作ったシンプルなチャットアプリは、Evil Martiansによって現実のプロジェクトで積極的に用いられている方法でデプロイ可能です。

本チュートリアルを進めるうえで何か問題がありましたら、お気軽にGitHubのissueを開いてお知らせください。


Part 1 | Part 2 | Part 3

スタートアップをワープ速度で成長させられる地球外エンジニアよ!Evil Martiansのフォームにて待つ。

関連記事

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)

新しいRailsフロントエンド開発(2)コンポーネントベースでアプリを書く(翻訳)

Rails: モデルのクエリをカプセル化する2つの方法(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: モデルのクエリをカプセル化する2つの方法(翻訳)

要点: サービスやコントローラなどのクラスからデータベースクエリのロジックを分離することは間違いなく優れた方法です。ロジックをモデルに置く場合、次の2とおりの方法が使えます。

1. クラスメソッド化する

def self.recent
  order(created_at: :desc)
end

2. ActiveRecordのスコープAPIを使う

scope :recent, -> { order(created_at: :desc) }

どちらにすればよいか

ActiveRecordのスコープはどっちみちクラスメソッドに変換されるので、どちらを選ぶかは見た目の問題に過ぎません。ただし、

スコープはいついかなるときでもチェイン可能である点がポイントです。

次のように、スコープの定義内に条件を含めた場合でもチェインできます。

scope :by_email, -> |email| { where(email: email) if email.present? }

クラスメソッドで同じことをした場合、メソッドをチェインできないことがあります。

def self.by_email(email)
  where(email: email) if email.present?
end

チェインできない理由は、self.by_emailemailがblankの場合にnilを返していることです。

ではどちらにすればよいか

チームの好みに合わせて決めればよいでしょう。その代わり、一度決めたらアプリ全体でその書き方を統一します。

関連記事

Railsのdefault_scopeは使うな、絶対(翻訳)

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)

$
0
0

概要

原著者の許諾を得て、CC BY-NC-SAライセンスに基づき翻訳・公開いたします。

記事のボリュームが大きいので前編/後編に分割しました。後編は来週公開予定です。

Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)

Herokuライクなデプロイソリューションの構築方法を解説します。
特定のクラウドプロバイダや、Dockerと無関係なツールを必要としません。

はじめに

本チュートリアルでは、Herokuにデプロイする感覚でソフトウェアをシンプルに自動デプロイするツールの作成方法をご紹介します。デプロイごとのバージョン管理にはDockerを使い、アップグレードやロールバックをやりやすくします。また、アプリの継続的デプロイ(CD)に弊社のSemaphoreも使います。

コンテナは任意のDocker Registryにホスティングできます。アプリの実行に必要なのはDockerがインストールされているホストだけです。

チュートリアルを終えると、サンプルアプリをリモートホストにHerokuと同じようにデプロイできるシンプルなRuby CLIスクリプトが使えるようになります。直前のバージョンへのロールバック、ログの追加、実行中のアプリのバージョントラッキングを行えるコマンドもあります。

本チュートリアルはデプロイ手順を中心に据えていますので、用途に応じて環境を調整すれば任意のアプリで使えます。ここではシンプルなHello WorldをRuby on Railsでblogフォルダに構築します。Railsアプリ構築の初歩については、RailsガイドのRails をはじめようの手順1〜4をご覧ください。

必要なもの

  • Docker: ホストと、アプリをデプロイするすべてのマシンにDockerがインストールされ、動作している必要があります。
  • Docker Registryのアカウント(Docker Hubなど)
  • SSHアクセスが可能でDockerがインストールされているクラウドプロバイダ(AWS EC2など)
  • Ruby 2.3: アプリをデプロイするすべてのマシンにインストールされている必要があります。

デプロイの手順

デプロイの手順は次の5つで構成されます。

  • ビルド: いつでも変更可能なビルド手順を備えた独自のコンテナをアプリごとにビルドします。
  • アップロード: アプリのコンテナのビルドが終わったらDocker Registryに送信する必要があります。初回はコンテナ全体のアップロードが必要なので多少時間がかかりますが、次回からはDockerのレイヤシステムでサイズや帯域を節約できるので速くなります
  • 接続: Docker Registryにコンテナを送信したら、次の手順を行うためにホストに接続します。
  • ダウンロード: ホストに接続したら、コンテナをダウンロードします。
  • 再起動: 最後の手順では、アプリを停止し、続いて停止時と同じ設定(ポート、ログ、環境変数など)で新しいコンテナを起動します。

手順の概要を把握できたので、作業を開始しましょう。

1. コンテナのビルド

この例では、アプリを実行するコンテナを1つ使います(Rubyはコンパイル言語ではないのでいわゆるビルドは不要です)。この場合のDockerファイルは次のとおりです。

FROM ruby:2.3.1-slim

COPY Gemfile* /tmp/
WORKDIR /tmp

RUN gem install bundler &&\
    apt-get update &&\
    apt-get install -y build-essential libsqlite3-dev rsync nodejs &&\
    bundle install --path vendor/bundle

RUN mkdir -p /app/vendor/bundle
WORKDIR /app
RUN cp -R /tmp/vendor/bundle vendor
COPY application.tar.gz /tmp

CMD cd /tmp &&\
    tar -xzf application.tar.gz &&\
    rsync -a blog/ /app/ &&\
    cd /app &&\
    RAILS_ENV=production bundle exec rake db:migrate &&\
    RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000

スクリプトを整理するために、Dockerfileはアプリの1つ上のフォルダ階層に置きます。次のような構成になります。

.
├── Dockerfile
├── blog
│   ├── app
│   ├── bin
... (アプリのファイルやフォルダ)

Dockerfileの各行について解説します。

FROM ruby:2.3.1-slim

これはコンテナのビルドに使うベースイメージです。Rubyがインストールされている必要があるので、自分で全部インストールするよりもプレインストール済みのコンテナを使う方が楽です。

COPY Gemfile* /tmp/
WORKDIR /tmp

ここでは、GemfileとGemfile.lockをコンテナの/tmpディレクトリにコピーし、次のコマンドを実行する/tmpに移動しています。

RUN gem install bundler &&\
    apt-get update &&\
    apt-get install -y build-essential libsqlite3-dev rsync nodejs &&\
    bundle install --path vendor/bundle

このRubyイメージのbundlerは古いので、warning表示を避けるためにアップデートしています。本チュートリアルで使われているのとは別のアプリで作業する場合は、他にもいくつかのパッケージ(多くはコンパイラ)が必要になるでしょう。最後にGemfileのgemをすべてインストールします。

Dockerの各コマンドは(layerなど)、コマンドの結果が同じ場合に再実行を避けるためにキャッシュされます。これで多少時間を節約できます。--pathフラグは、すべてのgemをローカルの定義済みパス(vendor/bundle)にインストールするよう指示します。

RUN mkdir -p /app/vendor/bundle
WORKDIR /app
RUN cp -R /tmp/vendor/bundle vendor
COPY application.tar.gz /tmp

ここでは、bundlerの最終的なインストールパスを作成し、インストールされたgemを前回のビルドキャッシュからすべてコピーしてから、圧縮されたアプリをコンテナ内にコピーします。

CMD cd /tmp &&\
    tar -xzf build.tar.gz &&\
    rsync -a blog/ /app/ &&\
    cd /app &&\
    RAILS_ENV=production bundle exec rake db:migrate &&\
    RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000

このコマンドはdocker runコマンドのときに実行されます。コンテナ内部の圧縮されたアプリを展開し、セットアップ手順(migrate)を実行してアプリを起動します。

Dockerfileの設定どおりに動作していることを確認するには、Dockerfileのあるrootディレクトリに移動して次のコマンドを実行します。

: 以下のmydockeruserはDocker Registryの登録済みユーザー名です。これは後でコンテナのバージョン管理に用います。

注2: Railsをproduction環境で実行する場合は、config/secrets.ymlファイルでSECRET_KEY_BASEなどの環境変数が必要です。ここでは単なるサンプルアプリを使っているので、development環境やtest環境と同様に固定値で安全に上書きできます。

$ cp blog/Gemfile* .
$ tar -zcf application.tar.gz blog
$ docker build -t mydockeruser/application-container .

上を実行すると、Dockerfileの各手順のビルドが以下のように開始されます。

Sending build context to Docker daemon 4.386 MB
Step 1/9 : FROM ruby:2.3.1-slim
 ---> e523958caea8
Step 2/9 : COPY Gemfile* /tmp/
 ---> f103f7b71338
Removing intermediate container 78bc80c13a5d
Step 3/9 : WORKDIR /tmp
 ---> f268a864efbc
Removing intermediate container d0845585c84d
Step 4/9 : RUN gem install bundler &&     apt-get update &&     apt-get install -y build-essential libsqlite3-dev rsync nodejs &&     bundle install --path vendor/bundle
 ---> Running in dd634ea01c4c
Successfully installed bundler-1.14.6
1 gem installed
Get:1 http://security.debian.org jessie/updates InRelease [63.1 kB]
Get:2 http://security.debian.org jessie/updates/main amd64 Packages [453 kB]
...

すべて問題なく完了すると、以下の成功メッセージが表示されます。

Successfully built 6c11944c0ee4

このハッシュ値はDockerによってランダムに生成されるので、コンテナをビルドするたびに異なります。

キャッシュが効いていることを確認するために、同じコマンドを再実行してみましょう。今度はほぼ一瞬で完了します。

$ docker build -t mydockeruser/application-container .
Sending build context to Docker daemon 4.386 MB
Step 1/9 : FROM ruby:2.3.1-slim
 ---> e523958caea8
Step 2/9 : COPY Gemfile* /tmp/
 ---> Using cache
 ---> f103f7b71338
Step 3/9 : WORKDIR /tmp
 ---> Using cache
 ---> f268a864efbc
Step 4/9 : RUN gem install bundler &&     apt-get update &&     apt-get install -y build-essential libsqlite3-dev rsync nodejs &&     bundle install --path vendor/bundle
 ---> Using cache
 ---> 7e9c77e52f81
Step 5/9 : RUN mkdir -p /app/vendor/bundle
 ---> Using cache
 ---> 1387419ca6ba
Step 6/9 : WORKDIR /app
 ---> Using cache
 ---> 9741744560e2
Step 7/9 : RUN cp -R /tmp/vendor/bundle vendor
 ---> Using cache
 ---> 5467eeb53bd2
Step 8/9 : COPY application.tar.gz /tmp
 ---> Using cache
 ---> 08d525aa0168
Step 9/9 : CMD cd /tmp &&     tar -xzf application.tar.gz &&     rsync -a blog/ /app/ &&     cd /app &&     RAILS_ENV=production bundle exec rake db:migrate &&     RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000
 ---> Using cache
 ---> ce28bd7f53b6
Successfully built ce28bd7f53b6

エラーメッセージが表示されたら、Dockerfileの構文やコンソールエラーをチェックしてやり直します。

次はすべて問題ないことを確認するために、コンテナのアプリを実行できるかどうかをテストしたいと思います。以下のコマンドを実行します。

docker run -p 3000:3000 -ti mydockeruser/application-container

これはコンテナを実行し、ホストのポート番号3000をコンテナのポート番号3000にマッピングします。問題が起きなければ次のようなRails起動メッセージが表示されます。

=> Booting Puma
=> Rails 5.0.2 application starting in production on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.8.2 (ruby 2.3.1-p112), codename: Sassy Salamander
* Min threads: 5, max threads: 5
* Environment: production
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

これで、localhost:3000をブラウザで開けばWelcomeメッセージが表示されます。

2. コンテナをDocker Registryにアップロードする

Docker Registryにログインするには以下の手順が必要です。

> docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don\'t have a Docker ID, head over to https://hub.docker.com to create one.
Username: mydockeruser
Password: ########
Login Succeeded

コンテナは完全に動作するので、今度はこれをDocker Registryにアップロードする必要があります。

$ docker push mydockeruser/application-container
The push refers to a repository [docker.io/mydockeruser/application-container]
9f5e7eecca3a: Pushing [==================================================>] 352.8 kB
08ee50f4f8a7: Preparing
33e5788c35de: Pushing  2.56 kB
c3d75a5c9ca1: Pushing [>                                                  ] 1.632 MB/285.2 MB
0f94183c9ed2: Pushing [==================================================>] 9.216 kB
b58339e538fb: Waiting
317a9fa46c5b: Waiting
a9bb4f79499d: Waiting
9c81988c760c: Preparing
c5ad82f84119: Waiting
fe4c16cbf7a4: Waiting

コンテナはレイヤごとにアップロードされますが、中には巨大なものもあります(100MB以上)初回に巨大なレイヤがアップロードされるのは問題ありません。今後はDockerのレイヤシステムを用いてアプリの変更分だけをアップロードし、ディスク容量や帯域を節約します。docker pushやレイヤについて詳しくお知りになりたい方は、公式ドキュメントをご覧ください。

pushが終わると成功のメッセージが表示されます。

...
9f5e7eecca3a: Pushed
08ee50f4f8a7: Pushed
33e5788c35de: Pushed
c3d75a5c9ca1: Pushed
0f94183c9ed2: Pushed
b58339e538fb: Pushed
317a9fa46c5b: Pushed
a9bb4f79499d: Pushed
9c81988c760c: Pushed
c5ad82f84119: Pushed
fe4c16cbf7a4: Pushed
latest: digest: sha256:43214016a4921bdebf12ae9de7466174bee1afd44873d6a60b846d157986d7f7 size: 2627

Docker Registryコンソールで新しいイメージを確認できます。

イメージを再度pushしてみると、すべてのレイヤが既に存在することがわかります。Dockerは再アップロードを回避するために、各レイヤのハッシュを照合してレイヤが既にあるかどうかをチェックします。

$ docker push mydockeruser/application-container
The push refers to a repository [docker.io/mydockeruser/application-container]
9f5e7eecca3a: Layer already exists
08ee50f4f8a7: Layer already exists
33e5788c35de: Layer already exists
c3d75a5c9ca1: Layer already exists
0f94183c9ed2: Layer already exists
b58339e538fb: Layer already exists
317a9fa46c5b: Layer already exists
a9bb4f79499d: Layer already exists
9c81988c760c: Layer already exists
c5ad82f84119: Layer already exists
fe4c16cbf7a4: Layer already exists
latest: digest: sha256:43214016a4921bdebf12ae9de7466174bee1afd44873d6a60b846d157986d7f7 size: 2627

3. リモート接続を開く

コンテナのアップロードが終わったので、リモートサーバーにダウンロードして実行する方法を見てみましょう。最初に、コンテナを実行するリモート環境の準備が必要です。ホストマシンで行ったときと同様に、DockerをインストールしてDocker Registryにログインしなければなりません。SSHでリモート接続するには、以下のコマンドを実行します。

ssh remoteuser@35.190.185.215
# 認証が必要な場合は以下を実行
ssh -i path/to/your/key.pem remoteuser@35.190.185.215

4. ダウンロード

リモートマシンでの設定をすべて終えた後は、ターミナルでのアクセスは不要になります。各コマンドはその環境で実行されます。コンテナをダウンロードしましょう。必要な場合はキーのフラグを指定することもお忘れなく。

$ ssh remoteuser@35.190.185.215 docker pull mydockeruser/application-container
Using default tag: latest
latest: Pulling from mydockeruser/application-container
386a066cd84a: Pulling fs layer
ec2a19adcb60: Pulling fs layer
b37dcb8e3fe1: Pulling fs layer
e635357d42cf: Pulling fs layer
382aff325dec: Pulling fs layer
f1fe764fd274: Pulling fs layer
a03a7c7d0abc: Pulling fs layer
fbbadaebd745: Pulling fs layer
63ef7f8f1d60: Pulling fs layer
3b9d4dda739b: Pulling fs layer
17e2d6aad6ec: Pulling fs layer
...
3b9d4dda739b: Pull complete
17e2d6aad6ec: Pull complete
Digest: sha256:c030e4f2b05191a4827bb7a811600e351aa7318abd3d7b1f169f2e4339a44b20
Status: Downloaded newer image for mydockeruser/application-container:latest

5. 再起動

コンテナを初めて実行したので、他のコンテナを停止する必要はありません。ローカルホストのときと同じコマンドを使って次のようにコンテナを実行できます。

$ ssh remoteuser@35.190.185.215 docker run -p 3000:3000 -d mydockeruser/application-container
f86afaa7c9cc4730e9ff55b1472c5b30b0e02055914f1673fbd4a8ceb3419e23

ここでは-tiフラグの代わりに-dフラグを与えているので、コンテナのハッシュだけが出力されます。これはコンテナをdetachedモードで動かす(出力をターミナルにアタッチしない)ことを表します。

ブラウザでリモートホストアドレス(ここでは35.190.185.21:3000を開いて、アプリが実行されているかどうかをチェックします。

(後編に続きます)

関連記事

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)

Dockerでsupervisorを使う時によくハマる点まとめ

PrometheusでDockerホスト + コンテナを監視してみた

Vue.jsサンプルコード(29)フィールドごとに全角英数字入力と半角英数字入力を自動で切り替える

$
0
0

29. フィールドごとに全角英数字入力と半角英数字入力を自動で切り替える

  • Vue.jsバージョン: 2.5.2
  • [全角]フィールドに入力する英数字は全角に、[半角]フィールドに入力する英数字は半角になります。
  • 日本語入力には影響しません。
  • 画面をリロードすると最初の状態に戻ります。

サンプルコード

ポイント

watchで変数の変更を監視しています。watchの内部で変数をさらに変更しても無限ループになりません。

  const vm = new Vue({
    el: "#app",
    data: {a: "", b: ""},
    watch: {
      a: function(v) {
        this.a = v.replace(/[A-Za-z0-9]/g, function(s) { return String.fromCharCode(s.charCodeAt(0) + 65248) })
      },
      b: function(v) {
        this.b = v.replace(/[A-Za-z0-9]/g, function(s) { return String.fromCharCode(s.charCodeAt(0) - 65248) })
      },
    },
  })

バックナンバー(Vue.jsサンプルコード)

Vue.jsサンプルコード(01〜03)Hello World・簡単な導入方法・デバッグ・結果の表示とメモ化


PostgreSQLのNOT NULL制約のロックを最小化して高速化する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

画像は元記事からの引用です。

PostgreSQLのNOT NULL制約のロックを最小化して高速化する(翻訳)

テーブル更新中にロックされたPostgreSQL

テーブル更新中にロックされたPostgreSQL

Doctolibは医師や患者向けに年中無休で運営されるWebサービスです。あらゆる医師のアジェンダを取り扱うという任務を完了したので、現在の重要な課題はダウンタイムゼロです。医師が自分たちのアジェンダやイベント通知にいついかなるときでもアクセスできるようにしなければなりません。サービスとして、新機能リリースのためにときおりデータベーススキーマのマイグレーションを行っています。このマイグレーションはリスクを伴うことがあり、サービス中断を何としても避けるために注意深く行わなければなりません。

マイグレーションは、データベーステーブルの変更を大規模に実施する場合は特別に慎重になります。Doctolibでは数日おきにデータのマイグレーションを行っていますが、残念なことにテーブルのデータ総量が著しく大きい(3000万行以上)ため、標準的なマイグレーションのベストプラクティスではサービスのダウンタイムを十分防止できないことがあります。私たちは現在危険な操作を防止するツールを使っていますが、マイグレーションによっては追加の安全策が必要になることもあります。

データベース制約の利用はデータの破損防止のために重要ですが、データベース制約の追加操作を誤るとテーブルをロックする危険があります。本記事でこの後説明するように、私たちはこの問題を別の角度から検討することにしました。PostgreSQLのおかげで、新しいカラムにNOT NULL制約を追加して操作中の読み書きを一切ロックせずに巨大なテーブルをマイグレーションすることができます。

厄介なマイグレーションをPostgreSQLで行う

より安全性の高いマイグレーションの必要性に気づかせてくれた、この事例についてもう少し詳しく説明します。この事例では、行数が3000万あるテーブルで、あるカラムにNOT NULL制約を追加します。データ自体はさほど巨大ではありませんが、うかつなマイグレーションを行うと一部のサービスでダウンタイムが生じる可能性があります。

カラムにNOT NULL制約を追加しようとする場合は、PostgreSQLで次のようにアトミックな操作として実行します。

ALTER TABLE テーブル名 ALTER COLUMN カラム名 SET NOT NULL;

これによってPostgreSQLでは次が行われます。

この操作は、次の場合にリスクを伴います。

  • そのテーブルを意図的に変更する場合
  • フルスキャンに時間がかかる場合(特に巨大なテーブルを扱う場合)

これによって、アプリのワーカーが待ち状態になってアプリがフリーズし、問題がたちまち広がって操作中にサービス全体が停止する可能性があります。

staging環境で試してみたところ、3000万行で操作に1.7秒を要しました。

もしこのテーブルに1秒間に100回書き込みを行っていたら、操作中に100を超えるデータベース接続がロックされたでしょう。

標準的な解決方法: とにかくやる

こういう状況についてよくある意見は、カラムにNOT NULL制約を追加し、慎重に作業を進めるためにサービスの利用頻度が最も低い適切な時間を選んでマイグレーション計画を立て、コストの大きいこのマイグレーション中はメンテナンスモードに切り替えればよいのでは、というものです。

私たちがこのアドバイスを採用したくない理由がおわかりでしょうか。

マイグレーション対象であるこのテーブルは、アプリの中でも重要な部分であり、医師や患者からのトラフィックが少ない夜中を選んでマイグレーションを決行する以外の選択肢がなくなってしまいます。残念ながら、夜勤中の医師にとってはこれでも問題が生じることが示されるでしょう。Doctolibユーザーに不便を強いるあらゆるリスクも回避できる、もっと賢い方法でなければなりません。

PostgreSQLのCHECK CONSTRAINTで切り抜ける

PostgreSQLのドキュメントを見てみましょう。

NOT-NULL制約は、常にカラムの制約として記述される。NOT-NULL制約は、チェック制約CHECK (カラム名 IS NOT NULL)を作成することと機能上同等である。

要するにCHECK CONSTRAINTはある意味でカラム制約と似ているのですが、これはそのテーブルに属しています。たとえば、priceカラムの値は常に100を超えていなければならないというCHECK CONSTRAINTは次のようになります。

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric,

    CONSTRAINT check_price_value CHECK (price > 100)
);

ここで注目したいのは、CHECK CONSTRAINTは値の非NULLを強制できる点です。

そしてここでのトリックは、チェック制約を追加するときにNOT VALIDオプションを発行できることです。このオプションを指定すると、「この制約は既存データについては有効ではない可能性があるので(既存データの)チェックは必ずしも必要なわけではない」とあなたが認識していることがPostgreSQLに伝わります。ただし、以後のINSERTUPDATEでは制約が強制されます。

本質的にこのオプションは、テーブルで行われる可能性がある仰々しい初期チェックを行わなくなります。この操作を行っても従来どおりEXCLUSIVE LOCKが取得されてテーブルへの書き込みは差し止められますが、すべての行に対するバリデーションを行わなくなるので非常に高速です(私たちのデモ環境では6ms)。

これこそ探していたものです!

この機能はどのように使えばよいのでしょうか?

1. CHECK CONSTRAINTを追加する。NOT VALIDの定義を忘れないこと!

ALTER TABLE テーブル名 ADD CONSTRAINT 制約名 CHECK (カラム名 IS NOT NULL) NOT VALID;

2. 制約のバリデーションは別のステートメントでPostgreSQLに指示するだけでよい

ALTER TABLE テーブル名 VALIDATE CONSTRAINT 制約名;

このVALIDATEコマンドは以下を実行します。

  • テーブルのフルスキャン
  • SHARE UPDATE EXCLUSIVEロック(他のALTER TABLEコマンドなどと同様、スキーマ変更だけをロック)を取得。このテーブルへの読み書きは引き続き可能。
  • PostgreSQLは、新しいデータについては(制約が)既に強制されていることを前提とするので、テーブル上の既存データをチェックして制約が有効であることを確認する。したがって、書き込みロックは完全に不要になる。

カラムのNOT NULL制約とCHECK CONSTRAINTのnot nullの違い

最終的な結果は同じになることもありますが、それでもいくつかの違いがあります。

  • チェック制約は名前が必要であり、テーブルに属する必要があります。NOT NULLカラムは後者のオプションのひとつに過ぎません。
  • パフォーマンスの観点から、PostgreSQLドキュメントには次のように書かれています。

PostgreSQLにおいては、明示的にnot-null制約を作成する方が効率が高い。

Stackexchangeのベンチマークによると、書き込み時のパフォーマンスで0.5%以上のペナルティが生じます。私たちのテストでは1%程度だったので私たちの事例では無視できますが、状況によってはパラメータで懸念が生じることがあるかもしれません。

  • 関連付けられたカラムを削除する前にはチェック制約を削除しなければならない
  • NOT NULLはpsqlで\d your_tableを発行するときにカラム名に続けて書くが、チェック制約は特定のセッション下で記述する

いずれの場合であっても、デフォルト値を用いてテーブルにすべてのデータを埋め戻すことをお忘れなく ;-)

新しい標準?

この種のマイグレーションは、サービス全体で用いられるコアのテーブルを変更する場合や、大量のデータが頻繁に変更される場合に非常にトリッキーになることがあります。幸いなことに、コアテーブルの変更の必要性は時間とともに減少します。

この特定のマイグレーションは、NOT NULL CONSTRAINTSをコアテーブルに追加するような状況で使ったことがありません。このマイグレーションは1回成功していますが、まだDoctolibの標準として定めていません。何より、このソリューションは私たちの「驚き最小の法則」の哲学に沿っていません。私たちの事例では容認できますが、今後もそうとは限りません。再びこの状況に直面するときがきたら、私たちのニーズに合ったソリューションを再度検討しなければならないでしょう。

追伸: パリで私たちのチームに参加いただけるPostgreSQLラブな開発者を募集しています。www.doctolib.fr/jobs/engineeringまでどうぞ。

関連記事

Rails: PostgreSQLのマイグレーション速度を改善する(翻訳)

PostgreSQL 10の使って嬉しい5つの機能(翻訳)

PostgreSQL 9.6→10アップグレードのダウンタイムをpglogicalで最小化(翻訳)

Rails: テストのリファクタリングでアプリ設計を改良する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: テストのリファクタリングでアプリ設計を改良する(翻訳)

Code Climate編集者メモ: 今回はゲストとしてMarko Anastasovの記事もご紹介します。Markoは開発者であると同時に、CI/デプロイサービスで知られるSemaphoreの共同設立者であり、Code ClimateのCIパートナーでもあります。


単体テストを書くという行為は、検証よりも設計という行為に近い — Bob Martin

テスト駆動開発(TDD)はテストのためのものであるという思い違いを未だによく見かけます。TDDを遵守することで開発が迷走する可能性を最小限に抑えることができ、最初にテストを書くことを義務付けることでテストの書き忘れも最小限に留められます。いつもの私は、超人であり続けなければとてもなしえないようなソリューションではなく、普通の人間のために設計されたソリューションを選びますが、ここでは少し違います。TDDは自動化テストを一種の乗り物のように用いて、私たちがコードを書く前にコードのことをいやでも考えざるを得ないように設計されています。なおこの方法は、特定の機能に接続されるすべてのコードが期待どおり動作していることを確認するのにデバッガを起動するよりもずっとよい方法です。TDDの目的はソフトウェア設計の改良であり、テストコードはその副産物のひとつです。

テストを必ず最初に書くことで、テストされるオブジェクトのインターフェイスについてじっくり考えるようになります。必要だがまだ存在しないオブジェクトについても同様です。作業は制御可能な小さな範囲で少しずつ進められます。テストが初めてパスしてもそこで作業は終わりではありません。再び実装に立ち戻ってコードをリファクタリングし、コードを美しく保ちます。コードが正しく動作していることを担保するテストスイートが存在するおかげで、自信を持ってコードを変更できます。

TDDの経験者なら誰でも、コードの設計力を問われ、そして磨かれることに気づくようになります。開発しながら常に「むー、このコードはprivateのままではまずそうだな」とか「このクラスの責務が増えすぎてしまった」という風に考えるようになるのです。

テスト駆動リファクタリング

あるコードのテストをどう書けばよいかわからなくなってくると、「red-green-refactor」というサイクルが止まってしまうこともあるでしょうし、たとえ書けたとしてもかなりつらい作業に思えることでしょう。テストを書くのがつらい部分は、しばしばコードの設計に問題があることを示します。あるいは、その部分のコードがTDDアプローチに沿って書かれていなかっただけかもしれませんが。テストコードの「匂い」は多くの場合アンチパターンと呼ぶのがふさわしく、テストとアプリコードの両方についてリファクタリングする機会であることを示します。

例として、Railsのcontroller specでの複雑なテストセットアップを見てみましょう。

describe VenuesController do

  let(:leaderboard) { mock_model(Leaderboard) }
  let(:leaderboard_decorator) { double(LeaderboardDecorator) }
  let(:venue) { mock_model(Venue) }

  describe "GET show" do

    before do
      Venue.stub_chain(:enabled, :find) { venue }
      venue.stub(:last_leaderboard) { leaderboard }
      LeaderboardDecorator.stub(:new) { leaderboard_decorator }
    end

    it "venueをidで検索して@venueに代入する" do
      get :show, :id => 1
      assigns[:venue].should eql(venue)
    end

    it "@leaderboardを初期化する" do
      get :show, :id => 1
      assigns[:leaderboard].should == leaderboard_decorator
    end

    context "userはpatronとしてログインしている" do

      include_context "patronがログインしている"

      context "patronはトップ10にいない" do

        before do
          leaderboard_decorator.stub(:include?).and_return(false)
        end

        it "leaderboardからpatronのstatsを取得" do
          patron_stats = double
          leaderboard_decorator.should_receive(:patron_stats).and_return(patron_stats)
          get :show, :id => 1
          assigns[:patron_stats].should eql(patron_stats)
        end
      end
    end

    # 簡単のため以後のテストケースは省略
  end
end

このコントローラのアクションは、技術的にはさほど長くありません。

class VenuesController < ApplicationController

  def show
    begin
      @venue = Venue.enabled.find(params[:id])
      @leaderboard = LeaderboardDecorator.new(@venue.last_leaderboard)

      if logged_in? and is_patron? and @leaderboard.present? and not @leaderboard.include?(@current_user)
        @patron_stats = @leaderboard.patron_stats(@current_user)
      end
    end
  end
end

ここでお気づきいただきたいのは、specセットアップのコードが長いと、たとえばVenue.enabled.findが呼び出されるというexpectationや、LeaderboardDecorator.newに正しい引数が渡されるというexpectationを開発者が書き忘れてしまいがちであるという点です。代入された@leaderboardの元は代入されたvenueであるかどうかがまったく明確になっていません。

MVCパラダイムに囚われてしまった開発者は(私も含めてですが)、ついコントローラにビジネスロジックを長々と書き連ねてしまい、よいspecを書くこともコードやspecのメンテも困難になってしまいます。この困難は、Railsのコントローラのたった1行のメソッドですら多くのことを行っていることが原因です。

def show
  @venue = Venue.find(params[:id])
end

上のメソッドはこれだけの作業を行っています。

  • パラメータを取り出す
  • アプリ固有のメソッドを呼び出す
  • ビューテンプレートで用いられる変数へ代入する
  • レスポンステンプレートのレンダリング

データベース内部やビジネスルールの奥深い部分に到達するコードを書き足すと、コントローラのメソッドがカオスになるだけです。

上のコントローラには、4つの条件を持つif文が隠れています。完全なspecでは、これをカバーするためだけに15とおりの組み合わせを記述しなければなりませんが、もちろんそのようなものは書かれていません。しかし、コードがコントローラの外に置かれる場合は事情が変わってきます。

改良版のcontroller specが次のようになっているとしましょう。外部から受け付けるリクエストを処理してレスポンスを準備するという作業を実行するためにはどのようなインターフェイスが望ましいでしょうか。

describe VenuesController do

  let(:venue) { mock_model(Venue) }

  describe "GET show" do

    before do
      Venue.stub(:find_enabled) { venue }
      venue.stub(:last_leaderboard)
    end

    it "有効なvenueをidで検索する" do
      Venue.should_receive(:find_enabled).with(1)
      get :show, :id => 1
    end

    it "見つかった@venueを代入する" do
      get :show, :id => 1
      assigns[:venue].should eql(venue)
    end

    it "venueのleaderboardをデコレーションする" do
      leaderboard = double
      venue.stub(:last_leaderboard) { leaderboard }
      LeaderboardDecorator.should_receive(:new).with(leaderboard)

      get :show, :id => 1
    end

    it "@leaderboardを代入する" do
      decorated_leaderboard = double
      LeaderboardDecorator.stub(:new) { decorated_leaderboard }

      get :show, :id => 1

      assigns[:leaderboard].should eql(decorated_leaderboard)
    end
  end
end

他のコードはどこに行ってしまったのでしょうか?ここではモデルを拡張して検索ロジックを単純化しています。

describe Venue do

  describe ".find_enabled" do

    before do
      @enabled_venue = create(:venue, :enabled => true)
      create(:venue, :enabled => true)
      create(:venue, :enabled => false)
    end

    it "有効なスコープ内で検索する" do
      Venue.find_enabled(@enabled_venue.id).should eql(@enabled_venue)
    end
  end
end

さまざまなif文は次のように単純化できます。

  • if logged_in?: 結果の違いはビューテンプレートで決定できる
  • if @leaderboard.present?: (古いコード)falseの場合の動作はビューで決定できる
  • その他のコードはdecoratorクラスに移動して新しいメソッドで詳しく記述できる
describe LeaderboardDecorator do

  describe "#includes_patron?" do

    context "userがpatronではない" { }

    context "userがpatronである" do
      context "userがリストにいる" { }
      context "ユーザーがリストにいない" { }
    end
  end
end

この新しいメソッドは、@leaderboard.patron_statsをレンダリングするかどうかをビューで決定できるようにします。この部分の変更は不要です。

# app/views/venues/show.html.erb
<%= render "venues/show/leaderboard" if @leaderboard.present? %>
# app/views/venues/show/_leaderboard.html.erb
<% if @leaderboard.includes_patron?(@current_user) -%>
  <%= render "venues/show/patron_stats" %>
<% end -%>

これで、コントローラのメソッドがかなりシンプルになりました。

def show
  @venue = Venue.find_enabled(params[:id])
  @leaderboard = LeaderboardDecorator.new(@venue.last_leaderboard)
end

このコードを次回使うときには、LeaderboardDecoratorに与える正しい引数とは何かをコントローラ側で把握する必要がある点がちょっと残念かもしれません。venue用の新しいdecoratorを1つ導入して、デコレーションされたleaderboardを返すようにしてもよいでしょう。この部分の実装は読者の練習用に残しておきます ;)

最後に

もっと詳しくお知りになりたい方は、SemaphoreブログでMarkoのRailsアプリのテスティングアンチパターン記事をご覧ください。

関連記事

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

Railsで重要なパターンpart 2: Query Object(翻訳)

Ruby: Proxyパターンの解説(翻訳)

Rails: belongs_to関連付けをリファクタリングしてDRYにする(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: belongs_to関連付けをリファクタリングしてDRYにする(翻訳)

belongs_toリレーションはRailsアプリで最もよく使われる関連付けなので、皆さまのアプリでも多数使われていると確信しています。さて、JobCategoryという2つのモデルがあるとしましょう。1つのjobは1つのcategoryに属し、1つのcategoryには多くのjobがあるというシンプルな関連付けがなされています。

Categoryモデルにはpublished:booleanという属性があり、そのcategoryのjobを表示してよいかどうかを指定します。ここでの目的は、publishされたカテゴリに割り当てられているjobだけを返すクエリを作成することです。

普通の方法

通常は、以下のような方法を使います。

Job.joins(:category).where(categries: {published: true})

これに何かまずい点があるのでしょうか?別にありません。しかしここでは「ロジックの分離」に着目したいと思います。Jobモデルで何か操作を行う場合、publishされたcategoryだけを取り出すという条件を満たすことを気にかけるべきではありません。これはCategoryモデルに関連するロジックなので、このロジックをそちらに移動しましょう。

ロジックの分離

class Category < ActiveRecord::Base
 has_many :jobs

  def self.publishable
    where(published: true)
  end
end

ここではクラスメソッドの代わりにスコープを使うこともできます(スコープかクラスメソッドかについては別記事をご覧ください【原文リンク切れ】)。今回の場合、クラスメソッドの方が私にとって明確に思えたのでクラスメソッドを使うことにします。これによってJobモデルは次のようになります。

class Job < ActiveRecord::Base
  belongs_to :category

  def self.publishable
    joins(:category).merge(Category.publishable)
  end
end

これで、次のように呼び出せるようになりました。

Job.publishable

改善の理由

メンテナンスするコードがもっとたくさんある場合になぜ2番目のソリューションの方がよいかについて疑問をお持ちの方もいらっしゃるかもしれません。理由は次のとおりです。

  1. ロジックを分離できます。categoryに関連するものはCategoryモデルに配置され、jobに関連するものはJobに配置されています。あなたの同僚は、ロジックをチェックして正確にはどんなクエリが使われるべきかを調べなくても、Job.publishableを呼び出すだけで済みます。
  2. 最初のバージョンのクエリだと、アプリのあちこちにJob.joins(:category).where(categries: {published: true})がばらまかれてしまいます。そのcategoryがpublishされているかどうかを調べるために条件をもっと詳しくチェックしなければならないとしたらどうしますか?ばらまかれているコードをすべて見つけ出すという残念な方法を取らざるを得なくなります。しかし2番目の方法なら、メソッドに変更を加えるだけで済みます。他に何も変更する必要はありません。

  3. 人間にとってより読みやすいコードになります。これはチームで若手開発者を抱えている場合に非常に重要な点です。

  4. Categoryモデルに関連付けられているあらゆるモデルでCategory.publishedを使えるようになります。

Railsでお困りの方にお知らせ

twitter または連絡用フォームにてお知らせください。サポート方法をご連絡いたします。

関連記事

Rails: テストのリファクタリングでアプリ設計を改良する(翻訳)

週刊Railsウォッチ(20180119)derailed_benchmarks gem、PostgreSQLをGraphQL API化するPostGraphile、機械学習でモック画像をHTML化ほか

$
0
0

こんにちは、hachi8833です。Nintendo Laboにいろいろ持ってかれそうで気になってます。


つっつきボイス: 「任天堂のものづくりセンス、パないなー」

それでは今週のウォッチ、いってみましょう。

Rails: 今週の改修

今回はCommit差分から見繕いました。

left_outer_joinsをunscopeできるようになった

# activerecord/lib/active_record/relation/query_methods.rb#351
     VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
                                      :limit, :offset, :joins, :includes, :from,
-                                     :readonly, :having])
+                                     :readonly, :having, :left_outer_joins])

つっつきボイス: 「scopeでleft_outer_joinsできるならunscopeもできないと、ってことかな」

重要度の低いダイジェストにデフォルトでSHA-1を使用するようになった

# railties/lib/rails/application/configuration.rb#103
           if respond_to?(:active_support)
             active_support.use_authenticated_message_encryption = true
+            active_support.use_sha1_digests = true
           end

ETagヘッダーなどの重要でないダイジェストにはMD5ではなくSHA-1を使う。
Rails.application.config.active_support.use_sha1_digests = true
new_framework_defaults_5_2.rb.ttより大意

pg-1.0 gemに対応

pgが0.21から1.0にメジャーバージョンアップしたそうです。

# Gemfile.lock#343
-    pg (0.19.0)
-    pg (0.19.0-x64-mingw32)
-    pg (0.19.0-x86-mingw32)
+    pg (1.0.0)
+    pg (1.0.0-x64-mingw32)
+    pg (1.0.0-x86-mingw32)

つっつきボイス: 「へー、pgはもう永遠に1.0にならないんじゃないかと思ってた」「queue_classicってメンテナ代わったのかな?↓」

# Gemfile#65
-  gem "queue_classic", github: "QueueClassic/queue_classic", branch: "master", require: false, platforms: :ruby
+  gem "queue_classic", github: "Kjarrigan/queue_classic", branch: "update-pg", require: false, platforms: :ruby

savesave!の後でオブジェクトがunfreezeされていたのを修正

破棄したオブジェクトがsave後に変更される可能性があったので修正されました。

# activerecord/lib/active_record/persistence.rb#65
     def create_or_update(*args, &block)
       _raise_readonly_record_error if readonly?
+      return false if destroyed?
       result = new_record? ? _create_record(&block) : _update_record(*args, &block)
       result != false
     end

つっつきボイス: 「これ本当ならエラーをraiseしたいところだろうな: 互換性とかの問題でfalseを返してるのかも」

MySQL: create_databasecollationが指定されている場合にデフォルトのcharsetを追加しないように修正

# activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#250
       def create_database(name, options = {})
         if options[:collation]
-          execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}"
+          execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}"
         else
           execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}"
         end

つっつきボイス: 「そうそう、知らずに|| 'utf8'が効いちゃうとハマるんだよなー」「修正後のテストでは寿司ビール対策でおなじみのutf8mb4_bin使ってますね」

MySQLのencodingをutf8からutfmb4に変更して寿司ビール問題に対応する

リファクタリング: Browserクラスを新設

システムテストのactionpack/lib/action_dispatch/system_testing/driver.rbのオプションがBrowserクラスに引っ越しました。

# actionpack/lib/action_dispatch/system_testing/browser.rb
+module ActionDispatch
+  module SystemTesting
+    class Browser # :nodoc:
+      attr_reader :name
+
+      def initialize(name)
+        @name = name
+      end
...

DHHによる修正2件

# rails/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt#9
   policy.font_src    :self, :https, :data
   policy.img_src     :self, :https, :data
   policy.object_src  :none
-  policy.script_src  :self, :https
+  policy.script_src  :self, :https, :unsafe_inline
   policy.style_src   :self, :https, :unsafe_inline

   # Specify URI for violation reports

つっつきボイス: 「CSP=コンテンツセキュリティポリシー」「unsafe_inlineはW3Cのこれですね↓」

; Keywords:
keyword-source = “‘self'” / “‘unsafe-inline'” / “‘unsafe-eval'” / “‘strict-dynamic'” / “‘unsafe-hashed-attributes'”
CSP3より

-Rails.application.config.content_security_policy do |p|
-  p.default_src :self, :https
-  p.font_src    :self, :https, :data
-  p.img_src     :self, :https, :data
-  p.object_src  :none
-  p.script_src  :self, :https
-  p.style_src   :self, :https, :unsafe_inline
+Rails.application.config.content_security_policy do |policy|
+  policy.default_src :self, :https
+  policy.font_src    :self, :https, :data
+  policy.img_src     :self, :https, :data
+  policy.object_src  :none
+  policy.script_src  :self, :https
+  policy.style_src   :self, :https, :unsafe_inline

つっつきボイス: 「少なくともpはないなー: Kernel.#pがあるから」「そういえば1文字のローカル変数で他にも使えないものがあったような…」

その後思い出しましたが、pryではcなどをローカル変数に使うと怒られるのでした。

[1] pry(main)> c=1
=> 1
[2] pry(main)> c
Error: Cannot find local context. Did you use `binding.pry`?

参考: Pryのコンソールで使えない変数

Rails

Railsチュートリアルが5.1.4に対応


つっつきボイス: 「安川さんたちが継続的翻訳システムを構築しているおかげでRailsチュートリアルもガイドもオープンな差分翻訳ができるようになっててうれしいです: 自分はバッチで翻訳する方が好きですが」

プロセスマネージャ再び

Dogfooding Process Managerの続きだそうです。


つっつきボイス: 「自前でプロセスマネージャをこしらえた話のようなんですが、このプロセスって何だろうと思って」「ざっとしか見てないけど、Unixのプロセスのことではなさそうに見える」「ところで、何とかmanagerってネーミングはたいていアンチパターンですね」「あー確かに」

そういえば野球の世界では監督はmanagerですが、日本だとマネージャーは違う意味に横滑りしてますね。

マイグレーションをpendingしたままRailsを本番で実行しないようにする方法

短い記事です。ActiveRecord::Migration.check_pending!でやれるそうです。

# 同記事より
if ($PROGRAM_NAME.include?('puma') || $PROGRAM_NAME.include?('sidekiq')) && Rails.env.production?
  ActiveRecord::Migration.check_pending!
end

RailsのForm Objectとルーティング(RubyFlowより)

# 同記事より
class NewQuestionnaireForm
   include ActiveModel::Model

  def to_model
    Questionnaire.new(title: title, questions: questions)
  end

  def save
    to_model.save
  end
end

Railsのメモリ容量を減らしてHeroku課金を節約(Awesome Rubyより)


同記事より

以下の記事に出てきたjemallocyajl-rubyなどを動員して節約に励んでいます。

Ruby: mallocでマルチスレッドプログラムのメモリが倍増する理由(翻訳)

⭐derailed_benchmarks: Railsアプリのさまざまなベンチマークを取れるgem⭐

上の記事にも使われていたgemで、★1800超えです。ヒープダンプ/メモリリーク調査/stackprofなどさまざまな静的/動的情報を取れます。

# READMEより
$ bundle exec derailed exec perf:stackprof
==================================
  Mode: cpu(1000)
  Samples: 16067 (1.07% miss rate)
  GC: 2651 (16.50%)
==================================
     TOTAL    (pct)     SAMPLES    (pct)     FRAME
      1293   (8.0%)        1293   (8.0%)     block in ActionDispatch::Journey::Formatter#missing_keys
       872   (5.4%)         872   (5.4%)     block in ActiveSupport::Inflector#apply_inflections
       935   (5.8%)         802   (5.0%)     ActiveSupport::SafeBuffer#safe_concat
       688   (4.3%)         688   (4.3%)     Temple::Utils#escape_html
       578   (3.6%)         578   (3.6%)     ActiveRecord::Attribute#initialize
...

つっつきボイス: 「derailed_benchmarksは結構使われている印象っすね」「作者はRichard Schneemanさんでした」

ベテランRubyistがPythonコードを5倍速くした話(翻訳)

今週の⭐を進呈いたします。おめでとうございます。

Stimulus: Turbolinksと相性のよい控えめなJSフレームワーク(Ruby Weeklyより)


stimulusjs/stimulusより

// hello_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  greet() {
    console.log(`Hello, ${this.name}!`)
  }

  get name() {
    return this.targets.find("name").value
  }
}

つっつきボイス: 「Stimulusって、確かDHHのBasecampがやってるやつですよね」「Turbolinksって最近オフにすること多いけど」

Railsを順を追ってアップグレードする(Awesome Rubyより)


つっつきボイス: 「冒頭の1. Stop the world、2. Long-lived upgrade branchとかまさに定番のアプローチ」「こうやって本番を二本立てにして↓リバースプロキシで振り分けながら少しずつ移行するというのもあるある: 検証が不十分なまま切り替えるとえらいことになったりするけど」


同記事より

ActiveRecordに欲しくなるEctoの機能(Awesome Rubyより)


infinum.coより

def registration_changeset(struct, params) do
  struct
  |> cast(params, [:email, :password])
  |> validate_required([:email, :password])
  |> unique_constraint(:email)
  |> put_password_hash()
end

def update_profile_changeset(struct, params) do
  struct
  |> cast(params, [:first_name, :last_name, :age])
  |> validate_required([:first_name, :last_name])
end

|>はElixirの「パイプライン演算子」だそうです。


つっつきボイス: 「Ectoって、ElixirのPhoenixフレームワークで使うやつか」「EctoはORMではない、って書いてますね」「この種のフレームワークを業務で使う動機は今のところ見えないなー」

参考: Rails使いがElixirのEctoを勉強した時のまとめ

Railsアプリの災害復旧プラン


engineyard.comより


つっつきボイス: 「disaster recovery planはRailsアプリに限らず重要っすね」「そういえば最近Engine Yardってひと頃ほど見かけない気がしてきた」

あるRails請負開発者の一日(Hacklinesより)

Planet Argonさん(@planetargon)がシェアした投稿


つっつきボイス: 「相当昔ですが、Oracle日本支社では犬飼ってるって話を思い出しました」「今のオフィス事情だと難しそう」

その他Rails小粒記事


つっつきボイス: 「JavaScriptのテスティングフレームワークというとmocha以外にJestもあるのね」
「最後のパーシャルレンダリング記事、でかいデータで素朴にeach回したら確かに遅い↓」

<% @users.each do |user| %>
    <%= render 'erb_partials/post', user: user %>
<% end %>

「こうやってcollection使う方が確実に速いけど、油断するとつい上みたいに書いちゃうことはあるな」「社内でもたまに見ますね」

<%= render partial: 'erb_partials/post', collection: @users, as: :user %>

参考: Railsガイド 3.4 パーシャルを使用する

Ruby trunkより

提案: Hash#transform_keys!recursive: trueオプション(継続)

config = MyAwesomeFormat.load(file); config.transform_keys!(recursive: true, &:to_sym)みたいに書きたいという主旨です。

def transform_keys!(recursive: false, &block)
  # do original transform_keys! here
  values.each do |v|
    if v.respond_to?(:each)
      v.each{|i| i.transform_keys!(recursive: true, &block) if i.respond_to?(:transform_keys!) }
    else v.respond_to?(:transform_keys!)
      v.transform_keys!(recursive: true, &block)
    end
  end if recursive
end

提案: GC速度と少々引き換えにメモリを削減(継続)


#14370より

Aaron Pattersonさんからのissueです。


つっつきボイス: 「最初にレス付けてるnormalpersonさんは昨年のRubyKaigiのあちこちで名前が出てきてた、普通じゃない人」「凄い名前w」

週刊Railsウォッチ(20170922)特集: RubyKaigi 2017セッションを振り返る(1)、Rails 4.2.10.rc1リリースほか

入れ違いで修正

Ruby

Ruby 2.5の陽の当たっていない新機能(Hacklinesより)

行カバレッジやブランチカバレッジ機能などを紹介しています。

Y -> 1: def hello(number)
Y -> 2:   if number == 1
Y -> 3:    'world'
Y -> 4:   else
N -> 5:     'mars'
Y -> 6:   end
Y -> 7: end
Y -> 8:
Y -> 9: hello(1)

つっつきボイス: 「この機能がRubyMineみたいなIDEと連携したらすごくうれしい」「名前忘れたけどこういうカバレッジのgemあった: 絶対に通過しないコードをあぶり出したりとかできる」

なお2.5のカバレッジについては以下でChangelogをざっくり訳してあります。

Ruby 2.5.0リリース!NEWSを読んでみた

Rubyの継承で動的に引数を渡す(RubyFlowより)

# 同記事より
class Render
  def self.engine; end

  def self.inherited(subclass)
    puts subclass        #=> Memo::Render
    puts subclass.engine #=> nil !!!
  end
end

つっつきボイス: 「Rubyってここまでエグいコードも書けるんだなって思いますね」「何でもアタッチできちゃうとか、ここまでくるともうオブジェクト指向言語というよりオブジェクト指向スクリプトみたいw」

Ruby 2.5のFrozenErrorクラス

2.5.0 :001 > NAME = 'Atul'.freeze
 => "Atul"
2.5.0 :002 > NAME << 'Joy'
Traceback (most recent call last):
        2: from /home/atul/.rvm/rubies/ruby-2.5.0/bin/irb:11:in `<main>'
        1: from (irb):2
FrozenError (can't modify frozen String)

つっつきボイス: 「今までRuntimeErrorだったのがFrozenErrorに変わるのはありがたい」「frozen_string_literalが完了するまでの混乱を少しでも軽くするためでもあるんでしょうね」

参考: frozen_string_literalが入って気づいた、メソッド設計の原則

Kernel.method_added


つっつきボイス:Kernel.method_addedなんてのがあるのか: 特定のメソッド追加にフックかける」「実体はModuleにあった↓」

# docs.ruby-lang.org/ja/2.5.0/method/Module/i/method_added.html
class Foo
  def Foo.method_added(name)
    puts "method \"#{name}\" was added"
  end

  def foo
  end
  define_method :bar, instance_method(:foo)
end

=> method "foo" was added
   method "bar" was added

参考: Module#method_added

aruba: CLIアプリをRSpecやMiniTestでテストするgem(Awesome Rubyより)


つっつきボイス: 「あのCucumberがやってるんですね」「バッチ処理のテストをRSpecとかで書けるし、Ruby以外に任意のCLIに使えるのがいいな」


app.cucumber.pro

Rubyのシンボル話その後

#14347はちょうど前回のウォッチで取り上げました。


つっつきボイス: 「あちこちで話題になってるやつですね」「途中まで読んでた」「やっとシンボルは文字列ではないということになったと」

Rubyのシンボルをなくせるか考えてみた(翻訳)

Rubyはやっぱり死んでない(Ruby Weeklyより)

こちらもEngine Yardのブログです。

ネストしたハッシュをlambdaでリファクタリング

同記事より
pub.doc[‘programs’].each &remove_icons(‘grades’, &remove_icons(‘units’))
def remove_icons value_key=nil, &block
  lambda do |key, value|
    next if key == ‘_order’
    value.delete ‘icons’
    if value_key
      value[value_key].each(&block) if block_given?
      value[value_key].each(&remove_icons) unless block_given?
    end
  end
end

つっつきボイス: 「うんうんよくあるやつ:再帰で書きましょう!みたいな」「Hashの再帰って何かと面倒ですよね」「せいぜい3階層ぐらいしか潜らないことをわかって書いてるのに、RuboCopに怒られたりとか」

高木さん

SQL

PostgreSQLにはmeltdownパッチは不要だが少し遅くなる(Postgres Weeklyより)


つっつきボイス: 「meltdownは基本的にカーネルの問題だからアプリにパッチが必要になることはそうないかと」「パッチで遅くなるのはもうしゃーない」「AWSもいろいろ言ってるけど対策すれば遅くなるっしょ」

参考: CPU脆弱性Meltdownのパッチ適用でベンチマークスコアが25%低下した

PostGraphile: PostgreSQLをGraphQL API化するJSライブラリ(Postgres Weeklyより)


graphile.orgより

以下を実行してhttp://localhost:5000/graphiqlをブラウザで開くといきなりGraphiqlが動きました。N+1クエリも克服しているそうです。これ凄いかも。

npm install -g postgraphile
postgraphile -c postgres://user:pass@host/dbname --schema schema_name

PGLogicalがアップデート(Postgres Weeklyより)

PostgreSQLの動作を知る(Postgres Weeklyより)

Pythonのツールを使います。


つっつきボイス: 「お、Internalとか書いてるけどこれはむしろ入門向け記事ですね: 量は多いけど相当やさしい内容」

JavaScript

(a ==1 && a== 2 && a==3)trueにする知見が続々

BPS社内で盛り上がりました。

// Stackoverflowより
var aᅠ = 1;
var a = 2;
var ᅠa = 3;
if(aᅠ==1 && a== 2 &&ᅠa==3) {
    console.log("Why hello there!")
}

ifに半角のハングル文字を使うという荒業を繰り出したり、C++などでやってみたりしています。

JavaScriptの二進木、再帰、末尾呼び出し最適化

nullとundefinedとは

CSS/HTML/フロントエンド

Web Componentsの秘密

参考: MDN Web Components

フロントエンドのエラー表示を再考する


logrocket.comより

blog.jxck.ioの新着記事


つっつきボイス: 「これは読んでおきたい記事」「そういえばこれと少し似た感じの、ネコのアイコンのブログ記事が話題になってましたね」「ネコのアイコン…?」「あったこれ↓」「あこの人か: アイコンとか全然気にしてなかったw」

参考: ソフトウェアの互換性と僕らのUser-Agent文字列問題

Screenshot-to-code-in-Keras: ニューラルネットワークでモック画像から静的HTMLページを生成(GitHub Trendingより)

アニメーションGIFが巨大すぎるのでここには貼りませんでした。


つっつきボイス: 「すげっ」「Bootstrapにも対応してるみたいですね」「一回こっきりの案件とかならかなりイケそう」「HTMLコーダー界に激震走るか」

その他

YAGNIを実践する


dev.to/gonedarkより


つっつきボイス: 「社内にもYAGNIを愛して止まない人がいるから彼を観察してるとだいたいわかりますよ」

参考: Wikipedia-ja YAGNI

Slackにprivate shared channel機能が追加


つっつきボイス: 「これありがたい: shared channelは前からあるけどpublicにしかできなかったんで」

Windows CLIの改善

WSLのchmod/chownの改良とtar/curlの追加です。

なお、こんなのもありました。

DOS窓の|は大丈夫だそうです。

Docker for macで/etc/localtimeがマウントできない問題

minio: Amazon S3 API互換のオブジェクトストレージサーバー


minio.ioより


つっつきボイス: 「S3互換のこういうのは他にもありますけどね」「GCPやAzureとかいろんなクラウドで使えるのはよさそう」「今さらですがオブジェクトストレージサーバーって何でしたっけ?」「AWS S3みたいなサービスがそれです: WebDAVみたいにRESTfulにオブジェクトにアクセスできるサービス」

HighwayHash: Go言語の爆速ハッシュ生成ライブラリ


つっつきボイス: 「10G/secとか確かに超速い」「ハッシュは速度だけあってもいかんので、ちゃんと分散してるかとかも大事ですね」

データがあれば使えるCloud AutoML VisionをGoogleが発表

一般のニュースにもなってますが一応。


つっつきボイス: 「今はデータサイエンティストやAIエンジニアが明らかに不足してるからどこもカスタマイズとかチューニングに手が回らなくて、こうやってそこそこのものを公開して好きに使ってくれ、みたいな方向に向かってる感じですね」「ユーザーに丸投げですか」

参考: Googleが「Cloud AutoML Vision」を発表、独自のデータセットを使ったカスタム機械学習モデルが簡単に構築できるように

番外

暗算術

a% of b = b% of aは初めて知りました。「25の16%=16の25%」みたいに使うそうです。


つっつきボイス: 「計算すると確かにそうなってるな: 式で見ると一瞬でわかるけど言われるまで気づきにくい」「英語圏なんで単位のフィート換算とかいらなさそうなのも多いです」「ひと頃入社試験でよく出されたフェルミ推定なんかやるときは、こういうのを何となくでも知っておかないと手も足も出なかったりしますね」

参考: 暗記しておくとなにかと便利なプチ公式まとめ

これは欲しい


つっつきボイス: 「Nintendo Laboとどっちが子どもにウケるかなと思って」「今ならポプテピピックっしょw」「あれはもう子どもの反応が面白すぎますね」

学習/プログラミング不要の産業ロボット


つっつきボイス: 「荷物の積み下ろしとかまでやってます」「人間雇う方がまだまだ安いな、今のところは」

Switchエミュレータ(GitHub Trendingより)


つっつきボイス: 「ソフトはともかくハードウェアはそうもいかないか」

その後、GPL V2というライセンスの厳しさや、パチンコの当たり判定システムの話題で盛り上がりました。

ルーシーさん

自己修復コンクリート


今週は以上です。

バックナンバー(2017年後半)

週刊Railsウォッチ(20180112)update_attributeが修正、ぼっち演算子`&.`は`Object#try`より高速、今年のRubyカンファレンス情報ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやRSSなど)です。

Rails公式ニュース

Ruby Weekly

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Hacklines

Hacklines

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

JSer.info

jser.info_logo_captured

Github Trending

160928_1701_Q9dJIU

Ruby: Kernel#itselfの美学(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Ruby: Kernel#itselfの美学(翻訳)

最近私は、Rubyでよくある問題を解決しました。コレクションで渡される項目ごとの頻度をカウントするという問題です。この問題の解決方法はいくつか考えられます。最初は以下のようにEnumerable#injectまたはEnumerable#each_with_objectを用い、空のハッシュをアキュムレータの値として使いました。

collection.each_with_object({}) { |item, accum| accum[item] = accum[item].to_i + 1 }

ハッシュのデフォルト値を用いて、もう少しスマートな方法に変えました。

collection.each_with_object(Hash.new(0)) { |item, accum| accum[item] = accum[item] + 1 }

これはこれでなかなかいいのですが、これよりずっと美しい方法があります。

Rubyの美学

この問題を解決する興味深い方法のひとつは、Enumerable#group_byを使う方法です。単に要素をそれら自身でグループ化し、各項目の頻度をカウントします。以下は実装方法の1つです。

collection.group_by { |item| item }.map { |key, value| [key, value.count] }.to_h

しかしながら、特にRubyの標準から見てもう少し何とかできそうな気がします。そしてもっといい方法に気づきました。Ruby 2.4ではActiveSupportのcore extensionの非常に便利なHash#transform_valuesが採り入れられました。これを応用すれば、次のように書き換えることができます。

collection.group_by { |item| item }.transform_values(&:count)

ずいぶんよくなりましたが、group_by { |item| item }のあたりをもう少し何とかできそうです。こういう場合に便利な道具がRubyにあるでしょうか?

そう、あるのです!Ruby 2.2で導入されたKernel#itselfは、単にそれ自身を返します。これを使うというアイデアは一見奇妙に思われるかもしれませんが、これがどんぴしゃりはまるのです。

collection.group_by(&:itself).transform_values(&:count)

コードがここまで美しくなりました。

まとめ

Rubyコードは気持ちよく読めることで特に知られています。そして私はRubyとこれほど長い間付き合っていても、Kernel#itselfのようにRubyという言語にトータルな美しさを付け加えるささやかなものを発見するたびに、言い知れない喜びに浸ります。

関連記事

Rubyのシンボルをなくせるか考えてみた(翻訳)

Rubyのクラスメソッドをclass << selfで定義している理由(翻訳)

Viewing all 1838 articles
Browse latest View live