Quantcast
Channel: Ruby on Railsの記事一覧|TechRacho by BPS株式会社
Viewing all 1381 articles
Browse latest View live

週刊Railsウォッチ(20180202)Rails 5.2.0 RC1と5.1.5.rc1リリース、Rails 6開発開始、メソッド絵文字化gemほか

$
0
0

こんにちは、hachi8833です。生まれて初めて皆既月食をじかに見ました。

2月最初のウォッチ、いってみましょう。

つっつき中に録音ボタンを押し忘れてしまったので、今回のつっつき成分はいつもより少なめです🙇

臨時ニュース

Rails 5.2.0 RC1と5.1.5.rc1リリースとRails 6開発開始

立て続けのリリースです。RC版の表記が「RC」と「.rc」の2とおりなのが妙に気になりました。

Rails 5.2.0 RC1リリース

最終版は2月中に出したい意向だそうです。

最初のRails 5.2リリースから2か月の間、最初のRCのためにあらゆる方法で改良・調整を行ってきました。

今回の目玉機能として、Active Storageフレームワークをdeeper content-type identificationで拡張するなど多くの改良が行われました。Active StorageはBasecampなどでさらに数か月production環境での試練を重ねてきました。すぐに使える堅固なフレームワークです。

5.2ベータでは他にも改良を行いました。高速なフィクスチャー読み込みActive Job discard時のエラーハンドリングのカスタマイズActive Recordクエリでアクセス元サイトをログに出力する機能などです。Railsは止まりません!

リリースも間近になりました。Rails 5.2ベータは既にBasecampなど多くのサイトで数か月間productionで動作しています。次のRCまたは最終リリースの目標は、今後のissue次第ですが、2月末までとなります。今回はRCなので、既にrails/masterブランチをrails/5-2-stableに移行し、rails/masterはRails 6.0の開発に充てられています。

Ruby on Railsを支えてくれている皆さまに改めて感謝します。
プレスリリースの冒頭を抄訳

Rails 6ブランチ登場↓

Rails 5.1.5.rc1リリース

バグ修正です。変更があったのは以下です。

Rails 5.2ミニチュートリアル: 新機能とActive Storageの詳細(RubyFlowより)


evilmartians.comより

早くもRails 5.2のまとめ記事が出ています。来週翻訳いたします。

Rails: 今週の改修

公式は少なめで、3つのうち2つが上のDHHのプレスリリースにも含まれています。

フィクスチャ挿入時のマルチステートメントクエリをサポートして高速化、insert_fixturesは非推奨に

eachで回さなくても引数でbuild_sqlに渡せるようになりました。

  # 従来
  %w(authors dogs computers).each do |table|
    sql = build_sql(table)
    connection.query(sql)
  end

  # 変更後
  sql = build_sql(authors, dogs, computers)
  connection.query(sql)

ActiveRecordのdiscard_onがブロックを取れるようになった

# activejob/lib/active_job/exceptions.rb
       def discard_on(exception)
         rescue_from exception do |error|
-          logger.error "Discarded #{self.class} due to a #{exception}. The original exception was #{error.cause.inspect}."
+          if block_given?
+            yield self, exception
+          else
+            logger.error "Discarded #{self.class} due to a #{exception}. The original exception was #{error.cause.inspect}."
+          end
         end
       end

ActiveRecordクエリでメソッド呼び出し元の位置をdevelopmentログに出力

以下のようにapp/models/version.rb:247:in 'downloads_count'などと出力されます。

Started GET "/news/popular" for ::1 at 2016-10-19 00:57:48 +0200
Processing by NewsController#popular as HTML
  Version Load (57.3ms)  SELECT  "versions".* FROM "versions" INNER JOIN "rubygems" ON "rubygems"."id" = "versions"."rubygem_id" LEFT OUTER JOIN "gem_downloads" ON "gem_downloads"."rubygem_id" = "rubygems"."id" AND "gem_downloads"."version_id" = $1 WHERE ("versions"."created_at" BETWEEN '2016-10-11 22:57:48.145796' AND '2016-10-18 22:57:48.145965') AND "versions"."indexed" = $2  ORDER BY gem_downloads.count DESC, "versions"."created_at" DESC LIMIT 10 OFFSET 0  [["version_id", 0], ["indexed", "t"]]
  ↳ app/views/news/show.html.erb:9:in `_app_views_news_show_html_erb___2784629296874387000_70222193538980'
  Rubygem Load (0.4ms)  SELECT  "rubygems".* FROM "rubygems" WHERE "rubygems"."id" = $1 LIMIT 1  [["id", 19969]]
  ↳ app/views/news/_version.html.erb:1:in `_app_views_news__version_html_erb__2744651331114605013_70222191156360'
  Version Load (0.8ms)  SELECT  "versions".* FROM "versions" WHERE "versions"."rubygem_id" = $1 AND "versions"."latest" = $2  ORDER BY "versions"."position" ASC LIMIT 1  [["rubygem_id", 19969], ["latest", "t"]]
  ↳ app/helpers/application_helper.rb:23:in `gem_info'
  GemDownload Load (0.3ms)  SELECT  "gem_downloads".* FROM "gem_downloads" WHERE "gem_downloads"."version_id" = $1 AND "gem_downloads"."rubygem_id" = $2 LIMIT 1  [["version_id", 882133], ["rubygem_id", 19969]]
  ↳ app/models/version.rb:247:in `downloads_count'

つっつきボイス:activerecord_cause gem要らなくなったか…と思ったら昨年末のウォッチで#26815扱ってた」「いい機能!」

PostgreSQL外部テーブルをサポート

# activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#530
+        def foreign_tables
+          query_values(data_source_sql(type: "FOREIGN TABLE"), "SCHEMA")
+        end
+
+        def foreign_table_exists?(table_name)
+          query_values(data_source_sql(table_name, type: "FOREIGN TABLE"), "SCHEMA").any? if table_name.present?
+        end

参考: 9.3で進化した外部テーブル — スライド

Rails

Rails + PostgreSQLでマルチテナントジョブキューを構築

ジョブキューのソリューションが乱立する中、PostgreSQLでやってみたそうです。

Passenger 5.2.0リリース: 非互換の変更あり


phusion.nlより

  • Ruby 2.5.0に対応
  • Apache integrationモードでPassengerResolveSymlinksInDocumentRootなどを廃止
  • 設定オプションのリファクタリング: あいまいな設定名の修正や設定の自動化など
  • etc.

EXTRACT関数を使って日付をうまく扱う(RubyFlowより)

# 同記事より
Order.where("EXTRACT(year FROM created_at) = EXTRACT(year FROM now())")

つっつきボイス: 「Karolさんの記事はいい感じのが多くて好きです」「EXTRACTは某案件で使ったことあるけど、whereEXTRACTしてるのか」「これインデックス効くんだろうか?と思ったら文中にも書いてあった↓: functional index作っとけって」

And the great thing is that you can also create a functional index for EXTRACT(year FROM created_at) to avoid sequential scanning and get much better performance.
同記事より

条件節をいい感じの名前のメソッドに切り出す(RubyFlowより)

# 同記事より
  def good?
    clean? && local? && excellent_coffee?
  end

  private

  def clean?
    !filthy?
  end
...
  def filthy?
    @name == 'Barry’s Caff'
  end

つっつきボイス: 「Andy Crollさんの記事はきっちりテンプレ化されてますね」「リファクタリングはわかるけど、filthy(不潔な)とか'Barry’s Caff'ってスゴいですね」「清潔とは『不潔じゃない』こと」

Tully’s Coffeeのもじりかもと思えてきました。

PowをPumaに切り替えた話(RubyFlowより)


つっつきボイス: 「Rails始めた頃、深く考えずにサーバーをPowとかthinとか取っ替え引っ替えしてました」「PowってMac専用なんですね」

Rails 5.2の例のCurrent


つっつきボイス: 「記事を引用いただいてうれしい: ありがとうございます」

localer: i18nの翻訳漏れを検出するgem

非常に新しいgemですが、Evil Martiansがスポンサーになっていますね。

  • 訳が抜けているymlの検出(パスの指定も可能)
$ localer check /path/to/rails/application
  • 特定のlocale/語のキーをオフにする
Locale:
  EN:
    Exclude:
      - /population\z/
      - .countries.france
  • CI統合
# .travis.yml

# other configuration options
script:
  - bundle exec bundle-audit
  - bundle exec rubocop
  - bundle exec rspec
  - bundle exec localer

i18n_generators: localeのyamlを自動生成&訳語追加

amatsudaさんによる有名なgemですが一応。Rails 5.1にも対応しています。


同リポジトリより

% rails g i18n_translation ja (de-AT, pt-BR, etc.)
# config/locales/translation_ja.ymlが生成される

つっつきボイス: 「これって自動翻訳までしてくれるのかな?」

調べてみると、2012年にはQiitaに「残念ながら現状は翻訳が全く行われない状態になってしまっています」とありましたが、手元でインストールしてrails g i18n_translation jaを実行してみたら見事自動翻訳できました。先のlocalerと合わせるとさらによさそうですね。

なお類似のhaml-i18n-extractorというローカライズ文字列抽出用gemも見つけたのですが、こちらは数年以上更新がありません。

その他Rails小物記事

Ruby trunkより

メソッド参照のシンタックスシュガーが欲しい(継続)

この間ウォッチで取り上げたyield_self is more awesome than you could thinkでこれに言及していました。method(:メソッド名)をJava 8みたいに簡潔に書けるようにしたいそうです。

#13581より
map(&Math->sqrt) (and just each(&->puts) probably?) -- Matz is explicitly against it;
map(&Math\.sqrt) (not sure about puts);
map(&Math.m(:sqrt)), each(&m(:puts)) (just shortening, no language syntax change)
map(&Math.:sqrt), each(&.:puts)
map(&Math:sqrt), each(&self:puts)
map(&Math#sqrt), each(&#puts) (it was my proposal, "just how it looks in docs", but I should reconsider: in docs it is Math::sqrt, in fact)
map(&Math:::sqrt), each(&:::puts)
map(&~>(:sqrt, Math)), each(&~>(:puts))
several by @Papierkorb:
map(&Math.>sqrt), each(&.>puts) (nobu (Nobuyoshi Nakada): conflicts with existing syntax)
map(&Math<sqrt>), each(&<puts>) (nobu (Nobuyoshi Nakada): conflicts with existing syntax)
map(&Math&>sqrt), each(&&>puts)
map(&Math|>sqrt), each(&|>puts) (too confusable with Elixir-like pipe, probably)

まだまだ続きそうな雰囲気です。


つっつきボイス: 「おーJava 8とな」「ASCIIに記号足りないですね」「ファットアロー=>の出番かな(ぶつかる)」「記号増やすとパーサーいじらないといけないから面倒」「Gobyならパーサーいじるのそんなに大変じゃないっす」

Java 8では::を使うそうです。

参考: java8 メソッド参照

URI#secure?が欲しい(継続)

uri.instance_of?(URI::HTTPS)
url.secure? # 上をこう書けたらいいな

2.3/2.4でメモリリーク(修正済み)

require 'pathname'

puts Process.pid

puts ARGV[0]
(ARGV[0] || 1).to_i.times { $LOAD_PATH.unshift(Pathname.new(__dir__) ) }

dot      = "."
filename = "ostruct"
1000.times { 1000.times { require filename }; print dot; GC.start; }

STDOUT.puts "exit?"
STDIN.gets
1 74.6 MB
2 149.5 MB
3 214 MB
4 290 MB
5 353.6 MB
9 575.4 MB
10 650.6 MB

Ruby

Reduxのストアを理解するためにRubyでちょっと再実装してみた

# 同記事より
class ReduxStore
  attr_reader :current_state

  def initialize(reducer)
    @reducer = reducer
    @listeners = []
    @current_state = nil
    dispatch({})
  end

  def dispatch(action)
    @current_state = @reducer.call(@current_state, action)
    @listeners.each { |l| l.call }
  end

  def subscribe(listener)
    @listeners.push(listener)
    ->{ @listeners.delete(listener) }
  end
end

参考: ReduxのAction、Reducer、Storeの(個人的な)整理メモ

RubyプロセスのメモリアロケーションをeBPFで調べる

著者のJulian Evansさんはrbspyというツールをつい最近公開してたちまち★900超えです。Noah Gibbs氏も絶賛。


つっつきボイス: 「eBPFはBPF(Berkeley Packet Filter)の拡張版か」「この人は後述のMacのカーネルバグを見つけた人」「レベル高い…」

参考: LinuxのBPF : (3) eBPFの基礎

abstriker: Rubyで抽象クラスを使うgem

# リポジトリより
class A1
  extend Abstriker

  abstract def foo
  end
end

class A3 < A1
  def foo
  end
end # => OK

class A2 < A1
end # => raise

Class.new(A1) do
end # => raise

つっつきボイス: 「Rubyでabstract!」「上位クラスでメソッドが宣言されてなかったらraiseするのか」「abstractの引数に埋まってるシンボルを取り出して探索してるみたいです」「Rubyだとコンパイル時に発見できないから実行しないと出ないですけどね」「マジック使わないとできないんでしょうね」「これは実装大変そう…」

Kiba: Ruby向けデータ処理&ETLライブラリ


kiba-etl.orgより

# Wikiより
job = Kiba.parse do
  source MySource
  transform MyFirstTransform
  transform { |r| r.merge(extra_field: 10) }
  destination MyDestination
end

Kiba.run(job)

つっつきボイス: 「Kibaって牙なのか(木場ジャナカッタ)」「まだ使い道がよくわからないけど、何かに使えそうな気がします」

参考: Wikipedia-ja Extract/Transform/Load(ETL)

rucc: Rubyで書かれたCコンパイラ

$ rucc -c hello.c
$ ls
hello.c  hello.o

つっつきボイス: 「何て読むのかな」「るーしーしー?」

Rubyのシンボルその後

Rubyのテスト環境

今年のRubyKaigi会場

こちらだそうです。

SQL

PostgreSQLでVACUUMしていいとき/いけないとき(Postgres Weeklyより)


つっつきボイス: 「VACUUMときたらPostgreSQL: 他では見かけない用語ですね」

参考: PostgreSQL 9.4.5 VACUUM
参考: PostgreSQL の VACUUM をなんとなくでするのはやめよう

PostgreSQLのストアドプロシージャ記事2本(Postgres Weeklyより)


つっつきボイス: 「ストアドプロシージャはORMフレームワークとかと相性悪いのでWebの人は知らなかったりするかも: 好きではないけど触ったことぐらいはある」「Railsでマイグレーションするときとかに困りそう」

ネットワーク設定のミスでテーブルが肥大化(Postgres Weeklyより)

PostgreSQL起動前にIPv6を殺すことで修正できたそうです。先週ご紹介したPostgreSQL設定ツールのCyberTecのブログです。


pgconfigurator.cybertec.atより

JavaScript

jQuery 3.3.0登場-> 3.3.1修正リリース

3.3.0に不足していたdependencyを3.3.1で追加したそうです。

参考: jQuery 3.3登場、約1年ぶりのアップデート。新機能も追加

今人気のJavaScriptリポジトリ

一番人気はFacebookのcreate-react-appでした。


同リポジトリより

JavaScriptエラートップテンと回避方法


同記事より

AutoprefixerからPostCSSへ


同記事より


つっつきボイス: 「ちょっと前に翻訳したWebpack記事↓にもPostCSSが登場してたけど、位置づけがよくわかってなかった」「PostCSSはJS版Sass的なやつ」「あ、Rubyに依存しないのか」
PostCSSの作者はロシア人だそうで、だからこのロシア正教っぽい画像↑なのかな」

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

CSS/HTML/フロントエンド

不適切に発行されたメジャーな証明書がChrome/Firefoxで順次無効になる


同記事より


つっつきボイス: 「これねー」「気が重い…」

参考: 世界30%のSSL証明書が3月と10月に強制無効化!? あなたのサイトが大丈夫か確認する3ステップ

GitHubの「草」をカスタマイズしてみた(Frontend Focusより)

See the Pen GitHub Contribution Graph in CSS Grid Layout by Ire Aderinokun (@ire) on CodePen.

同記事より

Variable Font記事2本(Frontend Focusより)

html {
  font-family: 'SourceSans' sans-serif;
  font-weight: 400;
}

@supports (font-variation-settings: "wght" 400) {
  html {
    font-family: 'SourceSansVariable', sans-serif;
    font-variation-settings: "wght" 400;
  }
}


webdesign.tutsplus.comより


つっつきボイス: 「1つのフォントでいろんな書体を使えるんですね」「日本語だとあまり要らなさそうだけどアルファベット文化圏では欲しいやつかも」

参考: Variable Fontについて

フロントエンドは複雑になる宿命(Frontend Focusより)


同記事より

Indexed Database APIの2.0が公開(Frontend Focusより)

// w3.orgより
var tx = db.transaction("books", "readwrite");
var store = tx.objectStore("books");

store.put({title: "Quarry Memories", author: "Fred", isbn: 123456});
store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567});
store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678});

tx.oncomplete = function() {
  // All requests have succeeded and the transaction has committed.
};

参考: MDN IndexedDB

IndexedDB は SQL ベースの RDBMS に似たトランザクショナルデータベースシステムですが、SQL ベース の RDBMS が固定された列を持つテーブルを使用するのに対して、IndexedDB は JavaScript ベースのオブジェクト指向データベースです。IndexedDB では、キーでインデックス付けされたオブジェクトを保存および取り出すことができます。Structured Clone アルゴリズムがサポートする、任意のオブジェクトを保存できます。データベースのスキーマを定義する、データベースへの接続を確立する、そして一連のトランザクションでデータの取り出しや更新を行うことが必要です。
developer.mozilla.orgより

その他

開発者が死守すべき最低限のエチケット(Frontend Focusより)


同記事より

  • インデントをちゃんとつける
  • ファイル配置はディレクトリで構造化する
  • 必要なコメントを書く
  • ドキュメントを書く
  • fooとかbarとか使うな
  • 意味不明なファイル名を使わない
  • 大文字小文字の区別(capitalization)をきちんと行う
  • DRYを心がける

書籍『[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識』予約開始

ドラフト: Javaにraw string literalを追加

Rubyのヒアドキュメント的な書き方ができるようになるそうです。

// 従来
String html = "<html>\n" +
              "    <body>\n" +
              "         <p>Hello World.</p>\n" +
              "    </body>\n" +
              "</html>\n";
// 案
String html = `<html>
                   <body>
                       <p>Hello World.</p>
                   </body>
               </html>
              `;

つっつきボイス: 「そうそう、これJavaで辛かった」「Rubyのヒアドキュメントってあまり使わなかったけど実はいいヤツなんですね」

CoreOSがRed Hat傘下に


つっつきボイス: 「これワンピースじゃないっすかw」「ああ、ワタイにわからんネタでした」「k8sって?」「Kubernetesか」

macOSでカーネルバグを見つけた

原因は不明ですが、execとtask_for_pidの間でsleepさせることで回避できるそうです。

参考: 17行のCコードでMacのシステム全体をフリーズさせられることが出来る不具合がmacOS High Sierraで確認される。

BSDは死につつある?

以下のyomoyomoさんの紹介を見るのが早いです。

参考: BSDは死につつある? 一部のセキュリティ研究者はそう考えている

TSL 1.1の廃止が延期に

参考: SSL3.0, TLS1.0~1.2の微妙な違いのまとめ

日本の改元、間が悪そう

Unicode 12のリリースが今年の2月なので、元号文字の追加はへたすると2020年のUnicode 13までかかるかもです。

おまいらの出会った最悪のコーディング文化を晒すスレ

開くたびに増えてます。

Goで書いたコードがヒープ割り当てになるかを確認する方法

この記事で絶賛されているAllocation Efficiency in High-Performance Go Servicesという記事がとても濃厚です。

番外

active_emoji: Rubyの主要メソッドを絵文字化するgem

# 同リポジトリより
class Array
  alias ⏪  <<
  alias 🈴 concat
  alias 💧 drop
  alias 🔁 each
  alias 🈳❓ empty?
  alias 🍀 sample
  alias 🎲 sample
  alias ♻ shuffle
  alias 👈 push
  alias 🍕 slice
end

class Object
  alias ⛄❓ frozen?
  alias ❄❓ frozen?
  alias ⛄ freeze
  alias ❄ freeze
  alias :"#⃣" hash
  alias 🔬 inspect
  alias 🆔 object_id
  alias 🚰 tap
end

module Kernel
  alias 🆚 <=>
  alias 📎 binding
  alias 🔲❓ block_given?
  alias 📥 gets
  alias 🔁 loop
  alias 📠 print
  alias 📤 puts
  alias 🎰 rand
  alias 👻 singleton_class
  alias 💤 sleep
  alias 💻 system
  alias ⚠ warn
  def 🔟; 10 end
  def 💯; 100 end
end

class String
  alias ⏪  <<
  alias ✖ *
  alias ➕ +
  alias 🔪 chop
  alias 🔪❗ chop!
  alias 🔡 downcase
  alias 🔡❗ downcase!
  alias 🈳❓ empty?
  alias 🍌 split
  alias 🔠 upcase
  alias 🔠❗ upcase!
  alias 📏 length
end


同リポジトリより


つっつきボイス: 「完璧にお遊びっすね」「こ、これはw」「何だかファイトが湧いてくるちきしょう、今夜読み解いてやる」「バナナ🍌が何でsplit?」「バナナスプリットってデザートありますね」「🔁🍕each_sliceとか」「🔁eachというのはちょっとー」

タイポ

/* dixyes/mianwrapperより */
#define mian main
#define ture true

つっつきボイス: 「やめて~」

ミトコンドリア


今週は以上です。

バックナンバー(2018年度)

週刊Railsウォッチ(20180126)Bootstrap 4登場でbootstrap_form gemが対応、PostgreSQLやnginxの設定ファイル生成サービスほか

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

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

Rails公式ニュース

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Focus

frontendfocus_banner_captured


Rails: パーシャルと`collection:`でN+1を回避してビューを高速化(翻訳)

$
0
0

概要

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

Rails: パーシャルとcollection:でN+1を回避してビューを高速化(翻訳)

Railsでビューのレンダリング(特にパーシャル)を正しく行うことの重要性に気づいてない人をよく見かけます。本記事では、さまざまなアプローチのパフォーマンスの相対数値を比較します。このトピックは、多くのブログ記事で見落とされがちです。

N+1クエリの回避

最初の重要なトピックは「N+1クエリ」です。N+1クエリをのさばらせると速度低下が不可避になり、その他のパフォーマンス最適化も効かなくなってしまうことがあるため、ぜひとも回避しましょう!

非常にシンプルな例から見てみましょう。

<% @users.each do |*user*| %> <div class="post">
      <%= *user*.post.title %> </div>
<% end %>

usersのリストを反復して、各userpostとしています。簡単ですね。

それでは、ユーザー1000人を以下のコードでassignしてテストしてみましょう。

assign(:users, *User*.all)

結果は以下のとおりです。

Warming up --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
          with N+1 1.000 i/100ms
Calculating --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- -
          with N+1 0.291 (± 0.0%) i/s --- 2.000 in 7.158333s

1秒あたりでレンダリング可能なビュー数は0.291です。これはかなり残念な数値なので、ビューで発生しているN+1クエリを最初に解決しましょう。現在のビューでは、イテレーションのたびにDBでSELECTを実行してuserpostを取り出しています。

N+1を今後も解決するためにBullet gemを導入します。私はBulletが大好きです❤。次の3つの理由からGoldiloaderよりもBulletが好みです。

  1. Bulletのコード自動更新は変更してよいかどうかをプロンプトで確認してくれる。
  2. Bulletはproduction環境では実行できないようになっている。
  3. BulletはN+1クエリを隠蔽せず、検出と理解を支援してくれる。

GemfileにBulletを追加してtest環境向けに設定します。これによって、N+1クエリで例外がraiseされ、解決しないとテストを完了できないようになります。

# app/config/environments/test.rb

config.after_initialize do
  Bullet.enable = true
  Bullet.bullet_logger = true
  Bullet.raise = true
end

テストを再実行すると、必要な情報をBulletが表示してテストを失敗させます。

Bullet::Notification::UnoptimizedQueryError:

USE eager loading detected
 User => [:post]
 Add to your finder: :includes => [:post]

そしてassignを以下のように変更します。

assign(:users, User.includes(:post))

修正後の結果は7倍ほど高速になりました。

Warming up --------------------------------------
      with N+1     1.000  i/100ms
   without N+1     1.000  i/100ms
Calculating -------------------------------------
      with N+1      1.539  (± 0.0%) i/s -      8.000  in   5.305367s
   without N+1     10.479  (± 9.5%) i/s -     52.000  in   5.057764s

Comparison:
   without N+1:       10.5 i/s
      with N+1:        1.5 i/s - 6.81x  slower

パーシャルのレンダリング

それでは本記事の本題であるパーシャルの利用に進みましょう。個別のpostをパーシャルに切り出してコードをリファクタリングすることにします。これはよい方法ですが、次のような駄目リファクタリングがあることも知っておきましょう。

1. パーシャルを切り出す。

<div class="post">
  <%= user.post.title %>
</div>

2. レンダリングする。

<% @users.each do |user| %>
    <%= render 'erb_partials/post', user: user %>
<% end %>

パフォーマンスは以下のようになります。

Warming up --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
 inline 1.000 i/100ms
 partial 1.000 i/100ms
Calculating --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- -
 inline 11.776 (± 8.5%) i/s --- 59.000 in 5.085002s
 partial 5.648 (±17.7%) i/s --- 28.000 in 5.043322s

Comparison:
 inline: 11.8 i/s
 partial: 5.6 i/s --- 2.09x slower

なるほど、確かにコードが2倍も遅くなってしまいました。これはパーシャルのレンダリングを反復しているのが原因です。Railsはuserごとにパーシャルを「オープン」して評価しなければならなくなります。これを解決するにはcollectionを使います。

ビューを次のように変更します。

<%= *render* partial: 'erb_partials/post', collection: @users, as: :user %>

変更後の結果は以下のとおりです。

Warming up --------------------------------------
        inline     9.000  i/100ms
       partial     1.000  i/100ms
    collection     6.000  i/100ms
Calculating -------------------------------------
        inline     96.394  (±11.4%) i/s -    477.000  in   5.016304s
       partial      8.989  (±22.2%) i/s -     43.000  in   5.108843s
    collection     57.828  (±13.8%) i/s -    288.000  in   5.092763s

Comparison:
        inline:       96.4 i/s
    collection:       57.8 i/s - 1.67x  slower
       partial:        9.0 i/s - 10.72x  slower

パフォーマンスを落とさずにコードをパーシャルに切り出せたことがわかります。修正後のコードでは、Railsによるパーシャルの評価は1回で完了し、その後userごとにレンダリングを行います。これは、先のN+1クエリで行った修正と同じに考えることができます。すなわち、この修正によってもコードをスケール可能にできるのです。

追伸

実際には(本記事のように)同一ページに1000ユーザーを表示するのはやめ、ページネーションを実装しましょう。
本記事で用いたコードはGitHubの私のリポジトリでご覧いただけます。

ボーナス: 他のレンダリングエンジンを試してみる

先ほどはerbで比較しましたが、slimhamlではどうなのでしょうか?以下は比較の結果です。

Warming up --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
 erb inline 8.000 i/100ms
 erb partial 1.000 i/100ms
 erb collection 5.000 i/100ms
 slim inline 10.000 i/100ms
 slim partial 1.000 i/100ms
 slim collection 6.000 i/100ms
 haml inline 9.000 i/100ms
 haml partial 1.000 i/100ms
 haml collection 4.000 i/100ms
Calculating --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- -
 erb inline 97.943 (±10.2%) i/s --- 488.000 in 5.046691s
 erb partial 9.438 (±21.2%) i/s --- 46.000 in 5.026193s
 erb collection 67.090 (± 6.0%) i/s --- 335.000 in 5.015540s
 slim inline 104.373 (± 8.6%) i/s --- 530.000 in 5.122621s
 slim partial 9.836 (±20.3%) i/s --- 49.000 in 5.123851s
 slim collection 69.146 (± 7.2%) i/s --- 348.000 in 5.059607s
 haml inline 85.732 (±11.7%) i/s --- 432.000 in 5.111380s
 haml partial 8.180 (±24.4%) i/s --- 40.000 in 5.165770s
 haml collection 41.069 (±21.9%) i/s --- 196.000 in 5.084682s

Comparison:
 slim inline: 104.4 i/s
 erb inline: 97.9 i/s --- same-ish: difference falls within error
 haml inline: 85.7 i/s --- same-ish: difference falls within error
 slim collection: 69.1 i/s --- 1.51x slower
 erb collection: 67.1 i/s --- 1.56x slower
 haml collection: 41.1 i/s --- 2.54x slower
 slim partial: 9.8 i/s --- 10.61x slower
 erb partial: 9.4 i/s --- 11.06x slower
 haml partial: 8.2 i/s --- 12.76x slower

いずれの場合も、インラインのレンダリングが最もよい結果を残しています。slimのパフォーマンスはerbと同程度(またはわずかに上回る)で、hamlのレンダリング時間はパーシャルでやや落ちるようです。

結論

  • パーシャルを使いましょう。ためらう必要はありません。
  • インラインレンダリングは確かに高速ですが、コードのメンテナンス性/読みやすさ/テストのしやすさも重要です。
  • すなわち、必要に応じてビューをパーシャルに分割しましょう。
  • ただし、collectionを使って正しく行いましょう。
  • もちろんN+1クエリは避けましょう。

関連記事

Railsのurl_helperの速度低下を防ぐコツ(翻訳)

Railsアプリのアセットプリコンパイルを高速化するコツ(翻訳)

Rails5「中級」チュートリアル(1)序章とセットアップ(翻訳)

$
0
0

概要

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

Rails5中級チュートリアルはセットアップが短めで、RDBMSにはPostgreSQL、テストにはRSpecを用います。
原文が非常に長いので分割します。章ごとのリンクは順次追加します。

注意: Rails中級チュートリアルは、Ruby on Railsチュートリアル(https://railstutorial.jp/)(Railsチュートリアル)とは著者も対象読者も異なります。

Rails5「中級」チュートリアル(1)序章とセットアップ(翻訳)

初めてアプリを作る方法を解説するチュートリアルはネット上に山ほどあります。本チュートリアルは、そこからさらに進んで、もっと複雑なRuby on Railsアプリを作る方法を詳しく解説いたします。

本チュートリアルでは、折に触れて新しい技術や概念を順次導入していきます。すなわち、どの新しいセクションにも学ぶべき新しいことが記されています。

本チュートリアルで扱うトピックには以下のものがあります。

  • Ruby On Railsの基礎
  • リファクタリング(ヘルパー、パーシャル、concern、デザインパターン)
  • テスト: TDD/BDD(RSpecとCapybara)、ファクトリー(FactoryBot)
  • ActionCable
  • ActiveJob
  • CSS、Bootstrap、JavaScript、jQuery

どんなアプリを作るか

自分と考えの近い人を探して出会えるプラットフォームを作ることにします。

このアプリの主要な機能は次のとおりです。

  • 認証(Deviseを使用)
  • 投稿の表示、検索、分類
  • インスタントメッセージ(ポップアップウィンドウと独自のメッセンジャー)
    • 非公開チャットやグループチャットを作成可能
  • ユーザーをつながり(contact)に追加する
  • リアルタイム通知

完成したアプリの外観については以下の動画でご覧いただけます。

完成したソースコードはGitHubのdomagude/collabfieldにあります。

目次

1-1 必要なもの

コードのあらゆる行について、その書き方を選んだ経緯を解説します。まったくの初心者であっても本チュートリアルを終えることは一応可能ですが、本チュートリアルのトピックの中には初心者には手に余る内容も含まれている点にご注意ください。

したがってまったくの初心者にとっては、学習曲線をかなり急上昇することになるので厳しくなるでしょう。新しい概念について触れるたびに、補足情報へのリンクを置くようにします。

理想的には、以下について基礎的な知識を持ち合わせているのがベストです。

1-2 セットアップ

基本的なRuby on Railsの開発環境が設定済みであることが前提です。まだの方はRailsInstallerをチェックしてください。

ある時期の私はWindows 10で開発していました。最初のうちはよかったのですが、やがてWindowsが引き起こすさまざまな謎障害をつぶして回る作業に疲れてしまいました。自分のアプリを動かすためにさまざまな裏技を繰り出さなければならず、これは時間の無駄だと気づきました。こうした障害を克服しても実になるスキルや知識は何も得られず、結局Windows 10でのセットアップでジタバタしただけで終わってしまったのです。

そういうわけで私はバーチャルマシン(VM)での開発に切り替えました。私が選んだのは、Vagrantに開発環境を構築してPuTTY でVMに接続する方法です。Vagrantを使ってみたい方は、こちらのチュートリアル動画が便利です。

1-3 新しいアプリを作る

データベースには、Ruby on Railsコミュニティで人気の高いPostgreSQLを使うことにします。PostgreSQLを使うRailsアプリを作ったことのない方は、こちらのチュートリアルをご覧ください。

PostgreSQLに慣れている方は、コマンドプロンプトを開いてプロジェクトを置きたいディレクトリに移動しましょう。

新しいアプリを作成するには以下を実行します。

rails new collabfield --database=postgresql

アプリの名前はCollabfieldとします。RailsはデフォルトではSQLite3を使いますが、ここではPostgreSQLを使いたいので、以下のオプションを追加することで指定する必要があります。

--database=postgresql

これで新しいアプリを生成できたはずです。

以下を実行して、作成したディレクトリに移動します。

cd collabfield

これで、以下を入力すればアプリを実行できます。

rails s

アプリを起動したので、どのように表示されるかを確認できるようになりました。ブラウザでhttp://localhost:3000を開きます。問題がなければ、Railsのwelcomeページが以下のように表示されるはずです。


  • 次回: Rails5「中級」チュートリアル(2)レイアウト(翻訳)

関連記事

Rails5「中級」チュートリアル(2)レイアウト(翻訳)

$
0
0

概要

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

Rails5中級チュートリアルはセットアップが短めで、RDBMSにはPostgreSQL、テストにはRSpecを用います。
原文が非常に長いので分割します。章ごとのリンクは順次追加します。

注意: Rails中級チュートリアルは、Ruby on Railsチュートリアル(https://railstutorial.jp/)(Railsチュートリアル)とは著者も対象読者も異なります。

目次

Rails5「中級」チュートリアル(2)レイアウト(翻訳)

それではコーディングを始めます。どこから始めてもよいのですが、私はWebサイトを新しく作るときは基本的な表示構造を最初に作ってから他のものを作るのが好みなので、この方法で進めることにしましょう。

2-1 Homeページ

今はhttp://localhost:3000を開くとRailsのwelcomeページが表示されるので、これを独自のデフォルトページに変えることにします。そのためには、Pagesコントローラを生成します。Railsのコントローラに慣れていない方は、Action Controllerを読んでRailsのコントローラの概要を理解しておいてください。コマンドプロンプトで以下を実行して、新しいコントローラを生成します。

rails g controller pages

このRailsジェネレータによっていくつかのファイルが作成されます。コマンドプロンプトの出力は以下のような感じになります。

このPagesControllerを使って、特殊な静的ページを制御します。テキストエディタでCollabfieldプロジェクトを開きましょう。私はSublime Textを使っていますが、お好みのエディタで構いません。

pages_controller.rbファイルを開きます。

app/controllers/pages_controller.rb

homeページの定義はここで行います。もちろん、homeページを別の方法で他のコントローラに定義することも可能ですが、私はhomeページをPagesControllerの内部で定義するのが好みです。

pages_controller.rbを開くと以下のようになっています(Gist)。

# app/controllers/pages_controller.rb
class PagesController < ApplicationController
end

これはPagesControllerという名前の空のクラスになっており、ApplicationControllerクラスを継承しています。ApplicationControllerのソースコードはapp/controllers/application_controller.rbにあります。

私たちが今後作成するコントローラは、すべてApplicationControllerクラスから継承します。つまり、ApplicationControllerクラスで定義したメソッドはすべてのコントローラで利用できるようになります。

indexという名前のpublicメソッドを定義しましょう。これはアクションとして呼び出せます(Gist)。

# controllers/pages_controller.rb
class PagesController < ApplicationController

  def index
  end

end

Action Controllerの概要をお読みになった方ならおわかりのように、呼び出されるコントローラとそのpublicメソッド(アクション)は「ルーティング」によって決定されます。それではルーティングを定義して、Webサイトのrootページを開いたときに呼び出されるコントローラとアクションをRailsに認識させてみましょう。app/config/routes.rbファイルを開きます。

Railsのルーティングがわからない方は、この機会にガイドのRailsのルーティングをじっくり読んでルーティングに慣れておきましょう。

ルーティングに以下のコードを追加します。

root to: 'pages#index'

追加後のroutes.rbは以下のようになります(Gist)。

# app/config/routes.rb
Rails.application.routes.draw do
  root to: 'pages#index'
end

Rubyのハッシュシンボル#はメソッドを文章の中で表記するときに使います。アクションは単なるpublicメソッドなので、pages#indexは「PagesControllerのpublicメソッド(アクション)であるindexを呼び出す」という意味です。

rootパスhttp://localhost:3000を開くと、このindexアクションが呼び出されます。しかしレンダリングするテンプレートが未定義のままなので、indexアクションに対応するテンプレートを新しく作ってみましょう。app/views/pagesディレクトリに移動してindex.html.erbファイルを開きます。このファイルには、通常のHTMLの他にERB(Embedded Ruby)コードを書けます。このファイルに以下のように書けば、テンプレートがブラウザに表示されるようになります。

<h1>Home page</h1>

http://localhost:3000を開くと、デフォルトのRails情報ページの代わりに以下のような感じで表示されるはずです。

これでやっと基本的な出発点にたどり着きました。ここからWebサイトに新しい要素を導入していきます。このあたりでgitの最初のcommitを行うのがよいでしょう。

コマンドプロンプトで以下を実行します。

git status

以下のような結果が出力されます。

参考までに、新しいRailsアプリを生成すると、新しいローカルgitリポジトリも初期化されます。

以下を実行して、現在の変更内容をすべて追加します。

git add -A

続いて以下を実行し、変更内容をすべてcommitします。

git commit -m "Generate PagesController. Initialize Home page"

試しに以下を実行してみましょう。

git status

変更点がすべてcommitされたので、「nothing to commit」と表示されます。

2-2 Bootstrap

ナビゲーションバーやレスポンシブなグリッドシステムを使えるよう、Bootstrapライブラリを使うことにします。Bootstrapを使うには、エディタでGemfileにbootstrap-sass gemを追加する必要があります。エディタでGemfileを開きます。

collabfield/Gemfile

bootstrap-sass gemをGemfileに追加します。ドキュメントにも書かれているように、sass-rails gemが存在していることも確認する必要があります。

...
gem 'bootstrap-sass', '~> 3.3.6'
gem 'sass-rails', '>= 3.2'
...

ファイルを保存して以下を実行し、追加したgemをインストールします。

bundle install

アプリを実行中の場合は、Railsサーバーを再起動して新しいgemを利用できるようにします。サーバーを再起動するには、Ctrl + Cでサーバーを停止し、rails sコマンドを再度実行してサーバーを起動するだけで完了します。

assetsディレクトリに移動してapplication.cssファイルを開きます。

app/assets/stylesheets/application.css

コメントアウトされている行の下に以下を追加します。

...
@import "bootstrap-sprockets";
@import "bootstrap";

続いてapplication.cssapplication.scssにリネームします。この変更はRailsでBootstrapライブラリを用いるのに必要です。また、これによってSassの機能を使えるようになります。

今後Sass変数を作成する場合に備えて、すべての.scssファイルがレンダリングされるよう順序を変更する必要があります。Sass変数が使われる前に定義されるよう、順序を変更したいと思います。

これを行うには、application.scssファイルの以下の2行を削除します。

*= require_self
*= require_tree .

Bootstrapライブラリが使えるようになるまであと少しです。もうひとつやっておかなければならないことがあります。bootstrap-sassドキュメントに記載されているとおり、BootstrapのJavaScriptはjQueryライブラリに依存しています。RailsでjQueryを使えるようにするには、jquery-rails gemを追加する必要があります。

訳注: Rails 5.1以降でjQueryを使う場合、gemよりもWebpackとyarnでインストールする方法が標準になりつつあります。この時点でWebpackとyarnでjQueryをインストールする場合、gem 'jquery-rails'の代わりに以下の方法が使えます。
1. Gemfileにgem 'webpacker', github: 'rails/webpacker'を追加し、bundle installを実行する
2. rails webpacker:installを実行し、Webpackerをインストールする
3. yarn install jqueryを実行し、jQueryをインストールする
サーバー再起動後の手順は同じです。

gem 'jquery-rails'

以下を実行します。

bundle install

再度サーバーを再起動します。

最後は、アプリのJavaScriptファイルでBootstrapとjQueryをrequireする手順です。application.jsファイルを開きます。

app/assets/javascripts/application.js

以下の行を追加します(訳注: require_tree .の前の行に追加します)。

//= require jquery
//= require bootstrap-sprockets

変更をgitにcommitします。

git add -A
git commit -m "Add and configure bootstrap gem"

2-3 ナビゲーションバー

ナビゲーションバーは、Bootstrapのnavbarコンポーネントを元に今後いろいろ変更を加えます。ナビゲーションバーはパーシャルテンプレートに保存します。

この時点でこれを行う理由は、アプリのあらゆるコンポーネントを別ファイルに分割するのが望ましいためです。コンポーネントを分割することで、アプリのコードのテストや管理がずっとやりやすくなりますし、コードを複製せずにコンポーネントをアプリの別の箇所で再利用することもできます。

以下のディレクトリに移動します。

views/layouts

以下のファイルを作成します。

_navigation.html.erb

パーシャルファイルの先頭にはアンダースコア_を付け、Railsフレームワークがこのファイルをパーシャルとして認識できるようにします。これを行うには、Bootstrapドキュメントのnavbarコンポーネントのコードをこのファイルにコピペして保存します。このパーシャルをWebサイトで表示するには、コードのどこかでレンダリングする必要があります。views/layouts/application.html.erbファイルを開きます。このファイルの内容は、デフォルトで常にレンダリングされます。

application.html.erbファイルには以下のメソッドがあります。

<%= yield %>

リクエストされたテンプレートはここでレンダリングされます。HTMLファイル内でRuby構文を使うには、<% %>(ERBの書式)で囲む必要があります。ERB構文の違いについて手っ取り早く知りたい方は、StackOverflowの回答をご覧ください。

2-1 Homeページセクションでは、ルーティングを設定することでroot URLが認識されるようにしました。これにより、GETリクエストを送信してrootページに移動すると、常にPagesController#indexアクションが呼び出されます。ルーティングに対応するアクション(ここではindex)は、yieldメソッドでレンダリングされるテンプレートを用いてレスポンスを返します。homeページのテンプレートがapp/views/pages/index.html.erbにあることを思い出しましょう。

ナビゲーションバーはすべてのページに表示したいので、ナビゲーションファイルのレンダリングはデフォルトのapplication.html.erbファイル内で行います。パーシャルファイルをレンダリングするには、パーシャルへのパスを引数に与えてrenderメソッドを呼び出します。以下のようにyieldメソッドの上にrenderを追加します。

...
<%= render 'layouts/navigation' %>
<%= yield %>
...

これで、http://localhost:3000をブラウザで開くと以下のようにナビゲーションバーが表示されます。

予告のとおり、このナビゲーションバーに変更を加えることにします。最初に、<li>要素と<form>要素をすべて削除します。今後ここには独自の要素を作成します。変更後の_navigation.html.erbファイルは次のようになります。

<!-- views/layouts/_navigation.html.erb -->
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <!-- ブランド表示と、モバイル表示切り替え用のグループ化 -->
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="#">Brand</a>
    </div>

    <!-- ナビゲーションリンク/フォームなどのコンテンツをここにまとめて表示をオンオフできるようにする -->
    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <ul class="nav navbar-nav">
      </ul>

      <ul class="nav navbar-nav navbar-right">
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>

これでレスポンシブなナビゲーションバーの基本部分ができました。ここでgitに新しくcommitするのがよいでしょう。コマンドプロンプトで以下を実行します。

git add -A
git commit -m "Add a basic navigation bar"

今度はナビゲーションバーの名前をBrandからcollabfieldに変更します。Brandはlink要素なので、リンクの生成には[link_to](https://apidock.com/rails/ActionView/Helpers/UrlHelper/link_to)メソッドを使うべきです。その理由は、link_toメソッドを使う方がURIパスを簡単に生成できるからです。コマンドプロンプトでプロジェクトディレクトリに移動し、以下のコマンドを実行します。

rails routes

このコマンドは、routes.rbファイルで生成される利用可能なルーティングを表示します。以下が出力されます。

この時点のルーティングは先ほど定義した1つだけです。出力結果にPrefixカラムがあることにご注目ください。このprefixを使って、表示したいページへのパスを生成できます。パスの生成は、このprefix名の後ろに_pathを追加するだけでできます。たとえばroot_pathと書けば、rootページへのパスが生成されます。それでは、このlink_toメソッドとルーティングの力を借りてやってみましょう。

<a class="navbar-brand" href="#">Brand</a>

上のコードを以下に置き換えます。

<%= link_to 'collabfield', root_path, class: 'navbar-brand' %>

メソッドの使い方がよくわからなくても、適当にググればたいてい解説ドキュメントを見つけられることを覚えておきましょう。たまにハズレのドキュメントもあるので、その場合はもう少し丁寧にキーワードを指定してググれば、有用なブログ記事やStackOverflowの回答が見つかることもあります。

第1引数で渡す文字列は、<a>要素の値を追加します。第2引数はパスの指定に必要で、ここでパスを生成するのにルーティングが役立ちます。第3引数はオプションで、ここで渡したものはhtml_optionsハッシュに組み込まれます。ここでは、ナビゲーションバーにBootstrapを効かせたいので、navbar-brandクラスの追加が必要です。

ただいまの小さな変更をgitにcommitしましょう。この後のセクションでアプリのデザインに手を加える予定ですが、このナビゲーションバーから変更を開始します。

git add -A
git commit -m "Change navigation bar's brand name from Brand to collabfield"

2-4 スタイルシート

スタイルシートファイルの構成方法をご紹介します。Railsには、スタイルシートの構成方法についての厳密な規則はなく、人それぞれ少しずつ違っています。

ここでは、私が普段用いている構成方法をご紹介します。

  • baseディレクトリ: アプリ全体で使われるSass変数やスタイルはここに置いています(デフォルトのフォントサイズやデフォルト要素のスタイルなど)。
  • partialsディレクトリ: ほとんどのスタイルはここに置いています。このディレクトリでは、コンポーネントやページごとにスタイルを分割するようにしています。
  • responsiveディレクトリ: ここでは、異なる画面サイズごとに、異なるスタイルルールを定義しています(デスクトップ画面のスタイル、タブレット画面のスタイル、スマートフォン画面のスタイルなど)。

最初に、以下を実行してgitリポジトリで新しいブランチを切ります。

git checkout -b "styles"

これにより、新しいgit branchが作成され、自動的にそのブランチに切り替わります。今後、新たなコード変更を別ブランチ上で実装する場合はこのようにします。

別ブランチを切る理由は、現在動作しているバージョン(masterブランチ)から、プロジェクトに追加する新しいコードを切り離し、変更によってmasterブランチに悪影響が生じることのないようにするためです。

実装が終わったら、変更をmasterブランチにmergeできます。

最初にディレクトリをいくつか作成します。

  • app/assets/stylesheets/partials/layout

このlayoutディレクトリでnavigation.scssというファイルを作成し、以下のコードを追加します(Gist)。

//app/assets/stylesheets/partials/layout/navigation.scss
.navbar-default, .navbar-toggle:focus, .collapsed, button.navbar-toggle {
  background: $navbarColor !important;
  border: none;
  a {
    color: white !important;
  }
}

上のScssコードでは、navbarの背景色とリンクの色を変更しています。既にお気づきのように、aセレクタが別の宣言ブロックの内部でネストしていますが、これはSassの機能です。!importantは、デフォルトのBootstrapスタイルを強制的にオーバーライドするのに使います。最後に、この部分では色名の代わりにSass変数が使われていることにお気づきかと思います。Sass変数を使う理由は、アプリ全体で色を変更できるようにするためです。このSass変数を定義しましょう。

最初に以下のディレクトリを作成します。

app/assets/stylesheets/base

baseディレクトリの下にvariables.scssファイルを作成し、以下を定義します。

$navbarColor: #323738;

試しにこの時点でhttp://localhost:3000をブラウザで開いてみると、スタイルはまだ何も変更されていません。スタイルが反映されない理由は、2-2 Bootstrapセクションでapplication.scssファイルから以下を削除したためです。

*= require_self
*= require_tree .

上を削除したのは、すべてのスタイルが自動的にimportされることのないようにするためでした。

つまり、スタイルシートのファイルを新しく作成するときは、メインのapplication.scssファイルで(明示的に)importしなければならないということです。importを追加したapplication.scssファイルは次のようになります(Gist)。

//app/assets/stylesheets/application.scss
// ...デフォルトのコメント

// Bootstrap
@import "bootstrap-sprockets";
@import "bootstrap";

// Variables
@import "base/variables";

// Partials - メインのcssファイル
@import "partials/layout/*";

variables.scssをpartialよりも先にimportする理由は、partialで使われるSass変数より先にSass変数が定義されるようにするためです。

navigation.scssファイルのコードの冒頭に、さらに以下のCSSを追加します。

//app/assets/stylesheets/partials/layout/navigation.scss
nav {
  .navbar-header {
    width: 100%;
    button, .navbar-brand {
      transition: opacity 0.15s;
    }
    button {
      margin-right: 0;
    }
    button:hover, .navbar-brand:hover {
      opacity: 0.8;
    }
  }
}

好みに応じて、上のコードを冒頭に追加する代わりに末尾に追加しても構いません。個人的には、CSSセレクタの「詳細度」の順でCSSコードを配置およびグループ化するようにしています。繰り返しますが、CSSファイルの構成方法は人それぞれ少しずつ違っています。私の場合、詳細度の小さい(大雑把な)セレクタを上に、詳細度の大きいセレクタを下に置くようにしています。たとえば、要素型セレクタはクラスセレクタより上になり、クラスセレクタはIDセレクタより上になるといった具合です。

ここで変更をgitにcommitしましょう。

git add -A
git commit -m "Add CSS to the navigation bar"

今度は、画面を下にスクロールしてもナビゲーションバーが常に最上部に表示されるようにしたいと思います。現時点でスクロールするほどのコンテンツがありませんが、コンテンツは今後増えます。この段階でナビゲーションバーを固定しておくのがよいとは思いませんか?

これを行うには、Bootstrapのnavbar-fixed-topクラスを使います。次のように、nav要素にこのクラスを追加します。

<!-- views/layouts/_navigation.html.erb -->
<nav class="navbar navbar-default navbar-fixed-top">

ついでに、Bootstrap Grid Systemの左側の境界にcollabfieldを配置したいと思います。現在のクラスがcontainer-fluidになっているので、現時点のcollabfieldはviewportの左側境界に配置されています。これを変更するには、(2行目の)このクラスをcontainerに変更します。

変更後の_navigation.html.erbファイルは次のようになります。

<!-- views/layouts/_navigation.html.erb -->
<div class="container">

変更をcommitします。

git add -A
git commit -m "
- in _navigation.html.erb add navbar-fixed-top class to nav.
- Replace container-fluid class with container"

http://localhost:3000をブラウザで開くと、Home pageというテキストがナビゲーションバーの下に隠れてしまいました。これはnavbar-fixed-topクラスのせいです。この問題を解決するには、navigation.scssに以下を追加して<body>要素を下に下げます。

//app/assets/stylesheets/partials/layout/navigation.scss
body {
 margin-top: 50px;
}

これで、アプリは以下のように正常に表示されるはずです。

変更をcommitします。

git add -A
git commit -m "Add margin-top 50px to the body"

先ほど、新しいブランチを切ってそこに切り替えて作業していたのを思い出しましょう。ここでmasterブランチに戻ることにします。

以下のコマンドを実行します。

git branch

以下のようにブランチのリストが表示されています。現在のブランチはstylesになっています。

masterブランチに切り替えるには、以下を実行します。

git checkout master

以下を実行すれば、stylesブランチで行った変更をすべてmergeできます。

git merge styles

このコマンドによって、2つのブランチがmergeされ、変更の概要が以下のように表示されます。

styleブランチが不要になったので、以下を実行して削除します。

git branch -D styles

関連記事

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

Rails 5.2新機能を先行チェック!Active Storage/ダイレクトアップロード/Early Hintsほか(翻訳)

$
0
0

概要

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

Rails 5.2新機能を先行チェック!Active Storage/ダイレクトアップロード/Early Hintsほか(翻訳)

本記事では、ファイルの新しいアップロード方法、チーム内でのCredentialの共有、Content Security Policyなど、主にActiveStorageを中心としてRails 5.1の新機能を、アプリを起動するまでの手順付きでご紹介いたします。


Rails 5.2は約束どおりの新年のプレゼントとはならず、正式リリーズまでもうひと月待つことになってしまいましたが、それでもRC1となり、今や安定してきたと考えられます。というわけで、一足早くプレゼントの包み紙を明けてしまいましょう。今ならgem install rails --prereleaseを実行することでRails 5.2をインストールできます。

訳注: Gemfileでインストールする場合はgem "rails", '>=5.2.0.rc1'を指定します。

Rails 6登場前の最後のメジャーアップデートである5.2のきらびやかな新機能の中でも、Active Storageはひときわ輝きを放っています。プロジェクトでファイルアップロードを扱える機能がRailsに組み込まれたのはこれが初めてです。DHHによるプレスリリースでは、Active StorageはBasecamp 3から切り出された、production環境生まれのフレームワークであることを謳っています。

本記事ではまずActive Storageについて解説し、続いてRails 5.2の他の機能をご紹介いたします。お時間がありましたら、どうぞ最後までお付き合いください。

Active Storageで添付ファイルを扱う

免責事項: 本記事では、Active Storageを既存のソリューション(CarrierWavePaperclipShrineなど)と比較することはせず、できるだけ初心者にわかりやすくActive Storageフレームワークを紹介するよう努めます。

Active Storageをアプリで有効にするには、最初にrakeタスクを実行します。コマンドラインでrails active_storage:installを実行してrails db/migrateで新規マイグレーションを追加することで、Active Storageで必要となる2つのテーブル(active_storage_attachmentsactive_storage_blobs)を追加します。Active StorageのREADMEによると、これらのテーブルでは以下を行います。

Active Storageでは、Attachment joinモデルを経由するポリモーフィック関連付けを使い、実際のBlobに接続します1Blobモデルは添付ファイル(attachment)のメタデータ(ファイル名やコンテンツの種類など)と識別子のキーをストレージサービスに保存します。

Active Storageのこのアプローチは、他の有名なソリューションと異なっています。Paperclip、Carrierwave、Shrineは、いずれも既存のモデルにコラムを追加する必要があります。添付ファイルを扱うgemで唯一広く使われているのは、仮想属性に依存するAttachinary gemです。これは実に使いやすいプロプライエタリなソリューションで、Cloudinaryのストレージ専用です。

Active Storageも同じような方向性ですが、ファイルの保存場所をハードウェアや著名なクラウドプロバイダなどから選べるようになっています2。Amazon S3、Google Cloud Storage、Microsoft Azureについては即座に利用できます。

scaffoldでもできる

ここでActive Storageが動くところを見てみましょう。作成済みの新規アプリがあることが前提です。Gemfilejbuilder gemをコメントアウトしてbundle installを実行しておくと、scaffoldで余分なファイルを生成せずに済みます。今はActive Storageをチェックしたいだけなので、私たちのオリジナリティを発揮するためにフル機能のCRUD構築に時間をかけないことにします。

$ rails g scaffold post title:string content:text
$ rails db:migrate  # 訳注: 補いました

投稿に画像を1つ添付できるようにしましょう。モデル定義に以下のコードを追加します。

# app/models/post.rb
class Post < ApplicationRecord
  has_one_attached :image
end

Active Storageを使い始めるには、コードを3箇所だけ変更する必要があります。これはその最初の1つです。

次は、scaffoldジェネレータが作ってくれたposts_controller.rbです。ここで必要なのは、次のようにpost_paramsメソッドの中にimageパラメータを書いてホワイトリスト化することだけです(strong parameters)。

# app/controllers/posts_controller.rb
def post_params
  params.require(:post).permit(:title, :content, :image)
end

参考までに、コントローラのコードの他の場所で既存のモデルにファイルを添付するには、以下のようにします。

@post.image.attach(params[:image])

メモ: コントローラのアクションでリソースに対してcreateupdateを使うときに、添付ファイルを「許可されたパラメータ」として渡すのであれば、上のコードは不要です(正常に動きません)。初期のチュートリアルによってはファイルを明示的にattachする必要があるとしていることもありますが、ここでは既に該当しません。

次はビューです。生成された_form.html.erbで、送信ボタンのすぐ上にfile_fieldを追加します。

<!-- app/views/posts/_form.html.erb -->
<div class="field">
  <%= form.label :image %>
  <%= form.file_field :image %>
</div>

次は、画像を表示します。

<!-- app/views/posts/show.html.erb -->
<% if @post.image.attached? %>
<!-- @post.image.present? は常にtrueを返すので、attached?で存在チェックすること -->
  <p>
    <strong>Image:</strong>
    <br>
    <%= image_tag @post.image %>
  </p>
<% end %>

これでできあがりです。マルチパートフォームでアップロードするときの本質的な詳細部分は、Railsがすべて代わりにやってくれます。rails sでサーバーを起動してlocalhost:3000/posts/newをブラウザで開き、適当な画像を選んで投稿を作成します。

First post with Active Storage

Active Storageのスモークテスト

投稿の編集や画像の変更ももちろんできますので、やってみてください。ご覧のとおりファイルアップロード機能がアプリで使えるようになりました!

ここまでの作業をまとめます。

  • モデル: 定義でhas_one_attachedメソッドを呼び出し、モデルのインスタンスごとの仮想属性とするシンボルを1つ引数として渡しました。ここでは属性名を:imageとしましたが、どんな名前でも構いません
  • コントローラ: imageを許可されたパラメータ(strong parameters)でホワイトリスト化しました。
  • ビュー: フォームにfile_fieldを追加し、アップロードした画像をimage_tagで表示しました。

今度は、フォーム送信時の舞台裏を見ていくことにしましょう。サーバーのログを見てみると、すべてのSQL文の下に、それが生成されたコードの位置が表示されていることがわかります。以前はQuery Trace gemの仕事でしたが、この機能がRailsに組み込まれました。この出力がうれしくない場合は(#31691で特定のrbenv設定で問題が生じることが報告されています)、development.rbファイルのconfig.active_record.verbose_query_logsオプションをfalseに設定してください。

Log for a POST request

添付ファイルをPOSTしたときのログ

このログから、Railsがフォームを処理し、受け取ったファイルをディスクに保存し、保存場所をエンコードしてキーを生成し、そのキーをactive_storage_blobsテーブルで参照し、postsテーブルでレコードを1件作成し、BlobPostactive_storage_attachments経由で関連付けている様子がわかります。

以下は、PostsControllershowアクションがGETで呼び出されたときの挙動です。

Log for a GET request

アップロードの結果をGETしたときのログ

1件のリクエストは3箇所で処理されます。ActiveStorage::BlobsControllerActiveStorage::DiskControllerはファイルを扱います。このようにして、画像のパブリックなURLは常に実際の場所から切り離されます。クラウドサービスを使っている場合は、BlobsControllerによってクラウド内の正しい署名済みURLにリダイレクトされます。

Postインスタンスで何ができるようになったかをrails consoleで見てみましょう。

Rails console

rails consoleでレコードをいじる

添付ファイルのURLを生成するには、urlではなくservice_urlを呼ぶ必要がありますのでご注意ください。url_forimage_tagなどのビューヘルパーはこれを認識して自動でやってくれるので、こうしたメソッドを明示的に呼ぶ必要はめったにありません。

N+1を解決する

ビューで添付ファイルが出力されるときには、少なくとも3件のデータベースクエリが発生します(親モデルで1件、active_storage_attachmentsで1件、active_storage_blobsで1件)。多数の添付ファイルをすべて含むActive Recordオブジェクトのコレクションについて出力を繰り返す場合には、何か注意が必要でしょうか?調べてみましょう。index.html.erbを変更して、投稿ごとの画像(または少なくとも画像ファイル名)を表示するようにし(post.image.filenameもそうしたクエリをすべてトリガします)、/postsをブラウザで更新してログを見てみましょう。

Active StorageのN+1クエリ

見事なまでのN+1クエリ

問題が起きているのがおわかりでしょうか?「N+1」という名のヒドラがおぞましき頭をもたげています。ありがたいことに、Active Storageにはちゃんと解決法が用意されています。この方法では、関連付けられたblobをincludesするwith_attached_imageスコープ(またはwith_attached_your_attachment_nameスコープ)を生成します。必要な作業は、PostsController#index@posts = Post.all@posts = Post.with_attached_imageに変更することだけです。結果を見てみましょう。

N+1問題の解決

問題が解決しました!

素晴らしい!しかし、Active Recordが常に正しい選択を行えることを当てにしたくないという理由で、includesではなくeager_loadpreloadを使いたい場合はどうすればよいのでしょうか?そんなときは次のようにします。

class Post < ApplicationRecord
  scope :with_eager_loaded_image, -> { eager_load(image_attachment: :blob) }
  scope :with_preloaded_image, -> { preload(image_attachment: :blob) }
end

ここで注意しておきたいのは、プリロードをネストしている点です。blobsの読み込みはattachmentsを経由します。image_attachmentは、Active Storageによって追加された関連付けの名前です。特にこの場合attachmentの名前を変更すると、関連付けの名前も変更されます。

複数の添付ファイル

CRUDで複数の添付ファイルを扱えるようにするのも朝飯前です。

  • モデル:
# app/models/post.rb
class Post < ApplicationRecord
  has_many_attached :images
  # ここで暗黙の関連付けを複数形にしていることに注意
  scope :with_eager_loaded_images, -> { eager_load(images_attachments: :blob) }
end
  • コントローラ:
# app/controllers/posts_controller.rb
def post_params
  params.require(:post).permit(:title, :content, images: [])
end
  • ビュー:
<!-- app/views/posts/_form.html.erb -->
...
<div class="field">
  <%= form.label :images %>
  <%= form.file_field :images, multiple: true %>
</div>
...
<!-- app/views/posts/show.html.erb -->
...
<% if @post.images.attached? %>
<p>
  <strong>Images:</strong>
  <br>
  <% @post.images.each do |image| %>
    <%= image_tag(image) %>
  <% end %>
</p>
<% end %>

添付ファイルの1つ(またはすべて)を削除したい場合、purgeメソッドかpurge_laterメソッドを使えます。後者は組み込みのActiveStorage::PurgeJobを使ってバックグラウンドでファイルを削除します。親モデルを削除すると、デフォルトで非同期削除も呼び出されます。

ImageMagickでさまざまなサイズの画像を扱う

ここまでの時点では、ユーザーがアップロードした画像はそのまま表示されますが、多くの場合これは望ましくない動作です。Active Storageでは、ImageMagickによる画像変換をサポートしています。必要な準備は、Gemfileに(コメントアウトで)追加済みのmini_magick gemを有効にするだけです。これで、たとえば次のようにImageMagickの変換を使えるようになります。

<%= image_tag image.variant(resize: "500x500", monochrome: true) %>

上のコードは、指定したblobの指定したvariantを指すURLを作成しますが、変換そのものは、その画像がブラウザから最初にリクエストされるまではActiveStorage::VariantsControllerで扱われません。Active Storageでは、元のblobがActive Supportからダウンロードされ、サーバーのメモリ上で変換され、再度サーバーにアップロードされる必要があるため、コストの高い操作の遅延を試みます。

ファイルを(遅延せずに)最初に処理し、その後でURLを取得したい場合は、image.variant(resize: "100x100").processed.service_urlを呼び出します。これは、その特定のvariantが既に実行済みかどうかをチェックし、実行済みの場合は同じ処理を繰り返しません。

動画のプレビュー画像の生成(ffmpegを使用)やPDFの生成(mutoolを使用)も行えます。ただしこれらのライブラリはRailsでは標準で提供されていないので、ツールの準備は自分で行うことになります。

ImageMagickが有効になれば、アップロードされたファイルは自動的に(かつ非同期で)分析されメタデータを生成します。post.image.metadataを呼ぶことで、{"width"=>1200, "height"=>700, "analyzed"=>true}のようなハッシュを取得できます。

画像をImageMagick以外のライブラリで扱いたい方に残念なお知らせです。現時点のActive Storageにはimgproxyなどの画像処理ライブラリを使う公式の手段がまだありません。

秘密情報の扱いの改良

ここまではスイスイ進められましたが、他に何か必須の設定はあるのでしょうか?なくても大丈夫。Railsでお馴染みのomakase方式では、基本的な設定が既にconfig/storage.ymlに含まれています。このファイルは次のようになっています。

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
#   service: S3
#   access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
#   secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
#   region: us-east-1
#   bucket: your_own_bucket

development.rbにも既にconfig.active_storage.service = :localが設定されているので、Active Storageをlocalhostで動かす場合にローカルディスクを使うことをRailsが認識します。yamlファイルにはAWS S3、Google GCS、マイクロソフトのAzureStorageのクラウドサービス向け設定もコメントアウト状態で含まれています。

storage.ymlは(gitなどで)ソース管理したいので、そこに秘密のキーを直接書き込むのはよくありません。Railsでは、そうしたキーをRails.application.credentialsに保存することが前提になっています。おや、そこは以前にはsecretsと呼ばれていたのではありませんか?はい、そのとおりです。しかしDHHは#30067で次のように説明しています。

config/secrets.ymlconfig/secrets.yml.encSECRET_BASE_KEYの組み合わせは混乱の元になります。これらの秘密情報に何を保存するべきなのかも、SECRET_BASE_KEYが一般的なセットアップと関連するものなのかどうかもはっきりしません。

この混乱に終止符を打つため、Railsでは「credential」という概念が新しく導入されました。

credentialは、config/credentials.yml.encファイルに暗号化された形で保存されるので、このファイルは安全にソース管理できます。もう環境変数の問題や、キー変更をチーム全員で同期する問題にわずらわされることはありません。

config/master.keyファイルは、いかなる場合であってもGitに登録してはいけません(このファイルはRails 5.2プロジェクトの.gitignoreに既に含まれています)。このファイルには、credentialを復号できる自動生成されたキーが含まれます。ドキュメントでも次のように警告されています。

このマスターキーを絶対に紛失しないこと!マスターキーは、チームがアクセス可能なパスワード管理ソフトウェアに保存すること。万一紛失すれば、あなたを含むいかなる人物も、暗号化されたcredentialに一切アクセスできなくなります。

では、内容が常に暗号化されているcredentialをどうやって編集すればよいのでしょうか? Rails 5.2から、そのための新しいタスクrails credentials:editが用意されています。このコマンドを実行すると、デフォルトのエディタで平文のテキストファイルが開くので、そこにkey_name: key_valueの形式でキーを書き込めます。Rails.application.credentials.key_nameでcredentialにアクセスできます。ネストしたキーを使っている場合はRails.application.credentials.dig(:section_name, :nested_key_name)でアクセスできます。一時ファイルを保存して閉じると、内容が暗号化されてconfig/credentials.yml.encに保存されます。ターミナルでrails credentials:showを実行するとキーを出力できます。

これで、同一のmaster.keyをチームのメンバー全員に渡せば、重要な情報が漏洩する心配をせずにGitで安全に共同作業できるようになります。production環境では、RAILS_MASTER_KEY環境変数のみ設定する必要があります。

メモ: Atomなどの外部エディタで編集する場合は、EDITOR="atom --wait" credentials:editのように実行する必要があります。修正量が少ない場合はEDITOR=vi credentials:editのようにシェルで動くエディタの方が手っ取り早いかもしれません。

Active Storageをクラウドで使う

Amazon S3を使って簡単なデモを行います。当然ながら、publicに読み出しアクセスできるS3バケット(bucket)が必要です。用意が整えば、後は以下の3つの手順を進めるだけで使えるようになります。

  1. storage.ymlamazon:セクションのコメントアウトを解除します。
  2. development.rbconfig.active_storage.service = :amazonを指定して、デフォルトのクラウドサービスをS3に切り替えます。
  3. コンソールでrails credentials:editと入力し、以下の要領でキーを入力します
aws:
  access_key_id: #idを入力
  secret_access_key: #アクセスキーを入力

これで完了です!アップロードしたファイルやvariantは自動的にAmazon S3経由で取り扱われます。post.image.service_urlは、バケットインスタンスを指す署名済みURLを生成します。

ダイレクトアップロード

Active Storageの開発が活発だった昨年夏、Fabio Akita氏は、Active Storageを有効にしたプロジェクトがHerokuなどのephemeralな(=短命な)ファイルシステムを用いるプラットフォーム上で動作する場合に、巨大なファイルを扱うことについて懸念を表明しました。Railsの作者DHHはTwitterのスレッドでブラウザからクラウドへの「ダイレクトアップロード(Direct Upload)」を実装することについて納得しました。ダイレクトアップロードは、Attachinaryが実装したCloudinaryストレージへのファイル送信と似たような方法で、アプリのバックエンドを完全にバイパスします。

ダイレクトアップロード機能については、Railsガイド(edge)にJavaScriptコードスニペットのよい説明が既に掲載されています3(最終的にCoffeeScriptではなくES6を使います!)。

ここではダイレクトアップロードを簡単に動かしてみましょう。以下の手順ではWebpackerを念頭に置いていますので、アプリでrails webpacker:installを実行しておいてください。

$ yarn add activestorage
// app/javascript/packs/application.js
import * as ActiveStorage from "activestorage";
import "../utils/direct_uploads.js"

ActiveStorage.start();
// app/javascript/utils/direct_uploads.js
// このフォルダとファイルを作成し、以下のコードをコピペする
// http://edgeguides.rubyonrails.org/active_storage_overview.html#example
<!-- app/views/posts/_form.html.erb -->
<div class="field">
  <%= form.label :images %>
  <%= form.file_field :images, multiple: true, direct_upload: true %>
</div>

上のdirect_upload: trueオプションによって、ファイルのフィールドのHTMLが以下のように生成されます。

<input multiple="multiple" data-direct-upload-url="http://localhost:3000/rails/active_storage/direct_uploads" name="post[images][]" id="post_images" type="file">

JavaScriptサンプルコードでは、Direct Upload用JSイベントを用いて、アップロードサイクルに従ってUIに応答を表示する(プログレスバーを更新するなど)方法が示されています。

EdgeガイドのDirect Uproad installationのサンプルCSSコードをコピーして、direct_uploads.cssファイルに貼り付けます(このファイルはapp/assets/stylesheetsの下に作成できます)。これで、サーバーを起動してpostページを開けば、巨大なファイルを選択できるようになり、長時間のタスクによってサーバーがブロックされていない様子を見ることができます。このアップロードは完全にXHRで行われ、プログレスバーが動的に更新されます。

Chromeでダイレクトアップロードする

ブラウザでファイルをダイレクトアップロードする

: 本記事執筆時点では、S3を使うダイレクトアップデートがFirefoxでXML Parsing Errorエラーになります。

S3を使う場合は、S3バケットの[Permissions]タブでCORS設定をオープンにする必要があります。以下はdevelopment環境で雑に動かすためだけの設定なので、production環境では設定を全開のままにしないでください!

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>DELETE</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

「鏡よ鏡」

Active Storageのもうひとつの大きな機能はミラーリングです。ミラーリングによって、複数のクラウドストレージプロバイダの間でファイルを同期して冗長性を高めたり、クラウドの統合に役立てたりできます。たとえば、storage.ymlに以下を記述するとします。

production:
  service: Mirror
  primary: local
  mirrors:
    - amazon
    - google

続いて、使うクラウドサービスをproduction.rb:productionで設定します。これにより、アップロードされたファイルがローカルに保存されると同時にAmazon S3とGoogle Cloud Storageにもバックアップされます。ファイルを削除すると、クラウドからも削除されます。

Active Storageのクラウド関連について今のところ説明できるのは以上です。productionで通用することを示さなければならないので、これについてさらに洞察を提供できればと思います。

この辺で、Rails 5.2のその他の目玉機能についても見てみましょう。

Content-Security-Policyヘッダー設定用DSL

セキュリティはやはり重要です。ここでアプリのセキュリティをさらに強化することにしましょう4CSP(Content Security Policy)は、ブラウザが受信するネットワークリクエストを制限するために設計されています(ブラウザページで読み込んでよいものを指定するなど)が、副次的効果として、ブラウザから送信されるリクエストにも制限をかけることでたちの悪いハッキングを防止します。

Railsアプリで簡単にCSPを設定できるようになりました。グローバルに設定する場合は以下のようにします。

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |p|
  # 現在のhostnameからの送信(セキュア通信のみ)をデフォルトですべて許可
  p.default_src :self, :https
  # data-urlからのフォントや画像の読み込みを許可
  p.font_src    :self, :https, :data
  # ActiveStorageアセットで使う可能性のあるhostnameは必ずここに追加すること
  p.img_src     :self, :https, :data, "cloudfront.example.com"
  # <object>タグの禁止(Flashさよなら)
  p.object_src  :none
  # インライン<style>を許可(オフにするには`:unsafe_inline`を削除)
  p.style_src   :self, :https, :unsafe_inline
end

コントローラごとに設定する場合は以下を使います(さらに動的になります)。

class PostsController < ApplicationController
  # グローバルポリシーのextend/オーバーライド
  content_security_policy do |p|
    # ユーザー固有のドメイン名をベースにするポリシーを設定
    p.base_uri :self, -> { "https://#{current_user.domain}.example.com" }
  end
end

ただし、(本記事執筆時点では)ドキュメントに記載されていない注意事項が1つあります。Webpackerとwebpack-dev-serverを使っている場合は、development環境でCSP設定を更新してhttp://localhost:3035ws://localhost:3035への接続を許可しなければなりません。これを行わないと、Webpackのホットリロード機能で必要なweb socket接続がRailsにブロックされてしまいます。詳しくはissue #31754をご覧ください。問題を発見したNick Savrovに感謝します。

# Webpackerを使う場合は以下のような感じでCSPを設定する必要がある
Rails.application.config.content_security_policy do |p|
  p.font_src    :self, :https, :data
  p.img_src     :self, :https, :data
  p.object_src  :none
  p.style_src   :self, :https, :unsafe_inline

  if Rails.env.development?
    p.script_src :self, :https, :unsafe_eval
    p.default_src :self, :https, :unsafe_eval
    p.connect_src :self, :https, 'http://localhost:3035', 'ws://localhost:3035'
  else
    p.script_src :self, :https
    p.default_src :self, :https
  end
end

これは、RailsチームがCSP設定をデフォルトで無効にすることを決定した理由のひとつでもあります。

どこからでもアクセスできるCurrentシングルトン

モデルの中からcurrent_userにアクセスする方法を知りたいと思ったことはありますか?StackOverflowの“current_user”に関するすべての質問第3位は、このメソッドをモデルで使う方法についての質問です。

この状況はRails 5.2から変更される予定です。今回追加されるCurrentシングルトンのマジックは、アプリのあらゆる場所からアクセス可能なグローバルストアであるかのように振る舞います5

# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :user
end

後はコントローラのどこかでuserを設定するだけで、モデル/ジョブ/メイラーなどあらゆる場所でアクセス可能になります。

class ApplicationController < ActionController::Base
  before_action :set_current_user

  private

  def set_current_user
    Current.user = current_user
  end
end

# モデルで以下を行えるようになる
class Post < ApplicationRecord
  # post作成時のユーザー指定は不要
  # デフォルトでカレントユーザーが使われる
  belongs_to :user, default: -> { Current.user }
end

このアイデアは新しいものではありません。Steve Klabnik作のrequest_store gemを使えば、あらゆるRackアプリで同じことができます。

: 「この機能は『関心分離の法則』に違反している」と思われるかもしれません。おっしゃるとおりです。何かおかしいと思ったときは使わないでください。

訳注: 以下の記事も参考にどうぞ。

Railsの`CurrentAttributes`は有害である(翻訳)

HTTP/2 Early Hints

HTTP/2の「Early Hints」は、ブラウザのページ内でアセットを使う前にアセットを事前にダウンロードできるようにする機能です。これによってHTTP/2のパイプラインリクエストが効き、ページの読み込み時間が短縮されます。

Pumaサーバーに--early_hintsフラグを追加して起動し、HTTP/2互換プロキシ(h2oなど)をサーバーの前に配置するだけで使えるようになります。

Early Hintsについて詳しくは、この機能の作者であるEileen Uchitelleの記事『HTTP2 Early Hints』をお読みください。

訳注: 『HTTP の新しいステータスコード 103 Early Hints』も参考にどうぞ。

Bootsnap

Railsのような多機能なフレームワークで作業する場合のトレードオフのひとつが「起動時間」です。巨大なモノリシックアプリの起動やタスクの実行に1〜2分を要することがあります。

Railsアプリを事前に起動しておくSpring gemに加えて、RubyやYAMLのファイル読み込みを高速化するBootsnap(Shopifyのツール)という一片のマジックがデフォルトでGemfileに追加されたことで、コールドスタートが2〜4倍も速くなります6

Bootsnapは、Springの設定が面倒なDockerでの開発や、CIサーバーで利用する場合に特に便利です。


Rails 5.2はまだ新しいので、私たちがproduction環境で試す機会はこれまでありませんでした。皆さまの情報をTwitterで共有するときには、お気軽に私たちのアカウントにもメンションを下さい。フォームで直接お問い合わせいただくこともできます。本記事の作成で大変参考になったActive Storage記事の著者である、Mike Gunderloy氏に感謝いたします。


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

関連記事

Rails 5.2を待たずに今すぐActiveStorageを使ってみた(翻訳)

Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)

Rails: パーシャルと`collection:`でN+1を回避してビューを高速化(翻訳)


  1. 添付ファイルのblob(binary large object)とは、本質的に文字どおり巨大なバイナリファイルです。active_storage_blobsではバイナリファイルをデータベースに保存せず、情報(ファイルサイズ、コンテンツの種類、メタデータ)を元にファイルの置き場所だけをトラックします。 
  2. ActiveStorage::Serviceを継承することで、他のクラウドサービスのサポートも実装できます。 
  3. RailsプロジェクトでWebpackerを使う場合の詳しい設定については、以下の「新しいRailsフロントエンド開発」シリーズ記事のpart 1part 2part 3をお読みください。 
  4. 5.2より前のRailsについては、secureheaders gemをご覧ください。 
  5. これは技術的にはスレッドごとのストアであり、リクエストが終了するとクリアされるので、マルチスレッドアプリでも安全に使えます。 
  6. このマジックが好きでない場合は、rails new --skip-bootsnapを実行します。 

Rails: メモリ使用量を制限してHerokuのR14エラー修正&費用を節約した話(翻訳)

$
0
0

概要

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

Rails: メモリ使用量を制限してHerokuのR14エラー修正&費用を節約した話(翻訳)

貯金箱でHerokuの費用節約をイメージ

理論的には、Herokuの512MB dynoが1つあればRails webサーバーとSidekiqプロセスを両方とも動かせます。トラフィックの少ないサイドプロジェクトで月7ドルを節約できればとても助かります。残念なことに、1つのdynoでRubyプロセスを2つ動かすとメモリの問題が生じることがあります。本記事では、Railsアプリのメモリ使用量を制限する方法について説明します。

最近読んだBilal Budhaniの良記事では、1つのHeroku dynoでSidekiqプロセスとPumaを同時に実行する方法について説明していました。私のサブプロジェクトの1つに適用したところ、R14エラーが大発生しました。

Error R14 (Memory quota exceeded)

Heroku R14 - Memory Quota Exceeded in Ruby errors

メモリ使用量が急上昇した後に、メモリエラーが大発生して自動的に再起動した

この問題を調査してメモリ使用量を最適化したところ、グラフは以下のようになりました。

Heroku R14 - Memory Quota Exceeded in Ruby fixed

メモリ使用量が安定し、その後ガベージコレクションが行われた

方法は次のとおりです。

1. Gemfileをダイエットする

Ruby世界にはさまざまなお便利gemがありますが、gemの削除は多くの場合最も楽チンな問題解決方法です。メモリの肥大化は、その他のコストよりも見落とされがちです。

gemごとのメモリ使用量をチェックするには、derailed benchmarksが最適です。

gem 'derailed_benchmarks', group: :development

Gemfileに上を追加してbundle exec derailed bundle:memを実行するだけでメモリ使用量をチェックできます。

私のプロジェクトではTwitterFacebookのボットプロファイルを強化します。今回驚いたのはtwitter gemが起動時に13MBものメモリを消費していたことです。最初にこのgemをより軽量なgrackleに置き換え(〜1MB)、最終的にTwitter APIへのHTTP呼び出しを行うカスタムコードを書きました。同様にkoala gem(〜1MB)も何とか取り除けました。

もうひとつ効果的だったのは、gon gem(〜6MB)をJavaScriptのカスタムデータ属性に置き換えたことです。

たった数行のJavaScriptコードを書かずに済ませたいという理由で、数MBのメモリを消費するgemファイルをいくつもインポートするのはぜひとも避けるべきです。

2. jemallocを使う

jemallocは、公式のMRIメモリアロケータの代わりに使えます。Herokuの場合、buildpackを使ってjemallocを追加できます。その結果、私のアプリではメモリ使用量が最大20%も削減されました。production環境にデプロイする前に、staging環境でjemallocを徹底的にテストしておきましょう。

3. コンカレンシーとワーカー数を制限する

トラフィックの多くないサブプロジェクトでは、スループットはさほど必要にならないでしょう。SidekiqやPumaのワーカー数やスレッド数を減らすことでメモリ使用量を制限できます。私のconfig/puma.rbは以下のとおりです。

threads_count = 1
threads threads_count, threads_count
port        ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { "production" }
workers 1

preload_app!

on_worker_boot do
  @sidekiq_pid ||= spawn('bundle exec sidekiq -t 1')
  ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end

on_restart do
  Sidekiq.redis.shutdown { |conn| conn.close }
end

plugin :tmp_restart

config/sidekiq.ymlは次のとおりです。

---
:concurrency: 1
:queues:
  - default
  - [critical, 100]

Pumaは、スレッドの最大数に1を指定しても最大7つまでスレッドを生成できます。これらの最小限の設定でも、私のSmart Wishlistアプリは100K程度のSidekiqジョブを引き続き処理可能であり、ReactフロントエンドとモバイルJSON APIの両方のサービスをこなしています。

4. JSONパーサーを最適化する

これらのSidekiqジョブは、iTunes API(一括リクエストは私のToDoリストで行います)から最新の価格をダウンロードしたりディスカウントを通知したりするのに必要です。つまり、そこではJSONのパースが相当行われているということです。こういう場合は、以下の1行修正でメモリ使用量とパフォーマンスの両方が改善されます。

gem 'yajl-ruby', require: 'yajl/json_gem'

yaji-rubyは、JSON gemと互換性のあるAPIを提供します。JSON.parse呼び出しをフックしてパフォーマンスとメモリ使用量を改善します。

まとめ

制限された環境での作業は、プログラミングのスキルを鍛えて新しい最適化方法を発見するよい方法のひとつです。理論的にはサーバーのメモリを増やせばいつでも問題を解決できますが、それよりも7ドル節約しませんか。

関連記事

Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)

データベースのランダム読み出しは要注意(翻訳)

「巨大プルリク1件vs細かいプルリク100件」問題を考える(翻訳)

$
0
0

概要

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

「巨大プルリク1件vs細かいプルリク100件」問題を考える(翻訳)

本記事では、昔ながらの問題である「巨大なプルリク1件と超細かいプルリク100件、どっちなら戦う気になれる?」に対する回答を示したいと思います。チームの一員としてよりよいコードを書くためのガイドラインについてもある程度解説します。今回の記事は、すべて以下のツイートから触発されました。

何が問題だったか

私は、Fullscript社で行われているコードレビューが今ひとつ活用されていないことに気づきました。featureブランチが長期間取り残されていることがしょっちゅうでした。featureブランチのコードは数千行にまで肥大化し、まともなフィードバックを返すことはおろか、レビューに途方もない時間がかかる始末です。心底ゲンナリでした。

開発者は、時間とやる気が満ちてくるまでレビューを先延ばしにしたりするので、開発プロセスが停滞してしまうことがあります。レビュアーは「レビューしなければ」というプレッシャーを肌で感じつつ、コードの表面をさっと眺めて「LGTM!👍」(良さげ)などと書いて終わらせることもよくあります。

たとえレビューがうまくいったとしても、大規模なリファクタリングをかけるにはタイミング的に手遅れになることもしばしばです。初期段階の設計ミスが頑固に根を張り、修正コストはスタートアップ企業が到底負担しきれないほどに跳ね上がってしまいます。ぐらついている基礎の上で何週間も作業を重ねたこともありました。

残念なことに、コードの品質は詳細な検査が必要なレベルにすら達しませんでした。誰もそこから学んでおらず、レビュープロセスは頓挫してしまったのです。

ほほう、ではどうやって改善する?

レビューを依頼するということは、他の誰かに「責任を共有してください🙇」とお願いするということです。依頼された人は問題を理解し、あなたのコードを把握し、問題がなければ(そう願いたいものです)承認しなければなりません。私たちは開発者として、この作業をできる限り軽減すべきです。

「早期」かつ「頻繁に」

フィードバックを依頼するのは、最も重要な時期、すなわち開発プロセスの早い段階で行うようにしましょう。こうすることで、レビュアーは設計上の問題を早い段階で検出する機会を得られますし、あなたが確かな基礎の上にコードを構築していることを担保できるようになります。チームは、開発プロセスが進んでからの書き直しというコストの高い作業や、既知の欠陥を持つコードを時間や予算の制約のせいでそのままmergeする事態を回避できます。

粒度を小さくする

「小さく」というのは、限りなくゼロ行に近づけるということです。たった1行の変更のレビューを嫌がるレビュアーはいないでしょう。プルリクのサイズ(=コードの行数)に上限を設けることで、粒度を下げやすくする効果が著しく向上します。レビュアーが1行ずつ丁寧に調べやすくなるのはもちろんのこと、レビュー時間も大きく削減できます。コードを定期的にmergeできるようになり、品質にも自信を持てるようになります。

作業のスコープを絞る

コードの行数を削減するための重要なコツは、プルリクのスコープ(=機能のセット)を絞り込み、解決するタスクを1つにする(または密接に関連する少数のタスクに絞り込む)ことです。スコープを絞ることで、レビュアーの認知機能にかけられる負荷を大きく軽減できます。1件のプルリクでいくつもの問題をいっぺんに解決しようとすると、ある問題がどのコードと関連しているのかを整理する作業がつらくなります。

機能が未完成でもリリースする(ただし内緒で)

機能が完成するまでリリースを差し止めることは比較的普通に行われます。残念なことに、これは巨大なfeatureブランチがいつまで経ってもなくならない主要な原因のひとつです。masterブランチでの開発が進むに連れて、mergeのコンフリクトやrebaseなどの楽しい事件が起きがちです。たとえmergeできたとしても、大規模な変更を無事にデプロイするのはチームにとって神経を削る作業です。目玉機能や大きな依存関係のアップグレードをデプロイする場合はなおさらです。

機能を一時的に取り消すツールを使って、未完成の機能をユーザーの目から隠しておくという手があります。これなら、コードのmergeやリリースの頻度が落ちずに済みますし、心配の種も減らせます。開発が完了に近づいたら、特定のアカウントやアーリーアダプタ(訳注: 新機能を喜んで使うユーザー)にだけ新機能へのアクセスをぼちぼち許可できるようになります。デプロイ作業の心配も減りますし、機能の成熟度に応じて機能へのアクセスを制御できるようになります。

追伸: Ruby on Railsをお使いの方には、flipper gemを強くおすすめいたします。

事前の計画

開発者がこうした制約のもとで作業すると、問題を細かな単位に分割して渡すようになります。この制約は、そのような明確な計画のない開発を手がけようとする誘惑を払いのけるのにも役立ちます。これには少々経験が必要ですが、そのうちに慣れて、自然に開発プロセスの一部に組み込まれるでしょう。

「ついでのリファクタリング」はしないこと

開発者は、問題に気づくとその場でリファクタリングすることがよくあります。リファクタリングはよいことですが、別のプルリクで行うべきです。そうすることでリファクタリングを早めにmergeできますし、関係のない機能リリースに修正が結び付けられることもなくなります。「ついでの改善」は、元々のプルリクのリリースが遅れれば巻き添えで遅れてしまいますし、最悪まったくリリースされなければそのまま失われてしまうでしょう。

やってみよう

コードレビューは、チーム内でのソフトウェア作成になくてはならない作業です。コードレビューは知識を共有する場であり、コードの品質を監視する門番でもあります。上述の制約の元で作業するようになったことで、Fullscript社のコードレビューで次のような成果を得られました。

  • コードレビューが短時間で完了し、品質も向上した
  • リリースのテストが容易になり、デプロイの苦労も大きく軽減された
  • 問題が発生した場合の変更の取り消しやロールバックが簡単になった

単純な話のように見えるかもしれません(し、実際そうかもしれません)が、実践のためにはチームが一丸となって経験を積む必要があります。本記事でお伝えしているメッセージに共感いただけましたら、始め方についてチームメンバーと話してみることをおすすめします。


ご意見や疑問がありましたら、元記事のコメント欄かTwitterまでお気軽にどうぞ。

また、本記事は私が地元のミートアップで発表したスピーチを元にしています。このスピーチは元々、大規模チームでのソフトウェア開発のアプローチについてKevin McPhillipsWillem van Bergenと雑談したときに閃いたものです。お二人に感謝します。

関連記事

いい感じのコードには速攻でLGTM画像を貼ってあげよう

Rails: テストのリファクタリングでアプリ設計を改良する(翻訳)

技術的負債を調査する10のポイント(翻訳)

Sourcetreeを使ってRailsアプリをHerokuにデプロイする

$
0
0

こんにちは。エンジニアアルバイトのohnoです。この記事ではタイトルそのまま、「Sourcetreeを使ってRailsアプリをHerokuにデプロイする」方法を書きました。
「rails heroku デプロイ」などで検索をすると、CLIを使ったデプロイ方法は多数見つかるのですが、GUIを使った記事はあまり見当たりません。そこで本記事では、gitのGUIクライアントであるSourcetreeを使って、Herokuにデプロイする方法を紹介します。

環境

macOSが前提です(Windows版のSourcetreeはUIの細かな部分が異なるそうです)。

  • Rails 5.1.4
  • Ruby 2.4.1
  • RubyMine 2017.2.4
  • Sourcetree 2.7
  • psql 9.6.5

Herokuにアプリケーションを作成する

当然ですが、まずはHerokuにアカウントを作り、ログインしましょう。ログインするとアプリケーション一覧ページが表示されるかと思います。
Herokuにアプリケーションを作成するために、このページの右上にある「NEW」ボタンをクリックし、「Create new app」を選択します。
スクリーンショット 2018-01-22 13.31.22.png

「App name」を入力し、「Create app」ボタンを押します。今回はdeploy-sampleというアプリケーション名にしました。「Choose a region」は「United States」のまま変更していません。
スクリーンショット 2018-01-22 13.32.46.png

「Open app」をクリックして確認してみましょう。
スクリーンショット 2018-01-22 13.33.25.png

以下のような画面が表示されていれば成功です。
スクリーンショット 2018-01-25 10.46.09.png

ローカルに作成したアプリケーションの設定

(注:詳細は割愛しますが、今回デプロイするアプリケーションは$ rails new deploy-sampleで作成した後に、$ rails g scaffold user name:stringを実行したものです。)
Herokuの推奨DBはPostgreSQLですが、RailsはデフォルトでSQLiteを使用しているのでいくつか設定をする必要があります。
まずはアプリケーションのGemfileを次のように編集してください。

  • 編集前
gem 'sqlite3'
  • 編集後
gem 'pg', group: :production
gem 'sqlite3', group: :development

続けて、database.ymlの内容を編集します

  • 編集前
production:
  <<: *default
  database: db/production.sqlite3
  • 編集後
production:
 <<: *default
 adapter: postgresql
 encoding: unicode
 pool: 5

これでアプリケーション側の設定は完了です。

Sourcetreeの設定

Sourcetreeを開くとこのような画面が表示されるはずです。
スクリーンショット 2018-01-22 15.16.34.png

この画面の「新規…」から「既存のローカルリポジトリを追加」を選択し、先ほど設定したローカルのアプリケーションを追加します。
スクリーンショット 2018-01-22 15.16.51.png

次に、追加されたアプリケーション名をクリックし、リモートリポジトリの設定をします。
「リモート」を右クリックし「リモートを追加…」を選択しましょう。

「URL/パス」の欄にはリモートリポジトリのURLを入力します。リモートリポジトリのURLはHerokuの「Settings」から確認することができます。今回「リモートの名前」はherokuにしました。
スクリーンショット 2018-01-22 14.43.03.png
Sourcetreeの設定は以上です。

デプロイ

ここまできたらあとはデプロイするのみです。

編集したファイルがSourcetreeの左下に表示されています。

チェックをつけるとファイルがステージングに移動し、コミットできるようになります。

「コミット」をクリックしコミットメッセージを入力後にコミットします。

ローカルでコミットした内容を「プッシュ」からHerokuにpushします。

次にHeroku側でデータベースの設定をします。Railsではマイグレーションを実行することでデータベースの設定が行えます。ただし、今回はGUIからマイグレーションを実行する方法がわからなかったので、Heroku CLIを使用します。
ここではmacOSにインストールする方法を紹介しますが、もちろん他のOSにもインストールできます。詳しくはこちらの公式ページをご覧ください。

macOSの場合はHomebrewを使ってインストールします。

$ brew install heroku/brew/heroku

インストールが完了したら、herokuコマンドを使ってログインしておきましょう。

$ heroku login

最後に、いまインストールしたHeroku CLIを使って以下のコマンドを実行します。

$ heroku run rails db:migrate

これでデプロイは完了です。

参考

関連記事

Rails: メモリ使用量を制限してHerokuのR14エラー修正&費用を節約した話(翻訳)

Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)


Rails tips: Active Recordの`#from`を使ってorderとdistinctを1つのクエリにする(翻訳)

$
0
0

概要

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

Rails tips: Active Recordの#fromを使ってorderとdistinctを1つのクエリにする(翻訳)

重複のない一意の結果を得ようとしたことがある方は、単純なクエリでこれを実現できないことにおそらくお気づきかと思います。Active Recordでは、操作する各カラムを明示的にselectすることが要求されるためです。わかりやすくするため、次の事例を考えてみましょう。

問題

Location.joins(:users).where(users: {enabled: true}).distinct.order('locations.name')

目的は、enabled: trueuserlocationごとに1件以上表示することです。locationのリストをnameでソートする必要もあります。上のクエリは動作しません。実行すると以下のエラーが発生します。

ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'locations.name'

解決方法

基本的には2つのクエリが必要です。

  1. enabled: trueuserを1件以上持つlocationをフェッチするクエリ
  2. locationnameで並べ替えるクエリ

Active Recordのfromメソッドを使えば、これを1つのクエリにできます。そのためには、Locationモデルを少々リファクタリングしなくてはなりません。クエリロジックを分離するためにクラスメソッドを2つ追加します。変更後のモデルは次のようになります。

class Location < ActiveRecord::Base
  has_many :users

  def self.enabled
    joins(:user).where(users: {enabled: true}).distinct
  end

  def self.by_name
    order(:name)
  end
end

上のリファクタリングについて詳しく知りたい方は、過去記事をご覧下さい。

この構成にすることで、fromメソッドを使えるようになります。

Location.from(Location.enabled, :locations).by_name

このfromメソッドをもう少し詳しく見てみましょう。このメソッドは引数を2つ取っています。

  1. FROMで使われるクエリ
  2. 後で操作できるようにするためのサブクエリとして使われる名前

生成されるSQLクエリは次のようになります。

SELECT `locations`.* FROM(SELECT DISTINCT `locations`.* FROM `locations` INNER JOIN `users` ON `users`.`location_id` = `locations`.`id` WHERE `users`.`enabled` = 1) ORDER BY `locations`.`name`

お知らせ: コードを正しくテストするには

コードを正しくテストするのは何かと困難であり、しかも最も大変なのはテストを書き始めるときです。テストを書き始めるときに役立つRSpec & Test Driven Developmentの無料ebookをご自由にダウンロードいただけます。

関連記事

Rails tips: モデルのクエリをカプセル化する2つの方法(翻訳)

Rails tips: belongs_to関連付けをリファクタリングしてDRYにする(翻訳)

Rails tips: `to_sql`でクエリをSQLに変換する(翻訳)

$
0
0

概要

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

Rails tips: to_sqlでクエリをSQLに変換する(翻訳)

Railsのto_sqlメソッドはRubyプログラマーならよくご存知かと思いますが、駆け出しの開発者はまだよく知らないかもしれません。このメソッドは非常に有用なので、改めてご紹介したいと思います。

背後の動作を明らかにするto_sql

実はto_sqlは素晴らしいデバッグツールであり、ある種のコードマジックを学ぶときにも役立ちます。ActiveRecordのDSLは実に小ざっぱりとしていますが、その背後では小ざっぱりしてないSQLクエリが生成されています。次のようにto_sqlを使うことでこのSQLクエリを白日の下に晒すことができます。

Location.where(city: 'New York').to_sql

サブクエリ

to_sqlメソッドはデバッグにももちろん有用ですが、複雑なサブクエリを書かなければならなくなったときにも役立ちます。いきなり生SQLを書くのではなく、ActiveRecordクエリ呼び出しを行ってから最後にto_sqlに渡し、そのクエリ全体を別のクエリに渡すことができます。高度なトピックなので、これについては日を改めてご紹介したいと思います。

to_sqlのようなスーパー便利なメソッドを他にもご存知でしたら、ぜひコメント欄でお知らせください。

RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Rails: マイグレーションを実行せずにマイグレーションのSQLを表示する(翻訳)

[Rails5] Active Support Core ExtensionsのString#acts_like_string?

Rails5「中級」チュートリアル(3-5)投稿機能: 単一の投稿(翻訳)

$
0
0

概要

概要

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

Rails5中級チュートリアルはセットアップが短めで、RDBMSにはPostgreSQL、テストにはRSpecを用います。
原文が非常に長いので分割します。章ごとのリンクは順次追加します。

注意: Rails中級チュートリアルは、Ruby on Railsチュートリアル(https://railstutorial.jp/)(Railsチュートリアル)とは著者も対象読者も異なります。

目次

Rails5「中級」チュートリアル(3-5)投稿機能: 単一の投稿(翻訳)

新しいブランチに切り替えます。

git checkout -b single_post

単一の投稿を表示する

現時点では、show.html.erbテンプレートやそれに対応するコントローラアクションがないので、I'm interestedボタンをクリックするとエラーが表示されます。このボタンをクリックしたら、選択した投稿のページにリダイレクトされるようにしたいと思います。

PostsControllerの内部にshowアクションを作成します。ここでクエリをかけて特定のpostオブジェクトをインスタンス変数に保存します(Gist)。

# controllers/posts_controller.rb
  def show
    @post = Post.find(params[:id])
  end

I'm interestedボタンは、選択した投稿へのリダイレクトを行います。このボタンのhref属性には投稿へのパスが保存されます。投稿を1件取得するためのGETリクエストを送信すると、Railsはshowアクションを呼び出します。このshowアクションの中ではidパラメータ(params)にアクセスします。このidは、特定の投稿を1件取得するためのGETリクエストからのものです。たとえば、ブラウザで/posts/1パスにアクセスすると、id1の投稿を取得するリクエストが送信されます。

portsディレクトリの下にshow.html.erbテンプレートを作成します。

views/posts/show.html.erb

このファイルに以下のコードを追加します(Gist)。

<!-- views/posts/show.html.erb -->
<div id="single-post-content" class="container">
  <div class="row">
    <div class="col-sm-6 col-sm-offset-3">
      <div class="posted-by">Posted by <%= @post.user.name %></div>
      <h3><%= @post.title %></h3>
      <p><%= @post.content %></p>
    </div>
  </div><!-- row -->
</div>

postsディレクトリの下にshow.scssファイルを作成し、以下のCSSを追加してページのスタイルを整えます(Gist)。

// assets/stylesheets/partials/posts/show.scss
#single-post-content {
  background: white;
  height: calc(100vh - 50px);

  h3 { 
    text-align: center;
  }
  p {
    margin: 50px 0;
  }
  .posted-by {
    font-size: 12px;
    font-size: 1.2rem;
    margin: 20px 0;
    color: rgba(0,0,0,0.5);
  }
}

ここではページの高さを100vh-50pxで定義しているので、ページのコンテンツの高さはビューポート(viewport)の高さいっぱいになります。これにより、要素内のコンテンツの量が多くても少なくても、このコンテナの色はブラウザの高さいっぱいまで白になります。vhは「ビューポートの高さ」を表すので、100vhという値を指定すると要素はビューポートの高さの100%まで引き伸ばされます。ナビゲーションバーの高さを100vh-50pxから引いておく必要があります。そうしておかないと、コンテナの高さが50px余分に伸びてしまいます。

これで、I'm interestedボタンをクリックするとページがリダイレクトされて以下のように表示されます。

show.html.erbテンプレートには後で他にも機能を追加しますので、今は変更をcommitしておきましょう。

git add -A
git commit -m "Create a show template for posts

- Add a show action and query a post to an instance variable
- Create a show.scss file and add CSS"

spec

こういうモーダルウインドウや選択した投稿のリダイレクトといった機能が正常に動くかどうかのチェックは、手動で行わず、すべてspecに含めておきましょう。capybaraを使ってユーザーのアプリ操作をシミュレートすることにします。

featuresディレクトリの下にpostsディレクトリを作成します。

spec/features/posts

このディレクトリにvisit_single_post_spec.rbファイルを作成し、feature specをそこに追加します。追加後のファイルは以下のようになります(Gist)。

# spec/features/posts/visit_single_post_spec.rb
require "rails_helper"

RSpec.feature "Visit single post", :type => :feature do
  let(:user) { create(:user) }
  let(:post) { create(:post) }

  scenario "User goes to a single post from the home page", js: true do
    post
    visit root_path
    page.find(".single-post-card").click
    expect(page).to have_selector('body .modal')
    page.find('.interested a').click
    expect(page).to have_selector('#single-post-content p', text: post.content)
  end

end

ここでは、手動での操作手順をすべて定義しています。最初にhomeページを開き、投稿をクリックすると、モーダルウインドウがポップアップすることを期待(expect)しています。I'm interestedボタンをクリックすると、投稿ページにリダイレクトしてコンテンツが表示されることを期待しています。

RSpecのhave_selectorhave_cssなどのマッチャーは、ある要素がユーザーに実際に見える状態になっているとデフォルトでtrueを返します。したがって、投稿でクリックした後にテスティングフレームワークはモーダルウインドウが表示されることを期待します。ある要素がユーザーに見えるかどうかは問題でない場合や、要素がDOMにあるかどうかだけを知りたい場合は、visible: false引数を追加します。

テストを実行してみましょう。

rspec spec/features/posts/visit_single_post_spec.rb

変更をcommitします。

git add -A
git commit -m "Add a feature spec to test if a user can go to a
single post from the home page"

single_postブランチをmasterにmergeします。

git checkout master
git merge single_post
git branch -D single_post

関連記事

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

Rails: Form Objectと`#to_model`を使ってバリデーションをモデルから分離する(翻訳)

$
0
0

概要

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

タイトルは内容に即したものに変えました。

Rails: Form Objectと#to_modelを使ってバリデーションをモデルから分離する(翻訳)

Rails 5.2リリースノートをひととおり読んでみて、ActiveStorageなどに興味を惹かれたので、試してみたくなりました。本記事ではプレリリース版を用いてシンプルなアプリをビルドしました。

目的は、ユーザーがアンケート(questionnaire)を作成して結果を回収できるアプリを作成することです。最初にForm Objectを用いて、アンケートのタイトルと質問リストを取得します。

# db/migrate/create_questionnaires.rb
create_table "questionnaires", force: :cascade do |t|
  t.string "title"
  t.string "questions", default: [], null: false, array: true
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end
# app/forms/new_questionnaire_form.rb
class NewQuestionnaireForm
  include ActiveModel::Model

  attr_accessor :title, :questions

  validates :title, presence: true

  def save
    Questionnaire.new(title: title, questions: questions).save
  end
end

このように書いてみたかった理由は、Ectoを見たときに、バリデーションをモデルから切り離せるのがとても便利だと思えたからです(条件付きバリデーションが不要になります!)。

コントローラはいたってシンプルで、scaffoldしたコントローラと大差ありません。以下はnewアクションとcreateアクションです。

# app/controllers/questionnaires_controller.rb
class QuestionnairesController < ApplicationController
  def new
    @questionnaire = NewQuestionnaireForm.new
  end

  def create
    @questionnaire = NewQuestionnaireForm.new(questionnaire_params)

    if @questionnaire.save
      redirect_to @questionnaire, notice: 'Questionnaire was successfully created.'
    else
      render :new
    end
  end
end

ビューでは、新しいform_withヘルパーを次のように使っています。

<!-- app/views/questionnaires/_form.html.erb -->
= form_with(model: @questionnaire, local: true) do |form|
  = form.label :title
  = form.text_field :title

アプリを起動してquestionnaires/newにアクセスしてみると、undefined method 'new_questionnaire_forms_path' for ...エラーメッセージが表示されました…。

うう残念。Railsは、form_withに渡したForm Objectのクラス名を受け取ると、対応するコントローラへのパスであると自動的に推論しますが、コントローラの名前だけが合っていません。

この修正方法はいくつか考えられます。form_withヘルパーの投稿先のURLを上書きする方法もあれば、Form Objectの#model_nameを上書きして、今扱っているのがQuestionnaireであるかのように見せかける方法もあります(この方法が有用なこともありますが、この状況ではダーティハックの恐れがあります)。

もっとよい方法を見つけるために、先ほどのForm Objectに立ち戻りましょう。このForm Objectは、Questionnaireという単一のオブジェクトだけを扱っています。しかも#saveメソッドでQuestionnaireを作成しています。先のForm Objectからモデルへの変換をシャドウイング(shadowing)として表すことができそうです。そしてActiveModel::Conversion#to_modelというメソッドがあることに気が付きました。このメソッドを使うようForm Objectを書き直すと次のようになります。

class NewQuestionnaireForm
   include ActiveModel::Model

  def to_model
    Questionnaire.new(title: title, questions: questions)
  end

  def save
    to_model.save
  end
end

questionnaires/newにアクセスすると、今度はちゃんとフォームが表示されます。タイトルを入力して[Submit]ボタンを押すと、データベースにquestionnaireのモデルが新しく作成されます。できました!

関連記事

Rails: Form ObjectとVirtusを使って属性をサニタイズする(翻訳)

Rails: dry-rbでForm Objectを作る(翻訳)

週刊Railsウォッチ(20180302)Ruby 2.6.0-preview1とWebpack 4.0リリース、爆速検索APIサービスAlgolia、Clowneでモデルをクローンほか

$
0
0

こんにちは、hachi8833です。Ruby生誕25周年おめでとうございます!

3月最初のウォッチ、いってみましょう。最近社内での記事つっつき会が盛り上がっていて嬉しい限りです。

Rails: 今週の改修

Rails 5.2はまだ動いています。「2月中に出したい」は叶いませんでした。以下は5.2のcommitからです。

紀元前の日付の扱いをPostgreSQLに合わせた

# activemodel/lib/active_model/type/date.rb#L44
         def new_date(year, mon, mday)
-          if year && year != 0
+          unless year.nil? || (year == 0 && mon == 0 && mday == 0)
             ::Date.new(year, mon, mday) rescue nil
           end
         end

つっつきボイス: 「出た〜紀元前!」「どこに使うんだろうか」「は〜ん、年を-4みたいにできるようになったのか↓」

# activerecord/test/cases/adapters/postgresql/date_test.rb#L31
+  def test_bc_date_leap_year
+    date = Time.utc(-4, 2, 29).to_date
+    topic = Topic.create!(last_read: date)
+    assert_equal date, Topic.find(topic.id).last_read
+  end

PostgreSQLのdatedatetime同様に無限値を扱えるようにした

# activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
+# frozen_string_literal: true
+
+module ActiveRecord
+  module ConnectionAdapters
+    module PostgreSQL
+      module OID # :nodoc:
+        class Date < Type::Date # :nodoc:
+          def cast_value(value)
+            case value
+            when "infinity" then ::Float::INFINITY
+            when "-infinity" then -::Float::INFINITY
+            else
+              super
+            end
+          end
+        end
+      end
+    end
+  end
+end

つっつきボイス: 「どゆこと?」「PostgreSQLって'-infinity'::dateって書き方できるとは」「さすがぽすぐれ: 知らないと使わないだろうけど」「マイナスのInfinityもあると: そりゃそうだ」

# activerecord/test/cases/adapters/postgresql/date_test.rb#L6
+class PostgresqlDateTest < ActiveRecord::PostgreSQLTestCase
+  def test_load_infinity_and_beyond
+    topic = Topic.find_by_sql("SELECT 'infinity'::date AS last_read").first
+    assert topic.last_read.infinite?, "timestamp should be infinite"
+    assert_operator topic.last_read, :>, 0
+
+    topic = Topic.find_by_sql("SELECT '-infinity'::date AS last_read").first
+    assert topic.last_read.infinite?, "timestamp should be infinite"
+    assert_operator topic.last_read, :<, 0
+  end

「そういえばMySQLのdateは確か内部でstringになってて、マイナスすると9999みたいに9並びになっちゃう9999-99-99みたいなありえない日付のクエリでも通ってしまう」「えー」「PostgreSQLは確か型チェックか何かをやってて、9999はまだしも9999-99みたいなのになるとエラーになったと思う」

参考: MySQL 5.6マニュアル 11.3.1 DATE、DATETIME、および TIMESTAMP 型

DATETIME 値の範囲は ‘1000-01-01 00:00:00.000000’ から ‘9999-12-31 23:59:59.999999’ であり

MemCacheとRedisでローカルキャッシュのread_multifetch_multiをサポート

# activesupport/lib/active_support/cache/strategy/local_cache.rb#L123
+          def read_multi_entries(keys, options)
+            return super unless local_cache
+
+            local_entries = local_cache.read_multi_entries(keys, options)
+            missed_keys = keys - local_entries.keys
+
+            if missed_keys.any?
+              local_entries.merge!(super(missed_keys, options))
+            else
+              local_entries
+            end
+          end


つっつきボイス: 「RedisとかMemCacheのAPIで1個ずつアクセスするより一括で取ってくる方が速いんで、そうなるように拡張系の命令を使ったと: まあ書いてあるとおりですねw」

シリアライズのinclude:が効かなくなることがある問題を修正

# activemodel/lib/active_model/serialization.rb#L180
         unless includes.is_a?(Hash)
-          includes = Hash[Array(includes).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
+          includes = Hash[Array(includes).flat_map { |n| n.is_a?(Hash) ? n.to_a : [[n, {}]] }]
         end

つっつきボイス: 「ネステッドなシリアライズかー」「ま、自分はシリアライズでJSONにしちゃうこと多いけど: ハッシュをシリアライズするよりJSONにしとけばRuby以外でも使えるし」「確かにー」

RuboCopの設定でFoo::methodスタイルを禁止に

# .rubocop.yml#L159
+# Prefer Foo.method over Foo::method
+Style/ColonMethodCall:
+  Enabled: true

RuboCopのデフォルト設定ではメソッド呼び出しの::記法は禁止されていないんですね。


つっつきボイス:YAML::loadみたいな記法見たことあったな」「クラスメソッドならこの呼び方も一応できる」

::は、定数(クラスやモジュールも含む)、コンストラクタ(Array()Nokogiri::HTML()など)の参照にのみ使う
Rubyスタイルガイドを読むより


Rubyスタイルガイドを読む: 文法(1)メソッド定義、引数、多重代入

Rails 6ではUTF-8エンコーディングを強制しなくなる

ここからはRails 6のmasterブランチです。

主要なWebサイトのほとんどでTLS 1.0が無効になったことで、IE8以下のブラウザの利用がどんどん困難になった。他のブラウザには影響しないので、UTF-8エンコーディングはデフォルトでは強制しないことにする。
Andrew White


つっつきボイス: 「『これでShift_JISで書けるぜ!』なんてことにはならないだろうけどね」「nilで『強制しない』という意味なのか↓」

# actionview/lib/action_view/railtie.rb#L12
+    config.action_view.default_enforce_utf8 = nil

「昔話になっちゃうけど、ガラケー時代のエンコーディング周りはほんと地獄でしたねー: キャリアに応じてShift_JISで出力したりEUC-JPで出力したりとか、キャリアごとに顔文字変換するとか」「音符がうんこになるバグなんてのもありましたね」「そもそも携帯ネットワークはインターネットでもなければIPネットワークすらないし、RFC準拠とかなかったし」「ひえ〜」「メアドをRFC準拠の正規表現で処理したらユーザーからメアドが通らなくなったって苦情が来たり: 数字で始まったりドット2つ重ね..みたいな非RFCなメアドが使われちゃってた」

参考: 【PHPで作る】初めての携帯サイト構築 – 第3回 携帯サイトの文字コードに気をつける

新機能: "rails routes --expandedを追加

$ rails routes --expanded
--[ Route 1 ]------------------------------------------------------------
Prefix            | high_scores
Verb              | GET
URI               | /high_scores(.:format)
Controller#Action | high_scores#index
--[ Route 2 ]------------------------------------------------------------
Prefix            | new_high_score
Verb              | GET
URI               | /high_scores/new(.:format)
Controller#Action | high_scores#new
--[ Route 3 ]------------------------------------------------------------
Prefix            | blog
Verb              |
URI               | /blog
Controller#Action | Blog::Engine

[ Routes for Blog::Engine ]
--[ Route 1 ]------------------------------------------------------------
Prefix            | cart
Verb              | GET
URI               | /cart(.:format)
Controller#Action | cart#show

こんな感じで出せるそうです。


つっつきボイス: 「これうれしい!」「Route 1はちょいダサかな」「いつものrails routesだとURLがめちゃ長くなりますからね」

新機能: ActiveModel::Attributes#attributesを追加

# activemodel/lib/active_model/attributes.rb#L69
+    def attributes
+      @attributes.to_hash
+    end

つっつきボイス: 「アトリビューツアトリビューツw」「何という名前」「その実態は#to_hashと: ということはattributesはハッシュじゃないのかな」「あと#to_hashすることでオブジェクトが別になるからdupしていることになる」

システムテストのスクショのパスを絶対パスに変更

ログからすぐ開くのに便利だからだそうです。

# actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb#L46
           def image_path
-            @image_path ||= absolute_image_path.relative_path_from(Pathname.pwd).to_s
+            @image_path ||= absolute_image_path.to_s
           end

つっつきボイス: 「こういう変更は地味に大事!マジで」「Pathname.pwdもUnixのシェルコマンドそのまんま」「先週のウォッチで出たrm_rfみたい」

Rails

Rails 5.2.0の良スライド3本立て


つっつきボイス: 「TechRachoでも5.2.0をまとめようと思ってたんですが、重要な部分はこのスライド3本でほぼ網羅されているんで、もういいかなと」「いいと思いまーす」「『ここを見ろ、以上』ということでー」

そのまま一同でしばしスライドつっつきになだれ込みました。

apply_join_dependencyかー」「association scopeのJOINが無視されてた話とか、つらいなー」

Webpack 4.0がリリース


auth0.comより


  • Node JS 4のサポート廃止: 今後はNode >= 8.9.4で
  • さよならCommonsChunkPlugin、こんにちはSplitChunksPlugin
  • WebAssemblyをサポート
  • javascript/autoなどのModule Typeをサポート
  • developmentモードとproductionモードを導入
  • webpack.config.jsでのエントリポイント定義が不要に
  • ビルドの高速化
  • プラグインシステムの刷新
  • コマンド体系がwebpck-cliに全面移行(webpack-cliは今後インストール必須)
  • NoEmitOnErrorsPluginなどの非推奨化

つっつきボイス: 「Node JS 4のサポート廃止は割りと大きいかも: これ系に依存してるのって結構あった気がする」「Zapierは”node.js v4.3.2″ってなってますね」「WebAssemblyもいいな」

書籍『99 Bottles of OOP』


sandimetz.comより

名著『オブジェクト指向設計実践ガイド』でおなじみのSandi Metz女史の新刊が昨年から発売されています。ちょうど翻訳まで終えた記事「Why We Argue: Style — Sandi Metz」の末尾の広告で知りました。セールに気づけばよかった…

同書の章タイトルだけ自分用に雑に訳してみたのですが、場所をとるのでGistに置いてみました。

なお、同書のタイトルは以下ののもじりです。宴会で一気飲みを煽るときに歌うんでしょうか。


つっつきボイス: 「↑ジャケがいい」「酔っぱらいの歌ですな」
「前作『オブジェクト指向設計実践ガイド』は手続き脳が抜けきれてない私にはすごくよかったんで、くやしいけどこの本も定価で買っちゃいました」「ワタイも買ってたのに後忘れてたー」「そういえばJavaのインターフェースって特異だから、あの考え方を前提にするとRubyではどう適用したらいいのか最初よくわかんないかもしれないですね」「それはあるかも」
「たとえばPHPやPerlみたいな世界からRuby脳に変わるときにはオブジェクト指向設計ができてないといろいろ大変っすね: Rubyのライブラリがそもそもオブジェクト指向全開なものが多いし」「とにかくこういう本は一冊読んでおくべき」

参考:

ActiveRecord.no_touchingでtouchingを一時的に無効化する(RubyFlowより)

# 同記事より
user = User.find(user_id)

ActiveRecord::Base.transaction do
  User.no_touching do
    user.photos.find_each do |photo|
      # userはtouchされない
      photo.update!(some_attributes)
    end
  end

  user.touch
end

つっつきボイス: 「む、サイトが開かない」「落ちてるかな?」

今は動いています。

参考: Rails API no_touching

KubernetesデプロイのためにRailsをリファクタリングした(RubyFlowより)

# 同記事より
require 'yaml'
require 'erb'

class ConfigParser
  def self.parse(file, environment)
    YAML.load(ERB.new(IO.read(file)).result)[environment]
  end
end

つっつきボイス: 「Kubernetesはこれだけconfigしないといけない↓のが大変: それぞれenvをどうやって注入するかとか」「そういうコンテナサービスについて気を付けるべき点は例の超定番『The Twelve-Factor App』で言われてますね」

  • ローカルアプリ
  • Dockerコンテナを使うローカルアプリlocal app using docker containers
  • docker-composeを使うローカルアプリ
  • CapistranoをOpenStackとベアメタルサービスにデプロイ
  • Kubernetesからminikubeと実際のクラスタにデプロイ


12factor.netより

そういえばRuby25の会場でtagomorisさんが「クバネーティス」と発音していました。個人的には「クーベルネイテス」の方が何だかドイツ語っぽくてええかなという気もしたり(↓ギリシャ語由来だそうです)。

タイムゾーン関連記事2本(Hacklinesより)

いずれも短い記事です。

# 同記事1より
class ApplicationController
  around_filter :set_time_zone

  private

  def set_time_zone
    old_time_zone = Time.zone
    Time.zone = 'Asia/Kolkata' # current_user.time_zone
    yield
  ensure
    Time.zone = old_time_zone
  end
end
# 同記事2より
  def in_time_zone(zone = ::Time.zone)
    time_zone = ::Time.find_zone! zone
    time = acts_like?(:time) ? self : nil

    if time_zone
      time_with_zone(time, time_zone)
    else
      time || self.to_time
    end
  end

つっつきボイス: 「うんうん、マルチロケールのアプリでこういうのやるよね: 弊社のManga Rebornでもそういうコード書いた覚えある」「記事1はTime.zone使ってて、記事2はTime.zone使うなin_time_zone使えって言ってるー」
Time.zoneってクラス変数だった気がするんだけど、マルチスレッドだと死ぬやつじゃね?」「それともconfigか? configならスレッドスコープ」「あ、両方か↓: config.time_zoneTime.zone」「一時的に使うだけならTime.zoneでやるなということね」

# http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.htmlより
# application.rb:
class Application < Rails::Application
  config.time_zone = 'Eastern Time (US & Canada)'
end

Time.zone      # => #<ActiveSupport::TimeZone:0x514834...>
Time.zone.name # => "Eastern Time (US & Canada)"
Time.zone.now  # => Sun, 18 May 2008 14:30:44 EDT -04:00

参考: Rails API in_time_zone

外部キーなしでActiveRecord関連付けを行う(RubyFlowより)

# 同記事より
def find_target
  # ... あまり関係ないコードも混じってるのであしからず ...
  sc = reflection.association_scope_cache(conn, owner) do
    StatementCache.create(conn) { |params|
      as = AssociationScope.create { params.bind }
      target_scope.merge(as.scope(self, conn)).limit(1)
    }
end

つっつきボイス:unscope?」「unscopeは割りと使いますよ: デフォルトスコープのorderだけ消したりとか」「今はreorderとかあるけど」

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

rails_refactor: 名前リファクタリングgem(Hacklinesより)


つっつきボイス: 「あー、これコントローラ名をリファクタリングするgemか」「IDEの機能一発でできるかなと思ったんですが」「できないんですねこれが: routes.rbとかビューとかあちこちリネームが必要になるんで地味ーに面倒」「ときどき直し漏れがあったり: でgit commitするときに気づくと」
「まーGemfileに追加してまでやることかな?とは思う」「使い捨てですかね」「でもこの機能がRailsに標準で取り込まれるならわかるし、あったらすごくうれしい: rails generate renameみたいな感じで」「あー!それ欲しいかもー!」

searchkick: ElasticsearchとRailsでインテリジェント検索(Awesome Rubyより)

ankaneさん作のgemです。

# 同リポジトリより

# モデル
class Product < ApplicationRecord
  searchkick
end

# インデックスにデータを追加
Product.reindex

# クエリ
products = Product.search("apples", fields: [:name])
products.each do |product|
  puts product.name
end

つっつきボイス: 「へーこういう曖昧マッチが効くのか↓」「Elasticsearchのハンドラ的なやつっぽいけど、結構何でもできそうに見える」

  • stemming – tomatoes matches tomato
  • special characters – jalapeno matches jalapeño
  • extra whitespace – dishwasher matches dish washer
  • misspellings – zuchini matches zucchini
  • custom synonyms – qtip matches cotton swab

Clowne: Rubyモデルをクローンするgem(RubyFlowより)


同記事より

# 同記事より
# app/cloners/order_cloner.rb
class OrderCloner < Clowne::Cloner
  include_association :additional_items
  include_association :order_items, scope: :available

  nullify :payed_at, :delivered_at

  finalize do |source, record, _params|
    record.promotion_id = nil if source.promotion&.expired?
    record.uuid = Order.generate_uuid
    record.total_cents = OrderCalculator.call(record)
  end
end

Clowneは「道化師」(clown)とクローンを掛けてますね。


つっつきボイス: 「ははぁ、上の図↑みたいにリレーションがチェインしているものをクリーンにクローンできるってことか」「たとえばユーザーアカウントの項目が複数のテーブルでできていて、そのアカウントをコピーしたい、でもリレーション先のマスターはコピーしたくない、とか」
「ところで他人が書いたdupとかって基本信じたくないというのは、ありますね」「あーたしかにdeep copy的なのは自分で実装しないとコワイw」「ActiveRecordに特化してそういう部分をやりやすくしてくれるgemなんでしょう、きっと」「複雑なやつをいっぱいクローンしないといけないときとかにはありがたいかも」
「そういえばJavaだとこの手のcloneメソッドは自分で書かないといけないようになってましたね」「ですですー」

Overcommitで各種静的解析を呼んでRubyコードをlintする(Random Rubyより)

Git Hookを使ってコミット時のフックでRuboCopなどを呼んでいます。



同リポジトリより


つっつきボイス: 「社内で使ってた人がいたのを思い出したので」「Overcommitは、基本入れた方がいいですね: CIが重いとつらくなるけど」「CIがfailしたらgit commitが失敗するようにしたり」「それはスバラシイ!」

「お、ちょうどいいところに: Overcommitって使ってみてどうでした?」「…みんなが使ってくれないと意味がないかも…」「あ確かに」「CI側をがっつり設定して、CIがfailするものをプッシュするとすごく怒られるようにしておけば、みんな自主的にこういうツールを使うようになるかも」

VAPIDでプッシュ通知

# 同記事より
# app.rb
post '/push' do
  Webpush.payload_send(
    message: params[:message]
    endpoint: params[:subscription][:endpoint],
    p256dh: params[:subscription][:keys][:p256dh],
    auth: params[:subscription][:keys][:auth],
    ttl: 24 * 60 * 60,
    vapid: {
      subject: 'mailto:sender@example.com',
      public_key: ENV['VAPID_PUBLIC_KEY'],
      private_key: ENV['VAPID_PRIVATE_KEY']
    }
  )
end

正直、プッシュ通知しようとするサイトはむかつきますが。


つっつきボイス: 「RFCだから一応標準」「Webプッシュで認証/承認周りをやってくれるのね」

参考: Web PushをFCMとVAPIDで認証してブラウザにプッシュ通知を送る

Proxyパターンを再考する(Hacklinesより)

#prependをProxyパターンで使うお話です。

# 同記事より
module Proxy
  require 'etc'

  # Dynamically re-creates receiver's class methods, intercepts calls 
  # to them and checks user before invoking parent code
  #
  # @param receiver the class or module which has prepended this module
  def self.prepended(receiver)
    obj_mthds = receiver.instance_methods - receiver.superclass.instance_methods
    obj_mthds.each do |m|
      args = receiver.instance_method(m).parameters # => [[:req, :x], [:req, :y]]
      args.map! {|x| x[1].to_s}
      Proxy.class_eval do
        define_method(m) do |*args|
          puts "*** intercepting method: #{m}, args: #{args}"
          raise "Unauthorised access!" unless Etc.getlogin == 'fred'
          super(*args)
        end
      end
    end
  end
end #module

class Account
  def initialize(balance)
    @balance = balance
  end

  def deposit(amount)
    @balance += amount
  end

  def withdraw(amount)
    @balance -= amount
  end

  def self.interest_rate_for(a_balance)
    a_balance > 10_000 ? '3.2%' : '5.5%'
  end

  prepend Proxy

end

つっつきボイス: 「変更前のコードの方、method_missingしてsendするとかエグいなー: Javaのひとたちはこういうコードが許せなかったりとか?」「いやーそこまではないかもー」「これは認証のプロキシなのか!: 権限がなければraiseして、権限があればデレゲートすると」「天才か?」

「で変更後のコード↑はというと、method_missingを使わずに、prependの中でclass_evaldefine_method使ってやると」「たしかにー: 変更後の方がキレイ」「とってもProxyパターン」

Ruby: Proxyパターンの解説(翻訳)

参考: Module#prepend

CertBot: Let’s Encrypt証明書をもっと楽に導入


certbot.eff.orgより: (CC BY

Let’s Encrypt Ruby on Rails and Nginxにしようかと思ってたのですが、記事中のこちらの方が気になったので。

主催しているEFFはElectronic Frontier Foundationという非営利団体です。


つっつきボイス: 「CertBotは有名っすね」「あれLet’s Encrpyptコマンドじゃなかったっけ?」「↓名前変わってた…」「ややこしいのう」

Certbot クライアント(旧・Let’s Encrypt クライアント)
https://letsencrypt.jp/command/より

「まあapt-cache searchしてあるほうを入れればいいかとw」「多分ディストリビューション公式のパッケージ使う方がそのdistroのapache/nginxバージョンにちゃんと合わせられてるので、機能的な問題とかない限り、無理に最新にこだわらない方が安心して使えそう」

参考: Wikipedia-ja 電子フロンティア財団

dev.toは記事のソーシャル画像をこうやって自動生成している(Hacklinesより)

# 同記事より
  def enter_urls
    urls.each do |url|
      3.times do
        enter_url_and_click_preview("#{url}?#{rand(10_000)}=#{rand(10_000)}")
      end
      enter_url_and_click_preview(url)
    end
  end

つっつきボイス: 「SEO業界では記事の冒頭に何か大きい画像を置いとくとアクセスが増えるみたいなのがありますね」「神社の御札みたいなw」「まあ確かにそういう画像があると視線の取っ掛かりにはなりそう」「文字ばっかりの記事よりはデザイン的な座りもよくなりそう」「randかよ↑」

Railsコンソールで便利なヘルパーメソッドを書く(RubyFlowより)

オーストラリアの方だそうです。

# 同記事より
module ConsoleMethods
  def load_jwt!
    token = ::JWT.create(user_id: 'USER_ID_HERE')
    RequestLocals.store[:jwt] = token.jwt
  end
end

つっつきボイス: 「へーConsoleMethodsを拡張するとRailsコンソールでいろいろやれるのか」「Railsコンソールをそこまで使うかどうかというのはあるけど、利用頻度が高いならこうやってヘルパー化しとくといいかも」

RailsをOracleに接続する手順(RubyFlowより)

# 同記事より
default: &default
  adapter: oracle_enhanced
  database: "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(Host=localhost)(Port=1521))(CONNECT_DATA=(SID=xe)))"

development:
  <<: *default
  username: "myproject_development"
  password: "myproject_development"

test:
  <<: *default
  username: "myproject_test"
  password: "myproject_test"

production:
  <<: *default
  username: "myproject_production"
  password: "myproject_production"

oracle-enhancedアダプタはdb:createができないらしいので、SQLで直接やってます。


つっつきボイス: 「来たなオラクル: 考えたくないヤツ」

Algolia: 爆速リアルタイム検索APIサービス


algolia.comより

とりあえずデモはめちゃめちゃ速いです。


つっつきボイス: 「そのデモサイトで適当に映画のタイトルぶっこんでみてください」「おーマジ速いわこれ!」「しかもUIカッケ~」「日本語だとあんまり引っかからないかも?」
「こういうの真面目にスクラッチから設計して作ってこれだけの性能出すのはタイヘン」「Solrとかカテゴリテーブルとか色々設計してSQLをWHERE文なりGROUP文なりを地獄のような感じで設計するとか」「有料だけどこれ使って速くなるなら使ってもええんじゃね?と思う」「裏でElasticsearchとか動いてるのかな」


algolia.com/productより

redis-cell: Redisをコマンドラインで使える

とりあえず手元で動かしてみました。Java 8入れろと言われました。


つっつきボイス:libredis_cell.soを読み出せる」「soできる!」「これで差し替えっぽく使えるという感じかな」

MonitでRailsアプリを監視(RubyFlowより)


mmonit.com/monit/より

監視というとやっぱりイヌですね。


つっつきボイス: 「Monitなつかし!」「こないだSupervisor入れようと思ってたら環境古すぎてMonit入れたけどなっ」「Monitは定番、かつめちゃ歴史長い: いわゆるスーパバイザです」「↓こういう処理とか、やりたいと思うことはひととおり書ける優秀なヤツ」「configはところどころよくわからないけど」

<!-- 同記事より -->
<!-- config/deploy/shared/monit.erb -->

check host <%= fetch(:website_url) %> with address <%= fetch(:website_url) %>
  if failed
    icmp type echo count 5 with timeout 15 seconds
  then alert

Datadog: サイト監視・分析サービス


datadoghq.comより

いっぬがカワイイ。無料プランで5ホストまでできるそうです。


つっつきボイス: 「こっちの方がかわいかったので」「そこ!?」「このDatadogも定番: サービス監視/サイト監視にも使うし、メトリック監視にも使われる」

DHHとconcerns


同ツイートより


つっつきボイス: 「おー大量のconcern!」「mixinがとてもうまく書かれているとこうなる」「しかし1回しか使わないようなmixinをconcernにするかと言われると…」「ですねー」

「これ厳密にはconcernじゃないかもしれないっす: Basecampのディレクトリ構成って、app/の下にこの場合はRecordings(複数形)というディレクトリを作って、その下にこういうのが置かれていました」「へー!」「concernで置きまくるんじゃなくて、あるモデルの配下にそれ用のモジュールを入れるみたいなフォルダ構成なんで、それならまあありかなとは思いました」「あーモデル名と同じフォルダが別にあってその下に置くと: それならワカル」「つまりincludeだけどあちこちで使われるんじゃなくて、あくまでそのモデル用のmixinか」「RuboCopの『1クラス100行制限』で怒られないためにはこのぐらい分散しないと」「例のYouTubeの番組を見た限りではそんな感じでした」

「このincludeは互いの依存関係のあるなしに応じてきれいに書き分けられているからとても読みやすい」「ここまでキレイに書けたらスゴイ」「includeは順序変わると動かなくなったりしがちですね」

「ところでDHHのYouTubeチャンネル、早くも一週間ほど更新止まってますね(´・ω・`)」「こりゃスターウォーズばりに次回作待たされるかもww」

「あとDHHの書き方なんですが、もう全然TDDじゃないんですよ」「お?」「モデルのテストすら全部振る舞いベースで、unitテストを書かないんで、スゲーなーと」「まあ確かに、自分で書いていない、提供されている機能だけを使うんだったらそのunitテストってなくてもいいんじゃね?って思ったりするな」「おー」「実はunitテスト懐疑派w」「たとえばモデルをcreateするテストとかは必要だと思うけど、そういうのはunitテストというより機能テストに近いんじゃないかな」

ここまでいける


つっつきボイス: 「ちゃんとメンテするとこのぐらいになるという見本」「しかし20msecはスゴイ」

Ruby trunkより

Ripperの新機能使いたいからnobuさんのパッチ使わせて

irbでRipperを使いたいそうです。確か今のirbはRubyで書かれた独自パーサーでした。

参考: Rubyリファレンスマニュアル Ripper

Ruby 2.5.0の「逆順バックトレース」がつらすぎる

「Railsで開発していると、バックトレースが逆順だったりそうでないところがあって頭がおかしくなりそう」だそうです。


つっつきボイス: 「何で逆順にしたんだろね?」「コールスタックを登っていく感じを出したかったとか?」

Ruby

Ruby 2.6.0-preview1がリリース(Ruby公式ニュースより)

Ruby生誕25周年のその日にリリースされました。

  • JITで--jit-verbose=1を指定可能に
  • 以下を含む新メソッド
  • パフォーマンス向上
    • Proc#call
    • block.callにブロックパラメータを渡した場合
    • ブロック渡しのパフォーマンス全般
  • その他
    • $SAFEがプロセスグローバルになり、0を渡せるようになった
    • ERB.newの引数の変更

とりあえず記念写真。

SinatraベースのSmashingとWordPressとGitHubウィジェットでイケてるダッシュボードを作った(RubyFlowより)


同記事より

マジックコメント一発でRubyを最適化

# 同記事より
# frozen_string_literal: true

HASH = {
  "mike": 123
}

def getmike
  HASH["mike"]
end

コメント欄で「細かい点がちょっと違うんじゃ?」とツッコまれています。


つっつきボイス: 「Ruby 2.2以降はハッシュキーのstring literalを自動でfreezeするのかー」「String.newと空文字""リテラルのエンコーディングは違う…だと?↓String.new使わんけどなっ」「本編記事よりコメントの方が情報量多いかも」

String.new.encoding
# => #<encoding:ascii-8bit>

"".encoding
# => #<encoding:utf-8>

オブジェクト指向だと何ができるようになるか(Ruby Weeklyより)

これもSandi Metz女史のがっつり読み物系記事です。

Rack::Proxyクイックツアー(RubyFlowより)

Rack Proxyはちょっと工夫するといい感じになるそうです。

  • サブドメインベースで複数のアプリにパススルーする
  • 移動したページへの面倒なリダイレクトルールを扱うのに便利
  • 単一のAPIリクエストを複数のコンカレントバックエンドリクエストに分散し、結果をマージする
  • 認証/承認をリクエストのプロキシより優先的に内部の信頼できるバックエンドに無条件に投げる
  • あるドメインから別のバックエンドへのプロキシでCORSが複雑になるのを回避する

つっつきボイス: 「Rackミドルウェアでやるのは確かに簡単で便利: 裏を返すと、Nginxの設定がいかに死ぬほど特殊で面倒くさいかということでもある」「それこそApacheのmod_rewriteに匹敵する地獄感を味わえますよ」「sendmail.cfとか」

参考: Mozilla オリジン間リソース共有 (CORS)

Rubyで文字列の重複を削減する(Ruby Weeklyより)

# 同記事より
if ENV['RAILS_ENV'] != "production"
  exec "RAILS_ENV=production ruby #{__FILE__}"
end

require 'memory_profiler'

MemoryProfiler.report do
  # this assumes file lives in /scripts directory, adjust to taste...
  require File.expand_path("../../config/environment", __FILE__)

  # we have to warm up the rails router
  Rails.application.routes.recognize_path('abc') rescue nil

  # load up the yaml for the localization bits, in master process
  I18n.t(:posts)

  # load up all models so AR warms up internal caches
  (ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table|
    table.classify.constantize.first rescue nil
  end
end.pretty_print

frozen_string_literal: true以外にもいくつかRubyの文字列のdupを防ぐ方法を試しています。


つっつきボイス: 「この+""って書き方↓いつも不思議でしょうがないんだよなー」「どうしてこうなった感」

buffer = String.new
buffer.encoding => Encoding::ASCII-8BIT

# vs 

# String @+ is new in Ruby 2.3 and up it allows you to unfreeze
buffer = +""
buffer.encoding => Encoding::UTF-8

Rubyで分散ファイル同期(Hacklinesより)

Ruby Distributed File SyncでRDFSだそうです。Dropbox的なことをやってみたかったとのこと。


つっつきボイス: 「こういうDropbox的なオープンソースソフトウェアありましたねー: 名前思い出せん…SynkThinkだ!」「割りとよくできますこれ: Win/Linuxでも使えるし」


syncthink.comより

Ruby 3のGuildとは


engineering.universe.comより


つっつきボイス: 「Guildの情報が意外に少なかったんですが、これが割りとまとまってるっぽかったので」

TruffleRuby Native

メモリ喰いの代わりに爆速で知られるTruffleRubyのネイティブオプションについての記事です。

MJIT記事の極めつけ

英語版まで出してます。


つっつきボイス: 「この間の翻訳記事↓より後に公開されててちょっとほっとしました」「MJITの作者自らの記事には勝てないっしょw」

Rubyの新しいJIT「MJIT」で早速遊んでみた(翻訳)

こういう書き方


つっつきボイス: 「『とかしてなくて』w」「自分もアクセサでやっちゃったりするかなー: メンバ生で触るの怖いときとか」「まあ隠しとけと言えばそれまでだけど」

MatzのRuby25スピーチまとめ

RubyKaigi 2018 CFP締め切りの駆け込みっぷりが凄い

サイト: RubyKaigi 2018 closed


rubykaigi.orgより

あるあるとはいえ、笑ってしまいました。

SQL

「Why upgrade PostgreSQL?」: PostgreSQLのバージョンごとの修正点を比較表示できるサイト(Postgres Weeklyより)


つっつきボイス: 「これ便利かも」「出力はベタベタっすね…」「読みづらっ!」

(動画)KubernetesネイティブのPostgreSQL(Postgres Weeklyより)

AWS RDSがPostgreSQL 10をサポート(Postgres Weeklyより)

JavaScript

2018年のJavaScript事情(JavaScript Weeklyより)

Reduxの原則に沿ってReact.jsでフロントエンド検索ウィジェットを作る

Mozillaのsource/mapのパフォーマンスをRustとWebAssemblyで改善(JSer.infoより)

TypeScript Deep Dive


つっつきボイス: 「ところで音声読み上げってヒアリングの練習にいいかも」「最近の英語音声読み上げのクオリティ凄いし」

Vue.jsとVuexで自動セーブする

CSS/HTML/フロントエンド

簡単なものはいずれ面倒になる(Frontend Weeklyより)

制約条件の理論をソフトウェアに適用する(Frontend Weeklyより)

「制約条件の理論」は経営寄りの概念だそうです。

参考: Wikipedia-ja 制約条件の理論

Utility-First CSSと関数型的CSS(Frontend Weeklyより)

Go言語だけでフロントエンドする

先週取り上げたhttps://easydatawarehousing.github.io/ferro/にも通じそう(こちらはRubyとOpalとFerro)。

30 Seconds of CSS: CSSスニペットあんちょこサイト(GitHub Trendingより)


atomiks.github.ioより

スニペット数はまだそんなにありませんが、1週間ほどで★1600超えです。


つっつきボイス: 「お、これも中々便利じゃん?」「この間の30 seconds of codeに通じますね」

Elm言語: ブラウザベースの純粋関数型言語

Sandi Metz氏が最近関心を持っているとのことなので。

その他

SourceTree Windows版が更新

Diesel: Rust向けORM & クエリビルダ


つっつきボイス: 「社内のRust勢向けにと思って」

Dockerイメージを小さくする3つのコツ

どのネットワークかと思ったら

開発手法

ITエンジニアには筋トレが大事

私も痛感しています。


つっつきボイス: 「筋肉といえば、ウェブ魚拓の人の筋肉の付き方はヤバイですね」「ガチムチやん…」

コアもOSもシェルも自作

Six Degrees of Wikipedia: Wikipediaの記事のつながりをグラフ表示

よくあるやつといえばそれまでですが、めちゃめちゃ速いです。

Hemingway.app: 英文添削できるエディタ

ちなみに世にあるエッセイ向け英文スタイルガイドの種本のほとんどは、The Elements of Styleだったりします。もう100年近いロングセラー。

論文だとChicago Manual of Styleが定番だったかな。

「パスワード無期限」が推奨に

番外

GitHub issueをブログ代わりに

内容よりそっちがびっくりだったので。

prompts: JSらしいプロンプト(GitHub Trendingより)

AppleのクラウドはGoogleのクラウド

参考: Appleのクラウドサービス「iCloud」がGoogleのクラウドを利用していることが正式に明らかに - GIGAZINE

敷き詰め問題

これ系の問題はたいてい難問になってる気がします。


つっつきボイス: 「先に進むとヤケクソで並べたようなヤツとかあるなー確かに」「草」「この直感に反する感がタマラン」

「プログラマーの誓い」


私はプログラマーとして、以下の誓約を果たすことを誓います。
* 仕事を誠実に、かつ倫理から逸れぬよう進めます。人々に害をなすいかなる要求にも従いません。
* 先人の学びを尊重し、かつ自分の学びを後進と共有します。
* プログラミングは技術であると同時に、科学であり、思いやりであり、共感でもあることを決して忘れず、精巧なアルゴリズムや技術上の論争よりも理解することの方が重要であることも決して忘れません。
* 「わかりません」と言うことを恥じず、困ったときは的確に支援を求めます。
* ユーザーのプライバシーを尊重します。プライバシー情報は、たとえ世界中が知っていようとも自分には明かされないものだからです。
* あらゆる物事を生涯にわたって注意深く扱い、自分がミスをする可能性があることを謙虚に認めます。
* コンピュータのためでなく、人間のためにコードを書くことを決して忘れません。
* 自分の書くコードや自分の行いがもたらす可能性のある結果について常に配慮し、社会的な問題と技術上の問題、どちらの困難についても一方を軽んずることなく同様に尊重いたします。
* 誇りを失わず、勤勉に仕事に専念いたします。
* 自分がいつかは間違いを犯し、間違ったまま進む可能性があることを認めます。先入観に囚われず、誰の言葉であっても決して侮ることなく尊重し注意深く耳を傾けます。
同リポジトリより: 大意

ひと目で分かるヒポクラテスの誓いのもじりですね。

蛇腹型ロボット

Snake-inspired robot uses kirigami to move | Harvard John A. Paulson School of Engineering and Applied Sciences

マヤ文字作文


つっつきボイス: 「そういえば最近カクヨムで読んでるこの小説↓、解説も凝ってて言語マニアにはたまらんかもですヨ」


今週は以上です。

バックナンバー(2018年度)

週刊Railsウォッチ(20180223)Ruby25開催、Rails6のパラレルテスト、書籍「RSpecによるRailsテスト入門」更新ほか

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

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

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

Awesome Ruby

Random Ruby

RubyFlow

160928_1638_XvIP4h

Hacklines

Hacklines

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

JavaScript Weekly

javascriptweekly_logo_captured

JavaScript Live

jslive_logo_captured

JSer.info

jser.info_logo_captured

Github Trending

160928_1701_Q9dJIU

Rails5「中級」チュートリアル(3-6)投稿機能: 特定のブランチ(翻訳)

$
0
0

概要

概要

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

Rails5中級チュートリアルはセットアップが短めで、RDBMSにはPostgreSQL、テストにはRSpecを用います。
原文が非常に長いので分割します。章ごとのリンクは順次追加します。

注意: Rails中級チュートリアルは、Ruby on Railsチュートリアル(https://railstutorial.jp/)(Railsチュートリアル)とは著者も対象読者も異なります。

目次

Rails5「中級」チュートリアル(3-6)投稿機能: 特定のブランチ(翻訳)

各投稿は、ある特定のブランチに属します。別ブランチのための特定のページを作成しましょう。

新しいブランチに切り替えます。

git checkout -b specific_branches

homeページのサイドメニュー

homeページのサイドメニューの更新に取りかかりましょう。特定のブランチへのリンクを追加します。index.html.erbファイルを開きます。

views/pages/index.html.erb

#side-menu要素の中にリンクをいくつか追加することにします。ファイルの中身をパーシャルに切り出しておきましょう。今やっておかないと、たちまち乱雑になってしまいます。
#side-menu要素と#main-content要素をカットしてそれぞれ別のパーシャルファイルに貼り付けます。pagesディレクトリの下にindexディレクトリを作成し、要素に対応するパーシャルファイルをそこに作成します。作成後のファイルは以下のようになります(Gist)。

<!-- views/pages/index/_side_menu.html.erb -->
<div id="side-menu"  class="col-sm-3">
</div><!-- side-menu -->
<!-- views/pages/index/_main_content.html.erb -->
<div id="main-content" class="col-sm-9">
  <%= render @posts %>
</div><!-- main-content -->

homeページテンプレート内でこれらのパーシャルファイルをレンダリングします。このファイルは以下のようになります(Gist)。

<!-- views/pages/index.html.erb -->
<%= render 'posts/modal' %>

<div class="container">
  <div class="row">
    <%= render 'pages/index/side_menu' %>
    <%= render 'pages/index/main_content' %>
  </div><!-- row -->
</div><!-- container -->

変更をcommitします。

git add -A
git commit -m "Split home page template's content into partials"

_side_menu.html.erbパーシャルにリンクのリストを追加します。追加後は以下のようになります(Gist)。

<!-- views/pages/index/_side_menu.html.erb -->
<div id="side-menu"  class="col-sm-3">
  <ul id="links-list">
    <%= render 'pages/index/side_menu/no_login_required_links' %>
  </ul>
</div><!-- side-menu -->

これで順序なしリストが追加されます。このリストの中で、リンクを持つ別のパーシャルをレンダリングしましょう。このリンクは、サインインしているかどうかにかかわらずすべてのユーザーに表示されます。このパーシャルファイルを作成してリンクを追加します。

indexディレクトリの下にside_menuディレクトリを作成します。

views/pages/index/side_menu

このディレクトリの下に_no_login_required_links.html.erbパーシャルを作成し、以下のコードを追加します(Gist)。

<!-- views/pages/index/side_menu/_no_login_required_links.html.erb -->
<li id="hobby">
  <%= link_to hobby_posts_path do %>
    <i class="fa fa-user-circle-o" aria-hidden="true"></i> Find a hobby buddy
  <% end %>
</li>

<li id="study">
  <%= link_to study_posts_path do %>
    <i class="fa fa-graduation-cap" aria-hidden="true"></i> Find a study buddy
  <% end %>
</li>

<li id="team">
  <%= link_to team_posts_path do %>
    <i class="fa fa-users" aria-hidden="true"></i> Find a team member
  <% end %>
</li>

ここでは、投稿の特定のブランチへのリンクをいくつか足しているだけです。hobby_posts_pathなどのパスをどこから得たらよいかわからない場合は、routes.rbファイルをご覧ください。さっきcollectionのネストをルーティングのresources:posts宣言の中に書いてあります。

ここでi要素の属性を注意深く見てみると、faクラスがあることに気づくでしょう。このクラスがあることでFont Awesomeのアイコンが宣言されます。Font Awesomeライブラリのセットアップはまだですが、幸いセットアップはとても簡単です。メインのapplication.html.erbファイルのhead要素の中に以下を追加します。

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">

これで以下のようにサイドメニューが表示されるはずです。

変更をcommitします。

git add -A
git commit -m "Add links to the home page's side menu"

小さい画面(幅767px1000px)でのBootstrapコンテナの表示がつぶれすぎていてよろしくないので、この幅の範囲で広げましょう。mobile.scssファイルに以下のコードを追加します(Gist)。

// assets/stylesheets/responsive/mobile.scss
...
@media only screen and (min-width:767px) and (max-width: 1000px) {
  .container {
     width: 100% !important;
  }
}

変更をcommitします。

git add -A
git commit -m "set .container width to 100%
when viewport's width is between 767px and 1000px"

ブランチページ

サイドメニューのリンクのひとつをクリックしてみるとエラーが表示されます。まだPostsControllerにアクションがなく、このコントローラに対応するテンプレートもありません。

PostControllerhobbystudyteamアクションを定義します(Gist)。

# controllers/posts_controller.rb
...
  def hobby
    posts_for_branch(params[:action])
  end

  def study
    posts_for_branch(params[:action])
  end

  def team
    posts_for_branch(params[:action])
  end
...

どのアクションもposts_for_branchを呼び出しています。このメソッドはアクション名に応じて特定ページのデータを返します。このメソッドをprivateスコープで定義しましょう(Gist)。

# contorllers/posts_controller.rb
...
private

def posts_for_branch(branch)
  @categories = Category.where(branch: branch)
  @posts = get_posts.paginate(page: params[:page])
end
...

@categoriesインスタンス変数は、特定のブランチから取り出したすべてのカテゴリです。たとえば、hobbyブランチページを開いたとすると、hobbyブランチに属するすべてのカテゴリが取り出されます。

投稿を取得して@postsインスタンス変数に保存するのにget_postsを使っており、その後ろにpagenateメソッドがチェインされています。paginateメソッドはwill_paginate gemによって提供されます。まずget_postsメソッドを定義しましょう。PostsControllerprivateスコープに以下を追加します(Gist)。

# controllers/posts_controller.rb
...
def get_posts
  Post.limit(30)
end
...

現時点のget_postsメソッドは投稿をきっかり30件取り出しますが、投稿の種類が絞り込まれていません。ここはもう少し改良できそうなので、後でこのメソッドに戻ることにします。

will_pagenate gemを追加してページネーションを利用できるようにします。

gem 'will_paginate', '~> 3.1.0'

以下を実行します。

bundle install

後足りないのはテンプレートだけです。テンプレートはどのブランチでも似たようなものなので、同じコードを何度も書くのではなく、すべてのブランチで共通する一般的な構造を備えたパーシャルを作成しましょう。postsディレクトリの下に_branch.html.erbファイルを作成します(Gist)。

<!-- posts/_branch.html.erb -->
<div id="branch-main-content" class="container">
  <div class="row">
    <h1 class="page-title"><%= page_title %></h1>
    <%= render 'posts/branch/create_new_post', branch: branch %>
  </div><!-- row -->

  <div class="row">
    <%= render 'posts/branch/categories', branch: branch %>
  </div>

  <div class="row">
    <div class="col-sm-12" id="feed">
      <%= render @posts %>
      <%= render no_posts_partial_path %>
    </div>
  </div><!-- row -->

  <div class="infinite-scroll">
    <%= will_paginate @posts %>
  </div>
</div><!-- container -->

ページの冒頭でpage_title変数が出力されていることがわかります。_branch.html.erbパーシャルをレンダリングするときにこの変数を引数として渡します。次に、リンクを表示する_create_new_postが出力されます。ユーザーはこのリンク先で新しい投稿を作成できます。branchディレクトリの下にこのパーシャルファイルを作成しましょう(Gist)。

<!-- posts/branch/_create_new_post.html.erb -->
<div class="col-sm-12">
  <div class="col-sm-8 col-sm-offset-2">
    <%= render create_new_post_partial_path, branch: branch %>
  </div><!-- col-sm-8 -->
</div><!-- col-sm-12 -->

レンダリングするパーシャルファイルの決定にはcreate_new_post_partial_pathヘルパーメソッドを使うことにします。posts_helper.rbファイルに以下のメソッドを実装します(Gist)。

# helpers/posts_helper.rb
...
  def create_new_post_partial_path
    if user_signed_in?
      'posts/branch/create_new_post/signed_in'
    else
      'posts/branch/create_new_post/not_signed_in'
    end
  end
...

create_new_postディレクトリを新しく作り、対応するパーシャルを2つその下に作成します(GistGist)。

<!-- posts/branch/create_new_post/_signed_in.html.erb -->
<div class="new-post-button-parent">
  <span>Cannot find anyone? Try to: </span>
  <%= link_to "Create a new post",
              new_post_path(branch: branch),
              :class => "new-post-button" %>
</div>
<!-- posts/branch/create_new_post/_not_signed_in.html.erb -->
<div class="text-center login-branch">
  To create a new post you have to
  <%= link_to 'Login',
              login_path,
              class: 'login-button login-button-branch' %>
</div>

次に_branch.html.erbファイルでカテゴリのリストを表示します。_categories.html.erbパーシャルファイルを作成します(Gist)。

<!-- posts/branch/_categories.html.erb -->
<% branch_path_name = "#{params[:action]}_posts_path" %>

<div class="col-sm-12">
  <ul class="categories-list">
    <%= render all_categories_button_partial_path,
               branch_path_name: branch_path_name %>
    <% @categories.each do |category| %>
      <li class="category-item">
        <%= link_to category.name,
                    send(branch_path_name, category: category.name),
                    :class => ("selected-item" if params[:category] == category.name) %>
      </li>
    <% end %>
  </ul>
</div><!-- col-sm-12 -->

このファイルでは、レンダリングするファイルの決定にall_categories_button_partial_pathヘルパーメソッドを使っています。このメソッドをposts_helper.rbファイルで定義しましょう(Gist)。

# helpers/posts_helper.rb
 ...
   def all_categories_button_partial_path
    if params[:category].blank?
      'posts/branch/categories/all_selected'
    else
      'posts/branch/categories/all_not_selected'
    end
  end
  ...

デフォルトではすべてのカテゴリが選択されます。params[:category]が空の場合は、ユーザーが選んだカテゴリが何もないことを表し、すなわちデフォルト値のallが選択されます。対応するパーシャルファイルを作成しましょう(GistGist)。

<!-- posts/branch/categories/_all_selected.html.erb -->
<li class="category-item">
  <%= link_to "All",
              send(branch_path_name),
              :class => "selected-item"  %>
</li>
<!-- posts/branch/categories/_all_not_selected.html.erb -->
<li class="category-item">
  <%= link_to "All", send(branch_path_name) %>
</li>

このsendメソッドは、文字列で表されるメソッドを呼び出すのに使われています。この方法によって柔軟性が高まり、メソッド呼び出しが動的になります。ここでは、現在のコントローラアクションに応じて異なるパスを生成しています。

次に、_branch.html.erbの内部で投稿をレンダリングしてno_posts_partial_pathヘルパーメソッドを呼び出します。投稿が見つからない場合はメソッドがメッセージを表示します。

posts_helper.rbに以下のヘルパーメソッドを追加します(Gist)。

# helpers/posts_helper.rb
...
def no_posts_partial_path
  @posts.empty? ? 'posts/branch/no_posts' : 'shared/empty_partial'
end
...

ここでは三項演算子を用いてコードを少しすっきりさせています。私は投稿が何もない場合にはメッセージを表示したくないと考えています。renderメソッドには空文字列を渡せないので、代わりに空のパーシャルへのパスを渡しています。空のパーシャルは何も表示したくないときに使います。

ビューにsharedディレクトリを作成して空のパーシャルを作成します。

views/shared/_empty_partial.html.erb

続いて、branchディレクトリの下にメッセージ表示用の_no_posts.html.erbパーシャルを作成します。(Gist)。

<!-- posts/branch/_no_posts.html.erb -->
<div class="text-center">Currently there are no published posts</div>

最後に、投稿数が多い場合にはgemのwill_paginateメソッドを用いて投稿を複数ページに分割します。

hobby/study/teamアクションに対応するテンプレートをそれぞれを作成します。それらのテンプレートで_branch.html.erbパーシャルファイルをレンダリングして特定のローカル変数を渡します(GistGistGist)。

<!-- posts/hobby.html.erb -->
<%= render 'posts/modal' %>
<%= render partial: 'posts/branch', locals: {
    branch: 'hobby',
    page_title: 'Find a person with the same hobby',
    search_placeholder: 'E.g. guitar playing, programming, cooking'
  } %>
<!-- posts/study.html.erb -->
<%= render 'posts/modal' %>
<%= render partial: 'posts/branch', locals: {
    branch: 'study',
    page_title: 'Find a person who studies the same field as you',
    search_placeholder: 'E.g. nutrition, calculus, astrophysics'
  } %>
<!-- posts/team.html.erb -->
<%= render 'posts/modal' %>
<%= render partial: 'posts/branch', locals: {
    branch: 'team',
    page_title: 'Find a person with similar interests as yours to your team',
    search_placeholder: 'E.g. musician for a band, developer for a project'
  } %>

これでブランチページのいずれかを表示すると、以下のように表示されます。

ページを下までスクロールすると、ページネーションもできるようになっています。

ブランチページの作成作業がだいぶ増えてきたので、ここで変更をcommitしましょう。

git add -A
git commit -m "Create branch pages for specific posts

- Inside the PostsController define hobby, study and team actions.
  Define a posts_for_branch method and call it inside these actions
- Add will_paginate gem
- Create a _branch.html.erb partial file
- Create a _create_new_post.html.erb partial file
- Define a create_new_post_partial_path helper method
- Create a _signed_in.html.erb partial file
- Create a _not_signed_in.html.erb partial file
- Create a _categories.html.erb partial file
- Define a all_categories_button_partial_path helper method
- Create a _all_selected.html.erb partial file
- Create a _all_not_selected.html.erb partial file
- Define a no_posts_partial_path helper method
- Create a _no_posts.html.erb partial file
- Create a hobby.html.erb template file
- Create a study.html.erb template file
- Create a team.html.erb template file"

spec

ヘルパーメソッドをspecでカバーしましょう。posts_helper_spec.rbファイルは次のような感じになります(Gist)。

# spec/helpers/posts_helper_spec.rb
require 'rails_helper'

RSpec.describe PostsHelper, :type => :helper do

  context '#create_new_post_partial_path' do
    it "returns a signed_in partial's path" do
      helper.stub(:user_signed_in?).and_return(true)
      expect(helper.create_new_post_partial_path). to (
        eq 'posts/branch/create_new_post/signed_in'
      )
    end

    it "returns a signed_in partial's path" do
      helper.stub(:user_signed_in?).and_return(false)
      expect(helper.create_new_post_partial_path). to (
        eq 'posts/branch/create_new_post/not_signed_in'
      )
    end
  end

  context '#all_categories_button_partial_path' do
    it "returns an all_selected partial's path" do
      controller.params[:category] = ''
      expect(helper.all_categories_button_partial_path).to (
        eq 'posts/branch/categories/all_selected'
      )
    end

    it "returns an all_not_selected partial's path" do
      controller.params[:category] = 'category'
      expect(helper.all_categories_button_partial_path).to (
        eq 'posts/branch/categories/all_not_selected'
      )
    end
  end

  context '#no_posts_partial_path' do
    it "returns a no_posts partial's path" do
      assign(:posts, [])
      expect(helper.no_posts_partial_path).to (
        eq 'posts/branch/no_posts'
      )
    end

    it "returns an empty partial's path" do
      assign(:posts, [1])
      expect(helper.no_posts_partial_path).to (
        eq 'shared/empty_partial'
      )
    end
  end
end

このspecもかなりシンプルです。ここではstubメソッドを用いてメソッドの戻り値を定義しました。paramsの定義は、controller.params[:param_name]のようにコントローラを選択してシンプルに行っています。最後に、インスタンス変数の代入にはassignメソッドを使っています。

変更をcommitします。

git add -A
git commit -m "Add specs for PostsHelper methods"

画面デザインの変更

ブランチページに表示する投稿のデザインを変えてみたいと思います。homeページではカード形式のデザインを使っています。ブランチページでリスト形式のデザインを作成し、ユーザーが多数の投稿を効率よく閲覧できるようにしてみましょう。

postsディレクトリの下にpostディレクトリを作成し、そこに_home_page.html.erbパーシャルファイルを作成します。

posts/post/_home_page.html.erb

_post.html.erbパーシャルの内容をカットし、この_home_page.html.erbファイルに貼り付けます。_post.html.erbパーシャルファイルには以下のコードを追加します(Gist)。

<!-- posts/_post.html.erb -->
<%= render post_format_partial_path, post: post %>

ここで呼んでいるpost_format_partial_pathヘルパーメソッドは、現在のパスに応じて、投稿をどのデザインでレンダリングするかを選択します。ユーザーがhomeページにいる場合はhomeページ向けのデザインでレンダリングし、ブランチページにいる場合はブランチページ向けのデザインでレンダリングします。_post.html.erbファイルの内容を_home_page.html.erbに移動したのはそのためです。

postディレクトリに_branch_page.html.erbファイルを作成し、ブランチページ向けの画面デザインを定義する以下のコードを貼り付けます(Gist)。

<!-- posts/post/_branch_page.html.erb -->
<div class="single-post-list" id=<%= post_path(post.id) %>>
  <%= truncate(post.title, :length => 60) %>
  <div class="post-content">
    <div class="posted-by">Posted by <%= post.user.name %></div>
    <h3><%= post.title %></h3>
    <p><%= post.content %></p>
    <%= link_to "I'm interested", post_path(post.id), class: 'interested' %>
  </div>
</div>

レンダリングするパーシャルファイルを決定するpost_format_partial_pathヘルパーメソッドをposts_helper.rbで定義します(Gist)。

# helpers/posts_helper.rb
def post_format_partial_path
  current_page?(root_path) ? 'posts/post/home_page' : 'posts/post/branch_page'
end

投稿のレンダリングはhomeページのテンプレート内で行われるため、担当するコントローラが異なります。このため、このままではpost_format_partial_pathヘルパーメソッドはhomeページで呼び出せません。このメソッドをhomeページのテンプレート内で使えるようにするには、ApplicationHelper(helper/application_helper.rb)の内側にPostsHelperをインクルードします。

include PostsHelper

spec

post_format_partial_pathヘルパーメソッドのspecを追加します(Gist)。

# helpers/posts_helper_spec.rb
context '#post_format_partial_path' do
  it "returns a home_page partial's path" do
    helper.stub(:current_page?).and_return(true)
    expect(helper.post_format_partial_path).to (
      eq 'posts/post/home_page'
    )
  end

  it "returns a branch_page partial's path" do
    helper.stub(:current_page?).and_return(false)
    expect(helper.post_format_partial_path).to (
      eq 'posts/post/branch_page'
    )
  end
end

変更をcommitします。

git add -A
git commit -m "Add specs for the post_format_partial_path helper method"

CSS

ブランチページの投稿スタイルをCSSで記述しましょう。CSSのpostsディレクトリの下に以下の内容でbranch_page.scssファイルを作成します(Gist)。

// stylesheets/partials/posts/branch_page.scss
.single-post-list {
  min-height: 45px;
  max-height: 45px;
  padding: 10px 20px 10px 0px;
  margin: 0 10px;
  border-bottom: solid 3px rgba(0, 0 , 0, 0.05);
  border-bottom-right-radius: 10%;
  transition: border-color 0.1s;
  overflow: hidden;
  &:hover {
    cursor: pointer;
  }
}

.page-title {
  margin: 30px 0;
  text-align: center;
  background-color: white !important;
  font-weight: bold;
  a {
    color: black;
  }
  a:hover {
    text-decoration: underline;
  }
}

.categories-list {
  margin: 10px 0;
  padding: 0;
}

.category-item {
  display: inline-block;
  margin: 15px 0;
  a {
    font-size: 16px;
    font-size: 1.6rem;
    color: rgba(0,0,0,0.7);
    border: solid 2px rgba(0,0,0,0.4);
    border-radius: 8%;
    padding: 10px;
  }
  a:hover, .selected-item {
    background: $navbarColor;
    color: white;
    border: solid 2px white;
    border-radius: 0px;
  }
}

.new-post-button-parent {
  text-align: right;
  span {
    font-size: 12px;
    font-size: 1.2rem;
  }
}

.new-post-button {
  display: inline-block;
  background: $navbarColor;
  color: white;
  padding: 8px;
  border-radius: 10px;
  font-weight: bold;
  border: solid 2px $navbarColor;
  margin: 10px 0;
  &:hover, &:active, &:focus {
    background: white;
    color: black;
  }
}

.login-branch {
  margin: 10px 0;
}

.login-button-branch {
  padding: 5px 10px;
  border-radius: 10px;
  &:hover, &:active, &:visited, &:link {
    color: white;
  }
}

#branch-main-content {
  background: white;
  height: calc(100vh - 50px);
}

#feed {
  background-color: white;
}

base/default.scssに以下を追加します(Gist)。

// assets/stylesheets/base/default.scss
.login-button, .sign-up-button {
  background-color: $navbarColor;
  color: white !important;
}

小画面デバイスでの表示を修正するため、responsive/mobile.scssに以下を追加します(Gist)。

// assets/stylesheets/responsive/mobile.scss
...
@media screen and (max-width: 550px) {
  .page-title {
      font-size: 20px;
      font-size: 2rem;
  }
  .new-post-button-parent {
    text-align: center;
    span {
      display: none !important;
    }
  }
  .post-button {
    padding: 5px;
  }
  .category-item {
    a {
      padding: 5px;
    }
  }
}

@media screen and (max-width: 767px) {
  .single-post-list {
    min-height: 65px;
    max-height: 65px;
    padding: 10px 0;
  }
}
...

訳注: application.scssに以下を追加する必要もあります。

// assets/stylesheets/application.scss
@import "partials/posts/*";

これでブランチページは次のように表示されるはずです。

変更をcommitします。

git add -A
git commit -m "Describe the posts style in branch pages

- Create a branch_page.scss file and add CSS
- Add CSS to the default.scss file
- Add CSS to the mobile.scss file"

検索バー

投稿リストを閲覧できるだけではなく、特定の投稿を検索できるようにもしたいと思います。_branch.html.erbパーシャルファイルのcategories rowの直前に以下を追加します(Gist)。

<!-- posts/_branch.html.erb -->
...
<div class="row">
  <%= render  'posts/branch/search_form',
              branch: branch,
              search_placeholder: search_placeholder %>
</div><!-- row -->
...

branchディレクトリの下に_search_form.html.erbパーシャルファイルを作成し、以下のコードを追加します(Gist)。

<!-- posts/branch/_search_form.html.erb -->
<div class="col-sm-12">
  <%= form_tag(send("#{branch}_posts_path"),
               :method => "get",
               id: "search-form") do %>
    <i class="fa fa-search" aria-hidden="true"></i>
    <%= text_field_tag  :search,
                        params[:search],
                        placeholder: search_placeholder,
                        class: "form-control" %>
    <%= render category_field_partial_path %>
  <% end %>
</div><!-- col-sm-12 -->

上のコードでは、sendメソッドを使ってPostsControllerの特定のアクションへのパスを現在のブランチに応じて動的に生成しています。また、特定のカテゴリが選択されている場合にはカテゴリ用のデータフィールドも送信します。ユーザーがカテゴリのひとつを選択すると、そのカテゴリに該当する結果だけを返します。

posts_helper.rbファイルにcategory_field_partial_pathヘルパーメソッドを定義します(Gist)。

# helpers/posts_helper.rb
...
  def category_field_partial_path
    if params[:category].present?
      'posts/branch/search_form/category_field'
    else
      'shared/empty_partial'
    end
  end
...

search_formディレクトリを作成し、その下に_category_field.html.erbパーシャルファイルを作成して以下のコードを追加します(Gist)。

<!-- posts/branch/search_form/_category_field.html.erb -->
<%= hidden_field_tag :category, params[:category] %>

検索フォームのスタイルを整えるため、branch_page.scssファイルに以下を追加します(Gist)。

// assets/stylesheets/partials/posts/branch_page.scss
.fa-search {
  position:absolute;
  bottom:14px;
  left:10px;
  width:20px;
  height:10px;
}

#search-form {
  position:relative;
  input {
    border: solid 2px rgba(0,0,0,0.2);
    border-radius: 10px;
    box-shadow: none;
    outline: 0;
  }
  input:focus {
    border: solid 2px rgba(0,0,0,0.35);
  }
  input#search {
    padding: 15px;
    width: 100%;
    height:20px;
    margin: 10px 0;
    padding-left: 30px;
  }
}

これで、ブランチページの検索フォームが以下のように表示されるはずです。

変更をcommitします。

git add -A
git commit -m "Add a search form in branch pages

- Render a search form inside the _branch.html.erb
- Create a _search_form.html.erb partial file
- Define a category_field_partial_path helper method in PostsHelper
- Create a `_category_field.html.erb` partial file
- Add CSS for the the search form in branch_page.scss"

このフォームはまだ機能していません。検索機能を使える何らかのgemを追加してもよいのですが、まだデータは複雑ではないので、簡単な検索エンジンを独自に作成することもできます。ここではPostモデルでスコープを用いてクエリをチェインできるようにし、コントローラに条件ロジックを追加します(次のセクションではこのコードをService Objectに切り出してコードをすっきりさせる予定です)。

まずはPostモデルでスコープを定義しましょう。手始めに、post.rbファイルでdefault_scopeを定義します。このスコープでは投稿を作成日で降順ソートし、最新の投稿がトップに来るようにします(Gist)。

# models/post.rb
...
default_scope -> { includes(:user).order(created_at: :desc) }
...

訳注: default_scopeについては次の記事もどうぞ。

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

変更をcommitします。

git add -A
git commit -m "Define a default_scope for posts"

default_scopeが正常に機能していることを確認するため、specに含めましょう。post_spec.rbファイルに以下を追加します(Gist)。

# spec/models/post_spec.rb
context 'Scopes' do
  it 'default_scope orders by descending created_at' do
    first_post = create(:post)
    second_post = create(:post)
    expect(Post.all).to eq [second_post, first_post]
  end
end

変更をcommitします。

git add -A
git commit -m "Add a spec for the Post model's default_scope"

それでは検索バーが機能するようにしてみましょう。posts_controller.rbget_postsメソッドの内容を以下で置き換えます(Gist)。

# controllers/posts_controller.rb
def get_posts
  branch = params[:action]
  search = params[:search]
  category = params[:category]

  if category.blank? && search.blank?
    posts = Post.by_branch(branch).all
  elsif category.blank? && search.present?
    posts = Post.by_branch(branch).search(search)
  elsif category.present? && search.blank?
    posts = Post.by_category(branch, category)
  elsif category.present? && search.present?
    posts = Post.by_category(branch, category).search(search)
  else
  end
end

ビューのときと同様、コントローラにこういうロジックを置くのはあまりよくありません。ここをもっとスッキリさせたいので、この後のセクションでこのメソッドのロジックを切り出す予定です。

このコードでは条件ロジックがいくつか連続しています。ユーザーからのリクエストに応じて、データをクエリするときのスコープを切り替えています。

Postモデルに以下のスコープを定義します(Gist)。

# models/post.rb
...
  scope :by_category, -> (branch, category_name) do
    joins(:category).where(categories: {name: category_name, branch: branch})
  end

  scope :by_branch, -> (branch) do
    joins(:category).where(categories: {branch: branch})
  end

  scope :search, -> (search) do
    where("title ILIKE lower(?) OR content ILIKE lower(?)", "%#{search}%", "%#{search}%")
  end
...

関連付けられたテーブルのレコードへのクエリには[joins]https://railsguides.jp/active_record_querying.html#joins)メソッドを使います。また、与えられた文字列を元に基本的なSQL文法を用いてレコードを検索しています。

これでサーバーを再起動していずれかのブランチページを表示すれば、検索バーが使えるようになっているはずです。また、カテゴリボタンをクリックしてカテゴリでフィルタすることも、特定のカテゴリを選択している状態で検索することでそのカテゴリに属する投稿だけを検索することもできます。

変更をcommitします。

git add -A
git commit -m "Make search bar and category filters
in branch pages functional

- Add by_category, by_branch and search scopes in the Post model
- Modify the get_posts method in PostsController"

これらのスコープをspecでカバーしましょう。post_spec.rbファイルの Scopes contextに以下を追加します(Gist)。

# spec/models/post_spec.rb
  it 'by_category scope gets posts by particular category' do
    category = create(:category)
    create(:post, category_id: category.id)
    create_list(:post, 10)
    posts = Post.by_category(category.branch, category.name)
    expect(posts.count).to eq 1
    expect(posts[0].category.name).to eq category.name
  end

  it 'by_branch scope gets posts by particular branch' do
    category = create(:category)
    create(:post, category_id: category.id)
    create_list(:post, 10)
    posts = Post.by_branch(category.branch)
    expect(posts.count).to eq 1
    expect(posts[0].category.branch).to eq category.branch
  end

  it 'search finds a matching post' do
    post = create(:post, title: 'awesome title', content: 'great content ' * 5)
    create_list(:post, 10, title: ('a'..'c' * 2).to_a.shuffle.join)
    expect(Post.search('awesome').count).to eq 1
    expect(Post.search('awesome')[0].id).to eq post.id
    expect(Post.search('great').count).to eq 1
    expect(Post.search('great')[0].id).to eq post.id
  end

変更をcommitします。

git add -A
git commit -m "Add specs for Post model's
by_branch, by_category and search scopes"

無限スクロール機能

ブランチページのいずれかを表示すると、最下部に以下のページネーションが表示されています。

[Next]のリンクをクリックすると、現在より古い記事のページにリダイレクトされます。こうする代わりに、FacebookやTwitterのフィードのように無限スクロールさせることもできます。この場合下にスクロールするだけで、ページの再読み込みやリダイレクトを行わなくても以前の投稿がリストの下に追加されます。驚くことに、この機能はJavaScriptを少し書くだけでとても簡単に実現できるのです。ユーザーがページ最下部までスクロールすると、「次のページ」からデータを取得するAJAX リクエストが常に送信され、リストの最下部に追加されます。

まずはAJAXリクエストとその条件を設定するところから始めましょう。ユーザーがある閾値まで下スクロールすると、AJAXリクエストが発火するようにします。javascripts/postsディレクトリの下にinfinite_scroll.jsファイルを作成し、以下のコードを追加します(Gist)。

// assets/javascripts/posts/infinite_scroll.js
$(document).on('turbolinks:load', function() {
  var isLoading = false;
  if ($('.infinite-scroll', this).length > 0) {
    $(window).on('scroll', function() {
      var more_posts_url = $('.pagination a.next_page').attr('href');
      var threshold_passed = $(window).scrollTop() > $(document).height() - $(window).height() - 60;
      if (!isLoading && more_posts_url && threshold_passed) {
        isLoading = true;
        $.getScript(more_posts_url).done(function (data,textStatus,jqxhr) {
          isLoading = false;
        }).fail(function() {
          isLoading = false;
        });
      }
    });
  }
});

訳注: 原文コードままだと動かなかったため、上のJavaScriptコードは修正してあります(参考)。

isLoadingは、一度に1件のリクエストだけが送信されるようにするための変数です。リクエストが進行中の場合、他のリクエストは開始されません。

最初にページネーション機能の有無と、表示する投稿が他にもあるかどうかをチェックします。次に、次ページへのリンク(ここからデータを取り出します)を取得します。続いてAJAXリクエストを呼び出すときの閾値(threshold)を設定します。ここではウィンドウ最下部から60pxまでを閾値に設定しています。すべての条件がパスしたら、getScript()関数で次のページからデータを読み込みます。

getScript()関数はJavaScriptファイルを読み込むので、どのファイルでレンダリングするかをPostsControllerで指定しなければなりません。レンダリングするファイルは、posts_for_branchメソッドの中でrespond_toの形で指定します(Gist)。

# controllers/posts_controller.rb
respond_to do |format|
  format.html
  format.js { render partial: 'posts/posts_pagination_page' }
end

このコントローラが.jsファイルを用いて応答しようとすると、posts_pagination_pageテンプレートがレンダリングされます。このパーシャルファイルは、新たに取り出した投稿をリストの末尾に追加します。投稿をappendしてページネーション要素を更新するパーシャルファイルを作成しましょう(Gist)。

<!-- posts/_posts_pagination_page.js.erb -->
$('#feed').append('<%= j render @posts %>');
<%= render update_pagination_partial_path %>

posts_helper.rbファイルにupdate_pagination_partial_pathヘルパーメソッドを追加します(Gist)。

# helpers/posts_helper.rb
def update_pagination_partial_path
  if @posts.next_page
    'posts/posts_pagination_page/update_pagination'
  else
    'posts/posts_pagination_page/remove_pagination'
  end
end

ここではwill_paginate gemのnext_pageメソッドを用いて、この後読み込める投稿がまだあるかどうかを決定しています。

対応するパーシャルファイルをそれぞれ作成します(GistGist)。

<!-- posts/posts_pagination_page/_update_pagination.js.erb -->
$('.pagination').replaceWith('<%= j will_paginate @posts %>');
<!-- posts/posts_pagination_page/_remove_pagination.js.erb -->
$(window).off('scroll');
$('.pagination').remove();

これで、いずれかのブランチページで下にスクロールすれば過去の投稿が自動的にリストの下に追加されるはずです。

ページネーションのメニューを表示する必要もなくなったので、CSSで隠しましょう。branch_page.scssファイルに以下を追加します。

# stylesheets/partials/posts/branch_page.scss
...
.infinite-scroll {
  display: none;
}
...

変更をcommitします。

git add -A
git commit -m "Transform posts pagination into infinite scroll

- Create an infinite_scroll.js file
- Inside PostController's posts_for_branch method add respond_to format
- Define an update_pagination_partial_path
- Create _update_pagination.js.erb and _remove_pagination.js.erb partials
- hide the .infinite-scroll element with CSS"

spec

update_pagination_partial_pathヘルパーメソッドをspecでカバーしましょう(Gist)。

# spec/helpers/post_helper_spec.rb
context '#update_pagination_partial_path' do
  it "returns an update_pagination partial's path" do
    posts = double('posts', :next_page => 2)
    assign(:posts, posts)
    expect(helper.update_pagination_partial_path).to(
      eq 'posts/posts_pagination_page/update_pagination'
    )
  end

  it "returns a remove_pagination partial's path" do
    posts = double('posts', :next_page => nil)
    assign(:posts, posts)
    expect(helper.update_pagination_partial_path).to(
      eq 'posts/posts_pagination_page/remove_pagination'
    )
  end
end

ここでは、postsインスタンス変数とそこにチェインされるnext_pageメソッドをdoubleを用いてシミュレートしています。RSpecのモックについて詳しくはこちらをご覧ください。

変更をcommitします。

git add -A
git commit -m "Add specs for the update_pagination_partial_path
helper method"

この時点で、下スクロールすると投稿が下に追加されることを確認できるfeature specを書くこともできます。infinite_scroll_spec.rbファイルを作成します(Gist)。

# spec/features/posts/infinite_scroll_spec.rb
require "rails_helper"

RSpec.feature "Infinite scroll", :type => :feature do
  Post.per_page = 15

  let(:check_posts_count) do
    expect(page).to have_selector('.single-post-list', count: 15)
    page.execute_script("$(window).scrollTop($(document).height())")
    expect(page).to have_selector('.single-post-list', count: 30)
  end

  scenario "User scrolls down the hobby page
            and posts list will be appended with older posts", js: true do
    create_list(:post, 30, category: create(:category, branch: 'hobby'))
    visit hobby_posts_path
    check_posts_count
  end

  scenario "User scrolls down the study page
            and posts list will be appended with older posts", js: true do
    create_list(:post, 30, category: create(:category, branch: 'study'))
    visit study_posts_path
    check_posts_count
  end

  scenario "User scrolls down the team page
            and posts list will be appended with older posts", js: true do
    create_list(:post, 30, category: create(:category, branch: 'team'))
    visit team_posts_path
    check_posts_count
  end

end

上のspecファイルではブランチページをすべてカバーしており、3つのページでこの機能が正常に動作することを確認しています。per_pagewill_paginate gemのメソッドです。ここではPostモデルを選択してページのデフォルト投稿数を設定するのに使っています。

このファイルのコード量を減らすためにcheck_posts_countメソッドを定義しています。同じコードを異なるspecで繰り返すのではなく、単一のメソッドに切り出しています。ページを開いたときに投稿が15件表示されることが期待されています。続いてexecute_scriptメソッドを用いてJavaScriptを実行し、ブラウザのスクロールバーを最下部までスクロールしています。スクロールが終わったら、最後に投稿が15件追加されることが期待されています。これで、ページには投稿が30件表示されます。

変更をcommitします。

git add -A
git commit -m "Add feature specs for posts' infinite scroll functionality"

homeページの更新

現在のhomeページには投稿が数件ランダムに表示されているだけです。これを改修して、すべてのブランチから投稿を数件表示できるようにしましょう。

_main_content.html.erbファイルの内容を以下で置き換えます(Gist)。

<!-- pages/index/_main_content.html.erb -->
<div id="main-content" class="col-sm-9">
  <h3 class="page-name"><%= link_to 'Hobby', hobby_posts_path %></h3>
  <div class="row">
    <%= render @hobby_posts %>
    <%= render no_posts_partial_path(@hobby_posts) %>
  </div><!-- row -->

  <h3 class="page-name"><%= link_to 'Study', study_posts_path %></h3>
  <div class="row">
    <%= render @study_posts %>
    <%= render no_posts_partial_path(@study_posts) %>
  </div><!-- row -->

  <h3 class="page-name"><%= link_to 'Team member', team_posts_path %></h3>
  <div class="row">
    <%= render @team_posts %>
    <%= render no_posts_partial_path(@team_posts) %>
  </div><!-- row -->
</div><!-- main_content -->

ブランチごとに投稿を区切るセクションを作成しました。

PagesControllerindexアクションにインスタンス変数をいくつか定義しましょう。定義後のアクションは次のようになります(Gist)。

# controllers/pages_controller.rb
  def index
    @hobby_posts = Post.by_branch('hobby').limit(8)
    @study_posts = Post.by_branch('study').limit(8)
    @team_posts = Post.by_branch('team').limit(8)
  end

先ほどno_posts_partial_pathヘルパーメソッドを作成しましたが、再利用しやすいように少々変更する必要があります(現在はブランチページでしか使えません)。このメソッドにpostsパラメータを追加すると次のようになります(Gist)。

# helpers/posts_helper.rb
def no_posts_partial_path(posts)
  posts.empty? ? 'posts/shared/no_posts' : 'shared/empty_partial'
end

postsパラメータを追加したことで、インスタンス変数は単純な変数に置き換えられ、パーシャルのパスも変わりました。そこで_no_posts.html.erbパーシャルファイルのパスも以下のように変更します。

posts/branch/_no_posts.html.erb

上のパスを以下に変更します。

posts/shared/_no_posts.html.erb

また、posts/_branch.html.erbファイルのno_posts_partial_pathメソッドを、@postsインスタンス変数を引数として渡すように変更します。

スタイルも少し追加しましょう。default.scssファイルに以下を追加します(Gist)。

// assets/stylesheets/base/default.scss
...
.container {
  padding: 0;
}

.row {
  margin: 0;
}

home_page.scssに以下を追加します(Gist)。

// assets/stylesheets/partials/home_page.scss
.page-name {
  margin: 15px 0px 15px 0px;
  text-align: center;
  background-color: white !important;
  font-weight: bold;
  a {
    color: black;
  }
  a:hover {
    text-decoration: underline;
  }
}
...

これでhomeページが以下のように表示されるはずです。

訳注: specの更新が原文で漏れていたので、以下に補います。

# /spec/helpers/posts_helper_spec.rb
  context '#no_posts_partial_path' do
    it "returns a no_posts partial's path" do
      expect(helper.no_posts_partial_path([])).to (
        eq 'posts/shared/no_posts'
      )
    end

    it "returns an empty partial's path" do
      expect(helper.no_posts_partial_path([1])).to (
        eq 'shared/empty_partial'
      )
    end
  end

変更をcommitします。

git add -A
git commit -m "Add posts from all branches in the home page

- Modify the `_main_content.html.erb file
- Define instance variables inside the PagesControllers index action
- Modify the `no_posts_partial_path helper method to be more reusable
- Add CSS to style the home page"

関連記事

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

Rails tips: 定数をfreezeして高速化するリファクタリング(翻訳)

$
0
0

概要

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

Rails tips: 定数をfreezeして高速化するリファクタリング(翻訳)

国コードを1つ受け取って国名を返す、非常にシンプルなRubyメソッドで考えてみましょう。多くの人、特にRubyを始めて間もない方は以下のようなコードを書くでしょう。

class SampleClass
  def country_name(country_code:)
    mappings = {
      'us' => 'USA',
      'de' => 'Germany'
    }

    mappings[country_code]
  end
end

対応付けをメソッドの外に移動することで、コードがより読みやすくなります。または別のメソッドに切り出しても構いません。定数化しておくとさらによくなりますが、定数をfreezeしておけばメモリ使用量も少し節約できるので完璧です。

class SampleClass
  COUNTRY_CODES_MAPPINGS = {
    'us' => 'USA',
    'de' => 'Germany'
  }.freeze

  def country_name(country_code:)
    COUNTRY_CODES_MAPPINGS[country_code]
  end
end

定数をfreezeしないと値が変更されてしまう可能性がありますが、freezeすることで変更を防止できます(RuntimeErrorが表示されます)。一般に、freezeすることでメモリアロケーションが削減されるため、メモリ使用量を節約できます。

Ruby 2.2で興味深いのは、ハッシュのキーに使われる文字列リテラルが自動的にfreezeされる点です。

このトリックを教えてくれた同僚のDarekに感謝いたします。


RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

ベテランRubyistがPythonコードを5倍速くした話(翻訳)

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


Railsアンチパターン: Decoratorの肥大化(翻訳)

$
0
0

概要

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

パターン名は英語表記としています。

Railsアンチパターン: Decoratorの肥大化(翻訳)

RailsでDecoratorを用いるとさまざまなメリットが得られます。モデルはスリムになり、ビューもすっきりし、手続き臭のする従来のビューヘルパーは過去のものになります。

RailsプロジェクトにDecoratorパターンを適用するとき、ともするとモデルとDecoratorを1対1で対応させたい誘惑にかられます。たとえばArticleのプレゼンテーションロジックはすべてArticleDecoratorに置く、という具合です。Decoratorが小さいうちなら、これは正当なアプローチでしょう。

しかしこのDecoratorの要件は時とともに注ぎ足されてさらに多くのメソッドが集結し、責務は増加し、いつしかDecoratorはゆっくりと肥大化していきます。ぎっしり詰まったメソッド群の中からいずれバグが顔を出すでしょう。

単一のDecoratorに何もかも詰め込んでいては、Railsのビューヘルパーを使っていたときと大差ありません。Decoratorを最大限に活用したいのであれば、オブジェクト指向プログラミングの原則をDecoratorにも適用することが重要です。

本記事では、これまで私がDecoratorでよく目にするコードの臭いをいくつかご紹介します。

変更の分散(divergent changes)

大規模なDecoratorでは機能が広範囲に渡っていることがよくあります。要件が変更されると、1つのDecoratorがいくつもの異なる要件に合わせようとしてたびたび自らを変更していたりします。これが「変更の分散」と呼ばれるコードの臭いです。

この臭いは、単一責任の原則に違反していることを示しています。プレゼンテーションロジックの面倒を見ることは、ここでは単一責任にはカウントされません;-)。機能の1つの部分について責任を持つメソッド群が互いに強く関連しているのであれば、おそらくそれらを独自のクラスに切り出すべきでしょう。

機能の羨望(feature envy)

不要なロジックをビューから切り出すとき、目についた最寄りのDecoratorに置いて済ませていることがよくあります。これをやってしまうと、Decoratorが他のオブジェクトに越境し始めるようになり、しまいには自分自身ではなく他のオブジェクトのことで手一杯になってしまいます。

これによってオブジェクト間に多数の結合が発生し、リファクタリングが困難になります。この種の機能は、もっと適切なDecoratorに移す必要があります。

プンプン臭うDecorator

以下は、Git commitのプレゼンテーションロジックを扱うDecoratorです。簡単のため、コードのある部分を大胆に省略してあります。私はDraper gemを用いていますが、他のgemや自作のDecoratorを使っていても同じです。

class CommitDecorator < Draper::Decorator
  delegate_all

  def author_link
    h.link_to(author.name, h.profile_path(author.username))
  end

  def parent_link
    h.link_to(parent.truncated_sha, h.project_commit_path(project, parent.sha))
  end

  def diff_stats
    h.t('commits.show.diff_stats_html',
        changed: diffs.count,
        additions: diffs.sum(&:additions),
        deletions: diffs.sum(&:deletions))
  end

  def file_changes
    diffs.map do |diff|
      DiffLine.new(
        status_class_for(diff), diff.path, diff.additions, diff.deletions)
    end
  end

  private

  DiffLine = Struct.new(:status_class, :path, :additions, :deletions)

  def status_class_for(diff)
    if diff.deleted?
      'deletion'
    elsif diff.added?
      'addition'
    else
      'change'
    end
  end
end

commitのコントローラは次のような感じだとします。

class CommitsController < ApplicationController
  decorates_assigned :commits, :commit

  def index
    @commits = find_project.commits
  end

  def show
    @commit = find_project.find_commit_by_sha(params[:sha])
  end

  private

  def find_project
    Project.find(params[:project_id])
  end
end

以下の2つのシナリオが想定されています。

  • プロジェクトのcommitリストが表示される
  • 変更されたファイルのcommitが1件表示される

問題点

このDecoratorには、先にご紹介した2種類の「コードの臭い」が両方とも出現しています。

ちっぽけな情報しか持っていないcommitの概要を表示したいだけであれば、1つのDecoratorに機能を満載するのはいかにもイケてない感じがします。

このDecoratorは他のオブジェクトの細かな部分にかなりちょっかいを出しています。特に、diff内部の詳細と非常に強く結合しています。

ちょっかいを出さないようにする

特にヤバイのは、ファイル変更情報を集めるためにdiffの詳細に立ち入っている点です。取り急ぎこの部分をDiffDecoratorに切り出すリファクタリングを行いましょう。

class DiffDecorator < Draper::Decorator
  delegate_all

  def status_class
    if deleted?
      'deletion'
    elsif added?
      'addition'
    else
      'change'
    end
  end
end

以下のCommitDecoratorがスッキリと変わったことにご注目ください。file_changesはデコレーションされたdiffと同義になりました。どうしても必要というほどではありませんが、ここではalias_methodを用いてインターフェイスを揃えています。

class CommitDecorator < Draper::Decorator
  delegate_all
  decorates_association :diffs

  alias_method :file_changes, :diffs

  def author_link
    h.link_to(author.name, h.profile_path(author.username))
  end

  def parent_link
    h.link_to(parent.truncated_sha, h.project_commit_path(project, parent.sha))
  end

  def diff_stats
    h.t('commits.show.diff_stats_html',
        changed: diffs.count,
        additions: diffs.sum(&:additions),
        deletions: diffs.sum(&:deletions))
  end
end

authorへのリンクと親commitは、このDecorator内でもうまく切り離されました。親自身もcommitオブジェクトなので、CommitDecoratorへのlinkメソッドが暗に追加されます。

class AuthorDecorator < Draper::Decorator
  delegate_all

  def link
    h.link_to(name, h.profile_path(username))
  end
end

class CommitDecorator < Draper::Decorator
  delegate_all
  decorates_association :diffs
  decorates_association :author
  decorates_association :parent

  alias_method :file_changes, :diffs

  delegate :link, to: :author, prefix: true
  delegate :link, to: :parent, prefix: true

  def link
    h.link_to(truncated_sha, h.project_commit_path(project, sha))
  end

  def diff_stats
    h.t('commits.show.diff_stats_html',
        changed: diffs.count,
        additions: diffs.sum(&:additions),
        deletions: diffs.sum(&:deletions))
  end
end

これでCommitDecoratorは、author_linkparent_linkをいくつか委譲するだけとなりました。

「変更の分散」に対処する

私は、あるモデルのデフォルト(または基本となる)Decorator的なものを用意する手法を常に好んでいます。このDecoratorでは、別のコンテキストでよく用いられるメソッドを定義します。これはCommitDecoratorと呼ぶのに相応しいでしょう。commit.decorateが呼び出されれば、デフォルトDecoratorによって常に自動でデコレーションされます。

デコレーションされる機能によっては、デコレーションを必要とするコンテキストが1つしかないこともあります。私は、その種の機能を独自のDecoratorクラスに配置して明示的に利用し、共通機能の中に埋もれないようにしておくのが好みです。

たとえば、commitのサマリーをデフォルトDecoratorでデコレーションできれば十分だとしましょう。しかし詳細なcommitは明らかに独自のデコレータを必要としています。

module Commits
  class DetailedCommitDecorator < Draper::Decorator
    delegate_all

    def initialize(*args)
      super(CommitDecorator.new(*args))
    end

    def diff_stats
      h.t('commits.show.diff_stats_html',
          changed: diffs.count,
          additions: diffs.sum(&:additions),
          deletions: diffs.sum(&:deletions))
    end
  end
end

私はいつも、コンテキストに特有なこの種のDecoratorを、デコレーションするクラスで名前空間化するようにしています。これならDecoratorの数が増えても概要を十分把握できます。

継承でDecoratorを用いる方法は、Decoratorの本質に逆らっているように感じられますし、親クラスのメソッドをオーバーライドするときに混乱しがちなので好きではありません。継承されたメソッドをオーバーライドするのであれば、モデルで定義済みのメソッドをオーバーライドする方法ではない、別のアプローチが必要になるかもしれません。

そういうわけで、私は最初に元のcommitオブジェクトをCommitDecoratorでラップしています。これはCommits::DetailedCommitDecorator.new(CommitDecorator.new(commit))みたいな方法と同等とも言えますが、両方のDecoratorにその都度適用しなければならないことを気にしたくありません。私のDecoratorはどれもdelegate_allを使っていることにご注意ください。このdelegate_allは、Decoratorが知らないメソッドから、デコレーションされるオブジェクトへ委譲するためのものです。

詳細なcommitの機能を切り出したことで、CommitDecoratorがこんなにシンプルになりました。

class CommitDecorator < Draper::Decorator
  delegate_all
  decorates_association :diffs
  decorates_association :author
  decorates_association :parent

  alias_method :file_changes, :diffs

  delegate :link, to: :author, prefix: true
  delegate :link, to: :parent, prefix: true

  def link
    h.link_to(truncated_sha, h.project_commit_path(project, sha))
  end
end

最後の仕上げに、コントローラで適切なDecoratorを適用します。

class CommitsController < ApplicationController
  decorates_assigned :commits
  decorates_assigned :commit, with: Commits::DetailedCommitDecorator

  def index
    @commits = find_project.commits
  end

  def show
    @commit = find_project.find_commit_by_sha(params[:sha])
  end

  private

  def find_project
    Project.find(params[:project_id])
  end
end

まとめ

Decoratorを従来のビューヘルパー代わりに用いることは避けましょう。Decoratorからコードの臭いが立ち昇ったら適切に処理し、メソッドや責務が複雑にならないようにしましょう。Decoratorクラスを新しく導入することを恐れてはいけませんが、Decoratorが存在してもよい正当な理由がある場合にのみ行いましょう。

皆さんのDecoratorからはどんな臭いがしますか?ぜひ原文末尾のコメント欄までお寄せください。

関連記事

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

Rails: テストのリファクタリングでアプリ設計を改良する(翻訳)

[保存版]人間が読んで理解できるデザインパターン解説#2: 構造系(翻訳)

Rails5「中級」チュートリアル(3-7)投稿機能: Service Object(翻訳)

$
0
0

概要

概要

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

Rails5中級チュートリアルはセットアップが短めで、RDBMSにはPostgreSQL、テストにはRSpecを用います。
原文が非常に長いので分割します。章ごとのリンクは順次追加します。

注意: Rails中級チュートリアルは、Ruby on Railsチュートリアル(https://railstutorial.jp/)(Railsチュートリアル)とは著者も対象読者も異なります。

目次

Rails5「中級」チュートリアル(3-7)投稿機能: Service Object(翻訳)

前述のとおり、コントローラ内にロジックを配置するとあっという間に複雑になってしまい、テストが複雑になってしまいます。そういうわけで、こうしたロジックを他の場所に切り出すのはよい考えです。私はそのためにデザインパターンを用いています。具体的にはService Objectと呼ばれるデザインパターンです(単にServiceとも呼ばれます)。

現時点のPostsControllerには以下のメソッドがあります(Gist)。

# controllers/posts_controller.rb
def get_posts
  branch = params[:action]
  search = params[:search]
  category = params[:category]

  if category.blank? && search.blank?
    posts = Post.by_branch(branch).all
  elsif category.blank? && search.present?
    posts = Post.by_branch(branch).search(search)
  elsif category.present? && search.blank?
    posts = Post.by_category(branch, category)
  elsif category.present? && search.present?
    posts = Post.by_category(branch, category).search(search)
  else
  end
end

Serviceを使ってこの大量の条件ロジックを取り除きたいと思います。Service Object(Service)デザインパターンは、単なる基本的なRubyのクラスです。Service Objectは、処理したいデータをこれに渡して、定義済みのメソッドを呼び出し、欲しい戻り値を受け取るという非常にシンプルなものです。

RubyではClassのinitializeメソッドにデータを渡します。これは他の言語で言う「コンストラクタ」に相当します。そしてクラス内で、定義済みのすべてのロジックを扱うメソッドを作成します。実際に作ってコードの様子を見てみましょう。

appディレクトリの下にservicesディレクトリを作成します。

app/services

このディレクトリの下にposts_for_branch_service.rbファイルを以下の内容で作成します(Gist)。

# services/posts_for_branch_service.rb
class PostsForBranchService
  def initialize(params)
    @search = params[:search]
    @category = params[:category]
    @branch = params[:branch]
  end

  # get posts depending on the request
  def call
    if @category.blank? && @search.blank?
      posts = Post.by_branch(@branch).all
    elsif @category.blank? && @search.present?
      posts = Post.by_branch(@branch).search(@search)
    elsif @category.present? && @search.blank?
      posts = Post.by_category(@branch, @category)
    elsif @category.present? && @search.present?
      posts = Post.by_category(@branch, @category).search(@search)
    else
    end
  end

end

前述したように、これはRubyの普通のクラスであり、パラメータを受け取るinitializeメソッドと、ロジックを扱うcallメソッドがあります。このロジックは、get_postsから持ってきたものです。

後は、get_postsメソッド内でこのクラスのオブジェクトを作成し、callメソッドで呼び出します。get_postsメソッドは次のような感じになります(Gist)。

# controllers/posts_controller.rb
  def get_posts
    PostsForBranchService.new({
      search: params[:search],
      category: params[:category],
      branch: params[:action]
    }).call
  end

変更をcommitします。

git add -A
git commit -m "Create a service object to extract logic
from the get_posts method"

spec

Serviceなどのデザインパターンのありがたい点は、単体テストが書きやすいことです。callメソッドのspecを書いて条件ごとにテストすればよいのです。

specディレクトリの下にservicesディレクトリを作成します。

spec/services

そのディレクトリの下に、posts_for_branch_service_spec.rbファイルを以下の内容で作成します(Gist)。

# spec/services/posts_for_branch_service_spec.rb
require 'rails_helper'
require './app/services/posts_for_branch_service.rb'

describe PostsForBranchService do

  context '#call' do
    let(:not_included_posts) { create_list(:post, 2) }
    let(:category) { create(:category, branch: 'hobby', name: 'arts') }
    let(:post) do
      create(:post,
              title: 'a very fun post', 
              category_id: category.id)
    end
    it 'returns posts filtered by a branch' do
      not_included_posts
      category
      included_posts = create_list(:post, 2, category_id: category.id)
      expect(PostsForBranchService.new({branch: 'hobby'}).call).to(
        match_array included_posts
      )
    end

    it 'returns posts filtered by a branch and a search input' do
      not_included_posts
      category
      included_post = [] << post
      expect(PostsForBranchService.new({branch: 'hobby', search: 'fun'}).call).to(
        eq included_post
      )
    end

    it 'returns posts filtered by a category name' do
      not_included_posts
      category
      included_post = [] << post
      expect(PostsForBranchService.new({branch: 'hobby', category: 'arts'}).call).to(
        eq included_post
      )
    end

    it 'returns posts filtered by a category name and a search input' do
      not_included_posts
      category
      included_post = [] << post
      expect(PostsForBranchService.new({name: 'arts', 
                                        search: 'fun', 
                                        branch: 'hobby'}).call).to eq included_post
    end
  end
end

このファイルの冒頭でposts_for_branch_service.rbファイルが読み込まれ、callメソッドの各条件がテストされます。

変更をcommitします。

git add -A
git commit -m "Add specs for the PostsForBranchService"

関連記事

Railsで重要なパターンpart 1: Service Object(翻訳)

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

Rails tips: ActiveRecordのトランザクションの概要(翻訳)

$
0
0

概要

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

Rails tips: ActiveRecordのトランザクションの概要(翻訳)

トランザクションとは、データベースレコードの作成といった細かな操作の集合であり、多くの場合、外部API呼び出しと組み合わせられます。たとえばゲストブックアプリを作成していて、ゲストの何人かがレコード作成に失敗することがあったとしても大したことはありませんが、オンラインストアでユーザーに課金するときにどこかでうまくいかなかった場合のことを考えてみましょう。

連続した操作のどこかで何かが失敗したら、そこまでの変更を取り消して、再度ユーザーに情報の入力と操作の再開を促したくなるでしょう。失敗した処理中に作成されたレコードはすべて削除しなければなりません。こうした処理はRailsのトランザクションでも行えます。

Railsのトランザクション作成

モデルのクラスやモデルのインスタンス(より一般的にはActiveRecord::Base)で.transactionを呼び出さなければなりません。用途に応じて最適な方法を選びます。1つのトランザクションは、そのモデルのインスタンスではなく、1つのデータベース接続に紐付けられます。

Order.transaction do
  @order.charge_credit_card!
  @user.order_histories.create!(@order)
end
@order.transaction do
  @order.charge_credit_card!
  @user.order_histories.create!(@order)
end
ActiveRecord::Base.transaction do
  @order.charge_credit_card!
  @user.order_histories.create!(@order)
end

トランザクションが失敗したとき

ぜひとも覚えておいていただきたい重要事項は、トランザクションのロールバックはエラーがraiseされたときしか行われないことです。トランザクションのブロック内でfind_byメソッドを使うなら、find_by!を使ってアプリで何らかの問題が生じたときにエラーがスローされるようにすべきです。トランザクションがロールバックした後でエラーがトランザクションブロックの外でraiseされ、エラーがキャッチできるようになります。

トランザクションを静かにロールバックさせたいのであれば、単にトランザクションブロック内でActiveRecord::Rollbackをraiseします。

トランザクションの詳細について

本記事はRailsトランザクションの概要をご紹介いたしました。ネストしたトランザクションやコールバックについても言及する値打ちがありますので、今後の記事にご期待ください。

Railsでお困りの方にお知らせ

知りたいことがありましたら、twitter または連絡用フォームにてお気軽にお問い合わせください。

RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

アトミックなトランザクションで冪等APIを強化する(翻訳)

Railsのトランザクションと原子性のバグ(翻訳)

週刊Railsウォッチ(20180309)RubyGems.orgのTLS 1.0/1.1接続非推奨、2年に1度のRailsアンケート、DockerのMoby Project、Ruby拡張をRustで書けるruruほか

$
0
0

こんにちは、hachi8833です。スマホを電車に置き忘れてしょんぼりしてます(´・ω・`)。

3月のウォッチ、いってみましょう。今回はつっつき成分は少なめです。

Rails: 今週の改修

5.2リリース直前だけに、大きな修正は見当たらない感じでした。5.2-stableと6.0向けmasterの両方に多数の同じ修正が当たっています。

まずは5.2-stableから。

関連付けの作成と検索の挙動を一貫させた

これは#29722の別案で、かつ#29601#a1fcbd9の復活です。
現時点では、関連付けの作成と通常の関連付け探索ではstore_full_sti_classが反映されておらず、eager loadingとpreloadingはこの設定を考慮しています。これでは、store_full_sti_class = falseの場合にeager loadingやpreloadingではポリモーフィックなレコードが作成されても検索できなくなります。
関連付けの作成と検索の挙動は一貫すべきです。
同PRより

# activerecord/lib/active_record/associations/preloader/association.rb#:121
             if reflection.type
-              scope.where!(reflection.type => model.base_class.sti_name)
+              scope.where!(reflection.type => model.base_class.name)
             end
# activerecord/lib/active_record/reflection.rb#L196
         if type
-          klass_scope.where!(type => foreign_klass.base_class.sti_name)
+          klass_scope.where!(type => foreign_klass.base_class.name)
         end

つっつきボイス:store_full_sti_classってメソッドかなと思ったら設定(属性)だったんですね」

# activerecord/lib/active_record/inheritance.rb#L43
  module Inheritance
    extend ActiveSupport::Concern

    included do
      # Determines whether to store the full constant name including namespace when using STI.
      # This is true, by default.
      class_attribute :store_full_sti_class, instance_writer: false, default: true
    end

その後#sti_nameString#demodulizeと追ってみました。

demodulizeは名前空間の部分を取り払うメソッドか↓」

'ActiveSupport::Inflector::Inflections'.demodulize # => "Inflections"
'Inflections'.demodulize                           # => "Inflections"
'::Inflections'.demodulize                         # => "Inflections"
''.demodulize                                      # => ''

cookie内の期限切れ情報の問題を修正

# actionpack/lib/action_dispatch/middleware/cookies.rb#L491
       private
         def expiry_options(options)
-          if options[:expires].respond_to?(:from_now)
-            { expires_in: options[:expires] }
+          if request.use_authenticated_cookie_encryption
+            if options[:expires].respond_to?(:from_now)
+              { expires_in: options[:expires] }
+            else
+              { expires_at: options[:expires] }
+            end
           else
-            { expires_at: options[:expires] }
+            {}
           end
         end

期限切れ情報を無効にするときにuse_authenticated_cookie_encryptionをチェックするようになりました。

Capybara 3.xに対応

# Gemfile#L16
-gem "capybara", "~> 2.15"
+gem "capybara", ">= 2.15", "< 4.0"

つっつきボイス: 「今のCapybaraは3.0.0.rc1がついこの間出たところなんですね」

PDFプレビューとしてmutoolsの他にPopplerも提供

MuPDFツールのライセンスが厳しいので、GPLライセンスのPopplerも使えるようにしたそうです。なお、検索してたらMUTOOLSという無関係な音楽制作ツールが出てきました。

# activestorage/lib/active_storage/engine.rb#L16
     config.active_storage = ActiveSupport::OrderedOptions.new
-    config.active_storage.previewers = [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
+    config.active_storage.previewers = [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
     config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
     config.active_storage.paths = ActiveSupport::OrderedOptions.new

参考: Poppler : PDFのコマンドラインツール | PDF

ActiveStorageで自分自身の置き換え時に添付blobがパージされないように修正

ここからはmasterブランチです。

# activestorage/lib/active_storage/attached/one.rb#L65
     private
       def replace(attachable)
-        blob.tap do
+        unless attachable == blob
+          previous_blob = blob
+
           transaction do
             detach
             write_attachment build_attachment_from(attachable)
           end
-        end.purge_later
+
+          previous_blob.purge_later
+        end
       end

has_one/belongs_toリレーションシップのdependent:を修正 => reject?

あるクラスのbelongs_tohas_oneリレーションシップでdependent: :destroyオプションを有効にした場合は、依存先クラスが削除された場合にこのクラスを削除すべきではない。
同commitより

# 同issueの再現手順より
class Parent
  has_one :child, dependent: :destroy
end

class Child
  belongs_to :parent, inverse_of: :child
  before_destroy { throw :abort }
end

c = Child.create
p = Parent.create(child: c)

p.destroy

# 期待する動作                  子がabortされても親はdestroyされない
p.destroyed? # false
p.child.destroyed? # false

# 実際の動作

p.destroyed? # true           #親がdestroyされてる
p.child.destroyed? # false

URI.unescapeでエスケープありなしのUnicodeを混在できるように修正

# 同commitより
    URI.unescape("\xe3\x83\x90")  # => "バ"
    URI.unescape("%E3%83%90")  # => "バ"
    URI.unescape("\xe3\x83\x90%E3%83%90")  # =>
                                         # Encoding::CompatibilityError
# activesupport/lib/active_support/core_ext/uri.rb#L13
       # YK: My initial experiments say yes, but let's be sure please
       enc = str.encoding
       enc = Encoding::UTF_8 if enc == Encoding::US_ASCII
-      str.gsub(escaped) { |match| [match[1, 2].hex].pack("C") }.force_encoding(enc)
+      str.dup.force_encoding(Encoding::ASCII_8BIT).gsub(escaped) { |match| [match[1, 2].hex].pack("C") }.force_encoding(enc)
     end

つっつきボイス:\xe3\x83\x90%E3%83%90を復元できてなかったのか」「引用符がにかかってて読みにくいですねー」

Rails起動時の明示的なハンドラオプション-uを追加

ハンドラ(PumaとかThinとか)の指定と環境の指定-eが混じってやりにくかったからだそうです。

$ bin/rails server thin     # 従来

$ bin/rails server -u thin  # 今後

Rails

globalize: ActiveRecordをi18n化するgem


同リポジトリより

# 同リポジトリより
I18n.locale = :en
post.title # => Globalize rocks!

I18n.locale = :he
post.title # => גלובאלייז2 שולט!

つっつきボイス: 「これ某案件のGemfileに入ってる…」「Readmeにはi18nのデファクトって書いてありますね」

これまでウォッチに登場した他のi18n関連gemをとりあえずリストアップしてみました。

PodCast: React on RailsとWebpackerの話(Hacklinesより)

★3700超えです。npmからyarnを使うように変わっています。react_on_railsのリポジトリを見ると「Webpacker 3.0に対応」とあります。先週4.0が出たばかりでしたね。

参考: react_on_rails導入

activerecord-turntable: シャーディングgem(Awesome Rubyより)

# 同リポジトリより
                  +-------+
                  |  App  |
                  +-------+
                      |
       +---------+---------+---------+---------+
       |         |         |         |         |
  `--------` `-------` `-------` `-------` `-------`
  | Master | |UserDB1| |UserDB2| |UserDB3| | SeqDB |
  `--------` `-------` `-------` `-------` `-------`

    development:
      clusters:
        user_cluster: # <-- cluster name
          algorithm: range_bsearch # <-- `range`, `range_bsearch` or `modulo`
          seq:
            user_seq: # <-- sequencer name
              seq_type: mysql # <-- sequencer type
              connection: user_seq_1 # <-- sequencer database connection setting
          shards:
            - connection: user_shard_1 # <-- shard name
              less_than: 100           # <-- shard range(like mysql partitioning) If you are using a modulo algorithm, it doesn't need it.
            - connection: user_shard_2
              less_than: 200
            - connection: user_shard_3
              less_than: 2000000000

つっつきボイス: 「ドリコムさんのgemです」「ドリコムさんはこういうのをgemに切り出す文化があるって聞いたことありました」「請負開発だと難しいところ…」「以前にPostgreSQLのシャーディング記事↓を翻訳したのを思い出しました」

ハンズオン: PostgreSQLシャーディング(翻訳)

be_jsonなどのJSON向けRSpecマッチャー(RubyFlowより)

★はまだ少ないですが。

# 同記事より
expect(response).to be_json('meta' => {})
expect(response).to be_json include('meta' => include('next' => end_with('offset=200')))
expect(response).to be_json hash_excluding('total')

つっつきボイス: 「こういうマッチャーが欲しい気持ちってむちゃくちゃよくわかりますね: レスポンスをこうやってjqっぽくテストするみたいな」「jqは癖強いけど速くて好きです♡」「be_jsonってメソッド名、JSONであるかどうかをチェックする、みたいに見えてちょい紛らわしいかも」

「ところでRSpecって何とかして英語的にしようとしてむしろハマっているような気がしません?」「もしかするとminiTestでよかったんじゃないかみたいな: この間のRuby25のMatzの対談でもそんな話が出てましたね: RSpecのマッチャーはいいと思いますが」「RSpecのマッチャーってそのままminiTestには持っていけないんですよね?」「やったことないけどできなさそうな気がします」「RSpecの構文ってDRYに書けDRYに書けって誘ってる気がする」

Rails: テスティングアンチパターン –後編(翻訳)

rack-reducer: Rackアプリで使えるURL書き換えgem(RubyFlowより)

# 同リポジトリより
# app/controllers/artists_controller.rb
class ArtistsController < ApplicationController
  def index
    @artists = Artist.reduce(params)
    @artists.all.to_json
  end
end

# app/models/artist.rb
class Artist < ActiveRecord::Base
  extend Rack::Reducer # makes `self.reduce` available at class level

  # Configure by calling
  # `reduces(some_initial_scope, filters: [an, array, of, lambdas])`
  #
  # Filters can use any methods your initial dataset understands.
  # Here it's an ActiveRecord query, so filters use AR query methods.
  reduces self.all, filters: [
    ->(name:) { where('lower(name) like ?', "%#{name.downcase}%") },
    ->(genre:) { where(genre: genre) },
    ->(order:) { order(order.to_sym) },
  ]
end

つっつきボイス: 「Rackミドルウェアかと思ったら、Rackを使うアプリで使えるgemですね: Sinatraでも使えるとか」「あ、ほんとだ」「でもこの名前だとRackミドルウェアかと思っちゃう(汗」

Service Objectがアンチパターンな理由(RubyFlowより)

# 同記事より
class Rating < ActiveRecord::Base
  belongs_to :user
  belongs_to :media

  def self.rate(user, media, rating)
    rating_record = Rating.find_or_initialize_by(user: user, media: media)
    rating_record.rating = rating
    rating_record.save

    # do some extra stuff here like run algorithmic data processing,
    # add social activities to timelines, etc.
  end
end

# And the updated controller code:
media = Media.find(params[:media_id])
rating = params[:rating].to_i
Rating.rate(current_user, media, rating)

つっつきボイス: 「結論はconcernsとPOROにしようってことみたいです」「この記事にもモーフィアスの絵が」「みんな好きですねー」

「先月の社内勉強会で『Service Objectは実は1種類ではないのでは』『少なくとも2種類あるのではないか』って話が出ましたけど、今のところそういう記事を英語圏でも見かけないんですよ」「推理小説の犯人が実は2人いる!とかみたいな?」「11人いる!とかw」

ruby_server_timing: RailsのサーバーメトリックスをChrome’s Developer Toolsで(GitHub Trendingより)


同リポジトリより


つっつきボイス: 「Chromeのコンソールで見られるのはちょっと便利そうっすね: ところでこれはScoutのサービスから取ってきてるみたいだけど有料?」「リポジトリにはScoutのアカウントは不要って書いてるから大丈夫なのかな?」


github.com/scoutappより

faster_path gemが0.3.1になってさらに高速化(RubyFlowより)

昨年のウォッチで取り上げたfaster_path gemです。Rustで書かれています。

FasterPath Rust Implementation Ruby 2.5.0 Implementation Time Shaved Off
FasterPath.absolute? Pathname#absolute? 91.9%
FasterPath.add_trailing_separator Pathname#add_trailing_separator 31.2%
FasterPath.children Pathname#children 13.2%
FasterPath.chop_basename Pathname#chop_basename 54.5%
FasterPath.cleanpath_aggressive Pathname#cleanpath_aggressive 73.8%
FasterPath.cleanpath_conservative Pathname#cleanpath_conservative 70.7%
FasterPath.del_trailing_separator Pathname#del_trailing_separator 80.6%
FasterPath.directory? Pathname#directory? 11.3%
FasterPath.entries Pathname#entries 8.4%
FasterPath.extname File.extname 41.2%
FasterPath.has_trailing_separator? Pathname#has_trailing_separator 67.6%
FasterPath.plus Pathname#join 66.4%
FasterPath.plus Pathname#plus 81.4%
FasterPath.relative? Pathname#relative? 84.1%
FasterPath.relative_path_from Pathname#relative_path_from 69.8%

同リポジトリより


つっつきボイス: 「これは速い: 置き換えは自分でやらないといけないんですかね?」「refinementできるみたいです↓」

require "faster_path/optional/refinements"
using FasterPath::RefinePathname

ruru: Rustで書けるRuby拡張(RubyFlowより)


同リポジトリより

# 同リポジトリより
#[macro_use]
extern crate ruru;

use ruru::{Boolean, Class, Object, RString};

methods!(
   RString,
   itself,

   fn string_is_blank() -> Boolean {
       Boolean::new(itself.to_string().chars().all(|c| c.is_whitespace()))
   }
);

#[no_mangle]
pub extern fn initialize_string() {
    Class::from_existing("String").define(|itself| {
        itself.def("blank?", string_is_blank);
    });
}

つっつきボイス: 「こんな感じで書けるんですねRust」「externとかマクロとかあって心がちょっとざわつく」「Ruby本家がさっきのfaster_pathみたいなのを標準で取り込んだりしないかなと思ったり: やらないだろうけど」

参考: プログラミング言語Rust

Railsについてのアンケート募集: 第5回(Hacklinesより)

Planet Argon社が、2年に一度ぐらいのペースで取っているRailsの使われ方などについてのアンケートです。


2016年度アンケートより


つっつきボイス: 「アンケート長いんで今は回答しなかったんですが、過去の回答が結構面白いかも」「おー、例外トラッキングツールはAirBrakeがトップ、とか↑」「GitHubの★とかとまた違った角度でいいかも」


planetargon.comより

その他Rails小物記事

Ruby trunkより

提案: ハッシュの値を省略できるようにしたい(継続)

x = 1
y = 2
h = {x:, y:}
p h #=> {:x=>1, :y=>2}
def login(username: ENV["USER"], password:)
  p(username:, password:)
end

login(password: "xxx") #=> {:username=>"shugo", :password=>"xxx"}

つっつきボイス: 「2つ目のはワカル気がしますけど、1つ目はどうかなー?」「こういうことができる実装の言語がどこかにあったような気がする…ES6かなと思ったら違った」

実際にはないメソッドがRandom::Formatterドキュメントにある

Range#===内部でinclude?じゃなくてcover?を使うようにしたい(継続)

#12612とかぶっているのを承知で再度上げています。こういうのを#===で書きたいようです。

IPAddress("172.16.10.1")..IPAddress("172.16.11.255")

参考: Range#cover?


つっつきボイス: 「このzverokさんの記事は何度か翻訳したことがあったので↓」

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

Ruby

RubyGems.orgがTLS 1.0/1.1を非推奨に(Ruby Weeklyより)


rubygems.orgより

OpenSSL versions 1.0.0t以下にリンクされたRubyや、JVM 6以下にリンクされたJRubyからRubyGems.orgに接続できなくなるそうです。

GitentialでGitHubリポジトリの昨年のRuby開発をビジュアル表示してみた(Awesome Rubyより)


同サイトより


同サイトより


つっつきボイス: 「見せ方が結構きれいだなと思って」「各コミッターの活動状況がわかるし」「オープンソースリポジトリのチェックによさそう」

ruby-advisory-db: bundler-auditとrubysec.comの脆弱性情報データベース

この間のウォッチで扱ったhttps://rubysec.com/サイトの脆弱性情報がここに集められているそうです。

RSpecを自作してDSLを理解する(Ruby Weeklyより)

# 同記事より
class Describe
  attr_reader :context_name

  def initialize(context_name, &block)
    @context_name = context_name
    instance_eval &block
  end

  def describe(context_name, &block)
    Describe.new(context_name, &block)
  end

  def it(context_name, &block)

  end
end

def describe(context_name, &block)
  Describe.new(context_name, &block)
end

describe NumberService do
  describe '#number' do
    it 'returns 12' do

    end
  end
end

Ruby言語の関数型プログラミング的側面(Hacklinesより)

高階関数やカリー化↓などをRubyでやってみる記事です。

クソコードを変態コードにしてやった【勉強会報告】

Rubyで遺伝的アルゴリズム(Hacklinesより)

CAPACITY = 20

def fitness
  weights = [2, 3, 6, 7, 5, 9, 4]
  values  = [6, 5, 8, 9, 6, 7, 3]

  w = weights
      .map
      .with_index { |w, idx| value[idx].to_i * w }
      .inject(:+)

  v = values
      .map
      .with_index { |v, idx| value[idx].to_i * v }
      .inject(:+)

  w > CAPACITY ? 0 : v
end

つっつきボイス: 「chromosome(染色体)とかsurvival of fittest(適者生存)とかそっち系の用語が出てきますね」「遺伝的アルゴリズムって言葉を久々に聞いた気がしました」

参考: Wikipedia-ja 遺伝的アルゴリズム

TensorFlowとRubyでCAPTCHAを解かせる(Awesome Rubyより)

CAPTCHAは機械学習で突破できるようになってしまったから昨今はもう意味がないという記事をこの間見かけましたが、さもありなんでした。

# 同記事より
require 'tensorflow'
# Loading Saved Model
saved_model = Tensorflow::Savedmodel.new
saved_model.LoadSavedModel(Dir.pwd + '/break-captcha-protobuf', ['serve'], nil)

# Read the image file and specify the image contents in a Tensor
image_file = File.new(Dir.pwd + '/break-captcha-protobuf/captcha-1.png', "r")
feeds_tensor = Tensorflow::Tensor.new(image_file.read)

参考: TensorFlow
参考: 機械学習を使ってCAPTCHAをわずか15分で突破するチャレンジが行われる - GIGAZINE

steep: Ruby 2.5を型付けするgem(Ruby Weeklyより)

class Foo
  # @implements SuperFoo
  # @type const Helper: FooHelper

  # @dynamic name
  attr_reader :name

  def do_something(string)
    # ...
  end

  def bar(symbol = :default, size:)
    Helper.run_bar(symbol, size: size)
  end
end
$ steep check lib/foo.rb
foo.rb:41:18: NoMethodError: type=FooHelper, method=run_bar
foo.rb:42:24: NoMethodError: type=String, method==~

つっつきボイス: 「作者のSoutaro Matsumotoさんは昨年のRubyKaigiでもRubyの型付けについて発表してて↓、論文の形でも発表してたりします」「アノテーションとかで型を指定する感じなんですね: こういうのをしなくていいのがRubyのいいところなのに、と思ったりもしますが」

週刊Railsウォッチ(20170922)特集: RubyKaigi 2017セッションを振り返る(1)、Rails 4.2.10.rc1リリースほか

image_optim: 外部ツールを使い分けて画像を最適化するgem(Ruby Weeklyより)

ORMを使いこなすには(Hacklinesより)

最近読んだ本(主にRuby関連)(Hacklinesより)

pwned: haveibeenpwned.comのAPIにRubyでアクセス(Awesome Rubyより)


haveibeenpwned.comより

# 同リポジトリより
password = Pwned::Password.new("password")
password.pwned?
#=> true
password.pwned_count
#=> 3303003

haveibeenpwned.comは、メアドを入力するとそのパスワードが抜かれているかどうかを調べるサイトのようです。前からあるようですが、自分のメアドを入れるのはちょっと勇気が要りそう。

参考: 自分のメアドが流出していないか確認できるウェブサービス「Have I been pwned?」 | YoutaChannel

GrpcをRubyで理解する(RubyFlowより)

Grpcサーバーとクライアントの動かし方を紹介しています。

# 同記事より
server = GRPC::RpcServer.new
server.add_http2_port("0.0.0.0:50051", :this_port_is_insecure)
server.handle(Handler.new)
server.run_till_terminated

ウィーンで開催のEuRuKo 2018 CFP募集

昨年のウォッチでも取り上げたEuRuKo、2017年度はこんな感じでした。

SQL

あなたの知らないPostgreSQL 10: CREATE STATISTICS編(Postgres Weeklyより)

Explainのテクニックをいくつか紹介しています。

# 同記事より
EXPLAIN ANALYZE SELECT * FROM tbl where col1 = 1;                            
                                                QUERY PLAN                                                 
-----------------------------------------------------------------------------------------------------------
 Seq Scan on tbl  (cost=0.00..169247.80 rows=9584 width=8) (actual time=0.641..622.851 rows=10000 loops=1)
   Filter: (col1 = 1)
   Rows Removed by Filter: 9990000
 Planning time: 0.051 ms
 Execution time: 623.185 ms
(5 rows)

つっつきボイス: 「これ翻訳打診してみますね」

PostgreSQLでMeltdown脆弱性をベンチマーク(Postgres Weeklyより)


同記事より


つっつきボイス: 「retpolineっていうのがパッチの名前なのか」

以下によると、”return”と”trampoline”の造語だそうです。

参考: Retpoline – Google’s fix for Spectre

JavaScript

webpack-server: webpack-dev-serverの後継


webpack-contrib/webpack-serveより

webpack-dev-serverはメンテナンスモードになったとのことです。

参考: webpack-dev-server の後継らしい webpack-serve について調べた - 備忘録β版

React-vis: React向けビジュアル表示ライブラリ


同リポジトリより

Uber製です。

dom-to-image: DOMノードから画像を生成(GitHub Trendingより)

# 同リポジトリより
var node = document.getElementById('my-node');

domtoimage.toPixelData(node)
    .then(function (pixels) {
        for (var y = 0; y < node.scrollHeight; ++y) {
          for (var x = 0; x < node.scrollWidth; ++x) {
            pixelAtXYOffset = (4 * y * node.scrollHeight) + (4 * x);
            /* pixelAtXY is a Uint8Array[4] containing RGBA values of the pixel at (x, y) in the range 0..255 */
            pixelAtXY = pixels.slice(pixelAtXYOffset, pixelAtXYOffset + 4);
          }
        }
    });

つっつきボイス: 「サンプルやデモが見当たらないっすね」「画像見たいのにー」

Propel ML: numpy風differentialプログラミングライブラリ(JavaScript Weeklyより)

import { grad, linspace, plot } from "propel";

f = x => x.tanh();
x = linspace(-4, 4, 200);
plot(x, f(x),
     x, grad(f)(x),
     x, grad(grad(f))(x),
     x, grad(grad(grad(f)))(x),
     x, grad(grad(grad(grad(f))))(x))


propelml.orgより

Notebookでとりあえず動かせます。

CSS/HTML/フロントエンド

コンテナクエリの問題(Frontend Focusより)

「飢えたデザイナー」のための倫理

社内勉強会で触れられていたので。先週のウォッチで扱った「プログラマーの誓い」のデザイナー版みたいな位置づけです。

デザイン世界の「失われた世代」(Frontend Weeklyより)

長くてエモめの記事です。後で読んでみます。

Go言語

gofaas: AWS LambdaのGo向けテンプレート(GitHub Trendingより)

# 同リポジトリより
$ make deploy
make_bucket: pkgs-572007530218-us-east-1
Uploading to 59d2ea5b6bdf38fcbcf62236f4c26f21  3018471 / 3018471.0  (100.00%)
Waiting for changeset to be created
Waiting for stack create/update to complete
Successfully created/updated stack - gofaas

ApiUrl  https://x19vpdk568.execute-api.us-east-1.amazonaws.com/Prod

one-file-pdf:ワンバイナリでPDF一発生成(GitHub Trendingより)

ソースも1ファイルです。

Moby Project: Dockerが推進するコンテナシステムフレームワーク(GitHub Trendingより)


同リポジトリより

Dockerが生んだMobyは、車輪を再発明せずに特殊コンテナシステムを組み立てるためのオープンなフレームワークです。さまざまな標準コンポーネントの「LEGOセット」と、それらをカスタムプラットフォームとして組み立てるフレームワークが提供されます。Mobyの中核にあるのは、「コンポーネント」「ツール」「アセンブリ」を提供する特殊コンテナシステムを組み立てるフレームワークです。
mobyproject.orgより大意

Slack-term: CLIでSlackしたい人に(GitHub Trendingより)


同リポジトリより

とりあえず手元で動かしてみました。
リアクションが文字のままなのと、日本語と絵文字が接すると文字化けしましたが、軽快です。


つっつきボイス: 「社内でVimをSlackクライアントにしてた人いましたね、そういえば」「そういえば!」

その他

Google SpreadsheetをWebサーバーにする


つっつきボイス: 「日本語記事ですが一応」「GETとかPOSTとか扱えるんですね…ゴクっ」

参考: Content Service  |  Apps Script  |  Google Developers

codepilot.ai: ローカルやStackoverflowなどのコードを検索するアプリ


同サイトより


とりあえずダウンロードして動かしてみました。あのメソッドどこに書いたっけみたいなときにプロジェクト/言語横断的に検索するのによさそうな雰囲気です。

git-sizer: Gitリポジトリのファイルサイズを簡易アナライズ(GitHub Trendingより)

サイズに着目して、問題ありそうなファイルを見つけるツールです。

# 同リポジトリより
$ git-sizer --verbose
Processing blobs: 1652370
Processing trees: 3396199
Processing commits: 722647
Matching commits to trees: 722647
Processing annotated tags: 534
Processing references: 539
| Name                         | Value     | Level of concern               |
| ---------------------------- | --------- | ------------------------------ |
| Overall repository size      |           |                                |
| * Commits                    |           |                                |
|   * Count                    |   723 k   | *                              |
|   * Total size               |   525 MiB | **                             |
| * Trees                      |           |                                |
|   * Count                    |  3.40 M   | **                             |
|   * Total size               |  9.00 GiB | ****                           |
|   * Total tree entries       |   264 M   | *****                          |
| * Blobs                      |           |                                |
|   * Count                    |  1.65 M   | *                              |
|   * Total size               |  55.8 GiB | *****                          |

Xray: Rustで書かれたElectronベースの実験的テキストエディタ(GitHub Trendingより)

内部で文字列をcopy-on-writeしているというのが気になったので。CRDTは「Conflict-Free Replicated Data Types」のことだそうです。


同リポジトリより

参考: rgasplit-group2016-11.pdf(CRDTの論文)

いいプルリクの書き方(Hacklinesより)


  • 読む側に考えさせない
  • 相手の立場に立てることが開発者として最強
  • 出発点と道筋を示す
  • どこが問題かをはっきり示す
  • 必要な情報(コンテキスト)をちゃんと添える
  • スクショや図を添える
  • etc.

番外

SATySFiはLaTexを超えるか

TextPathView: 筆順をアニメーション表示(GitHub Trendingより)

筆順というより輪郭ですが。
ちなみに日本と中国で漢字の筆順は結構違ってるそうです。

参考: いわゆる「正しい筆順」の幻想


今週は以上です。

バックナンバー(2018年度)

週刊Railsウォッチ(20180302)Ruby 2.6.0-preview1とWebpack 4.0リリース、爆速検索APIサービスAlgolia、Clowneでモデルをクローンほか

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

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

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

Awesome Ruby

Random Ruby

Ruby on Rails Security Project

RubyFlow

160928_1638_XvIP4h

Hacklines

Hacklines

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Frontend Focus

frontendfocus_banner_captured

JavaScript Weekly

javascriptweekly_logo_captured

Github Trending

160928_1701_Q9dJIU

Rails tips: Explaining Variableパターンによるリファクタリング(翻訳)

$
0
0

概要

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

Rails tips: Explaining Variableパターンによるリファクタリング(翻訳)

ロジックの変数化リファクタリング(Explaining variable refactoring)とは、読んで字のごとく、複雑な式のロジックを、意味のある名前を持つ一時変数に移し替えることです。

このようにリファクタリングすることでコードがシンプルで読みやすく、かつ意味が明確になります。このリファクタリングの真の意図をご理解いただくために、以下のような複雑なif条件を考えてみましょう。

if(user.sign_in_count > 0 && user.posts.any? && (company.employees.size > 100 || company.mode == :maintenance)
  # 何かする
end

ここで使われている条件をすべて把握するには、しばし目を凝らす必要があるでしょう。これら条件のひとつひとつを、次のように動作をわかりやすく説明する変数に置き換えることでシンプルにできます。

user_signed_in = user.sign_in_count > 0
user_has_posts = user.posts.any?
large_company = company.employees.size > 100
company_in_maintenance = company.mode == :maintenance

if(user_signed_in && user_has_posts && (large_company || company_in_maintenance)
  # 何かする
end

ロジックを別のメソッドにすることも可能です。こうすることで読みやすく、コードが自ら語るようになるとは思いませんか?条件文を読むときにコードの作者の意図で迷うことがなくなります。


Railsでお困りの方にお知らせ

知りたいことがありましたら、twitter または連絡用フォームにてお気軽にお問い合わせください。

RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

[Ruby/Rails] 例外で深くなったネストをGuard Clauseですっきりさせる

Rails tips: belongs_to関連付けをリファクタリングしてDRYにする(翻訳)

Viewing all 1381 articles
Browse latest View live