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

RailsアプリをAWS Elastic Beanstalkにデプロイする手順(翻訳)

$
0
0

概要

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

RailsアプリをAWS Elastic Beanstalkにデプロイする手順(翻訳)

前回の記事はElixirをElastic Beanstalkにデプロイする方法でしたが、今回は数あるフレームワークの中でもRailsを愛する会社であるSyndicodeより、RailsアプリをAWS Elastic Beanstalkにデプロイする方法のチュートリアルをお送りいたします。なお弊社ではRails開発者を絶賛募集中です。

Elastic Beanstalkについて簡単におさらいします。これはAWS(Amazon Web Services)上にアプリを設定する手順を自動化するクラウドデプロイメントサービスです。本チュートリアルではコマンドラインインターフェイス(CLI)を用いてAWS Elastic BeanstalkにRailsアプリをデプロイする方法をご紹介します。

1. Elastic BeanstalkのCLIをインストールする

Macの場合、HomeBrewでaws-elasticbeanstalkをインストールします。

brew install aws-elasticbeanstalk

Homebrewを使ったことがない場合やLinux環境の場合は、pip(Pythonのパッケージ管理ツール)でインストールします。

sudo pip install awsebcli

Windowsの場合は以下を実行します。

pip install awsebcli

2. Raisアプリをgit cloneする

ここではhttps://github.com/engineyard/todo.gitのサンプルアプリをgit cloneします。Elastic Beanstalk CLIで自分のRailsアプリを使いたい場合は、アプリをGitリポジトリに置く必要があります。

git clone https://github.com/engineyard/todo.git

3. IAMロールを作成する

AWSサイトの手順に従って、IAMロールを2つ作成します。必要なのはaws-elasticbeanstalk-ec2-roleaws-elasticbeanstalk-service-roleです。アプリや環境はElastic Beanstalk CLIで作成しますが、これらのIAMロールの作成だけはElastic Beanstalkコンソールで[Create New Application]をクリックして行います。この手順は一度だけ行う必要があります。以下を順に実行します。

  • Elastic Beanstalk consoleを開きます。
  • [Create New Application]を選択します。
  • ウィザードに従って[Permission]ページまで進みます。
  • [Next]をクリックしてIAM consoleを開きます。
  • [Allow]を選択してロールを作成します。

4. Elastic Beanstalk CLIのセットアップ

cd todo
eb init

AWS Access KeyとAWS Secret Access Keyが1つずつ必要です。ない場合は、IAM consoleでユーザーを1つ作成してcredentialをダウンロードします。リージョン、利用するアプリ([Create New Application])、プラットフォームのバージョンはデフォルト値で構いません。共有するToDoアプリには既にプラットフォームのバージョンが含まれています。独自のRailsアプリを使う場合は、Ruby 2.3 Pumaを必ず追加してください。

5. 環境を作成する

eb create todo_production

Elastic BeanstalkはSecurity Group、ELB、Auto Scalingグループを作成します。3分もすれば環境が整い、以下でアプリをチェックできるようになります。

eb open

環境の情報


Engine Yardのサンプルデプロイアプリより

以下にご注意ください。

  • Rubyは/opt/rubies/ruby-2.3.4の下に置かれます
  • Railsアプリは/var/app/currentの下に置かれます
  • ユーザー名はWebアプリ名が使われます

6. データベースを作成する

このToDoアプリの設定は、実際のアプリで使うには少々正しくない点があります。ToDoアプリのconfig/database.ymlではSQLite3データベースを使っているため、データベースを設定していなくても動いてしまいます。todo_production環境がEC2インスタンスを1つ持っているだけなので、さしあたってこれで十分です。しかしAuto ScalingグループでEC2インスタンスがもうひとつ作成されると、新しいインスタンスは独自のSQLite3データベースを持ちます。これは期待する動作ではないでしょう。データベースを1つにまとめるには、config/database.ymlを削除します。DATABASE_URLを使うので、このファイルは不要になります。

git rm config/database.yml
git commit -m 'Remove database.yml'

RDS consoleでRDSインスタンスを1つ作成します。MySQLまたはPostgreSQLを使えます。このToDoアプリのGemfileにはmysql2 gemとpg gemが両方入っているので、どちらもサポートされています。オプションのデータベース名フィールドには「todo」と入力します。論理データベースは、RDSインスタンスの作成後に作成されます。セキュリティのため、[Publicly Accessible]はNoに設定します。RDSインスタンスの準備が整ったら、Detailsアイコンをクリックして[Security Group]をクリックします。rds-launch-wizard-2 (sg-041b107e)のような文字列が表示されます。

[Security Group]ページで、[Inbound]、[Edit]の順にクリックし、PostgreSQL用のルールを追加します。sourceでCustomを選択してElastic Beanstalk環境のSecurity Groupを入力します。sgと入力するとSecurity Groupのリストが表示されます。

正しいSecurity Groupが見つからない場合は、グループ名にElastic Beanstalk環境idが使われています。環境idを取得するには、eb statusと入力します。私の場合、環境idがe-kq7hjkf7dtで、Security Group名がawseb-e-kq7hjkf7dt-stack-AWSEBSecurityGroup-44MI138FQVGとなっています。AWSEBLoadBalancerSecurityGroupを含む名前は選択しないでください。


Engine Yardのキャプチャ画像より

これでElastic Beanstalkによって作成されたEC2インスタンスがRDSインスタンスにアクセスできるようになるはずです。このElastic Beanstalk環境に紐付けられるRDSインスタンスを作成することもできますが、この環境をterminateするとRDSインスタンスもterminateしてしまうためおすすめできません。

7. DATABASE_URLを設定する

RDS credentialを使ってDATABASE_URL環境変数を設定します。形式はdb_type://username:password@hostname:port/db_nameです。たとえば、PostgreSQLインスタンスを作成した場合は次のcredentialを使います。

user: engineyard
password: mysecretpassword
hostname: eypostgres.cjb9zibjzcpd.us-west-2.rds.amazonaws.com
port: 5432
db name: todo

続いて以下を実行します。

eb setenv DATABASE_URL=postgres://engineyard:mysecretpassword@eypostgres.cjb9zibjzcpd.us-west-2.rds.amazonaws.com:5432/todo

次に、アプリをdatabase.ymlなしでデプロイしてページを開きます。

eb deploy
eb open

以上で、単独のRDSインスタンスを用いるElastic Beanstalk上でRailsアプリが動きました。

8. Secret Key Base

この環境変数を使わない場合は、SECRET_KEY_BASEを設定するか、encrypted Rails secretsを使う場合はRAILS_MASTER_KEYを設定します。bundle exec rake secretでsecret key baseを新しく生成します。

eb setenv SECRET_KEY_BASE=cccae61c0c117c787745b596655caa50062dc3fc739505df02e209d9e737a2f39ab484d20e63d5937e1c58901e81109523807f66be421728851fecc2262ed5a8

9. SSH

eb initを実行すると、public keyを追加できます。新しいkeyペアを作成することもできます。eb initを既に実行した場合は、--interactiveオプションを付けて再度実行することもできます。eb sshを実行して、自分の環境のEC2インスタンスに接続します。

10. 以上でおしまいです

AWS Elastic BeanstalkにRailsアプリをデプロイする簡単な方法をご紹介いたしました。サポートされるRubyバージョンやAppサーバーに制約があるため、一部については制御できないことがあります。Sidekiqなどのバックグランドワーカーについては改良が必要です。現時点では、.ebextensionsにファイルをひとつ作成し、SidekiqワーカーをRailsアプリと同じインスタンスで実行する必要があります。中規模アプリでは、Sidekiq専用のインスタンスを用意すべきです。

もっと詳しく知りたい方は、ぜひweekly newsletterをご購読ください。

関連記事

無料で使えるAWSアカウント用セキュリティ監査ツールの紹介(翻訳)

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


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

$
0
0

こんにちは、hachi8833です。Ruby 2.5リリースまでもう少しです。

Rails勉強会@東京 第93回に初参加させていただきました。2年半ぶりの開催だったそうです。

お知らせ: 年末年始にかけて週刊Railsウォッチをお休みいたします: 次回は1月12日です。

今年最後のRailsウォッチ、いってみましょう。

Rails: 今週の改修

今週の更新情報は多めなので選別してみました。

セキュリティ関連ヘッダーを追加

# actionpack/lib/action_dispatch/railtie.rb#26
     config.action_dispatch.default_headers = {
       "X-Frame-Options" => "SAMEORIGIN",
       "X-XSS-Protection" => "1; mode=block",
-      "X-Content-Type-Options" => "nosniff"
+      "X-Content-Type-Options" => "nosniff",
+      "X-Download-Options" => "noopen",
+      "X-Permitted-Cross-Domain-Policies" => "none"
     }

つっつきボイス: 「X-で始まるヘッダって確かIE向けがほとんどだったと思うはRFC定義されていない独自拡張を表している」「X-Download-Options(IE向け)はまだわかるけど、↓の良記事見るとFlashがらみのヘッダが目につくなー: Railsでそこまで手を回すのってどうなんだろ?」「secureheadersにもこのヘッダ入ってるんで、セキュリティ関係者が入れときたいと思ってるらしいことはワカッタ」「10月のウォッチで扱ったgemですね」

db.createのエッジケースを修正

スキーマキャッシュの読み込み時には現在のマイグレーションバージョンをフェッチする。
しかしデータベースが存在しない場合に接続を取れずにエラーになる。これはデータベース作成時に問題になる。
データベースがない場合はスキーマキャッシュは不要なのでエラーを無視するよう修正。
#31311より大意

# activerecord/lib/active_record/migration.rb#56
-      def current_version(connection = Base.connection)
+      def current_version(connection = nil)
+        if connection.nil?
+          begin
+            connection = Base.connection
+          rescue ActiveRecord::NoDatabaseError
+            return nil
+          end
+        end

ActiveStorage::Blobからvariantを削除

# activestorage/app/models/active_storage/blob.rb#273
  def delete
-    service.delete key
+    service.delete(key)
+    service.delete_prefixed("variants/#{key}/") if image?
 @kaspth

つっつきボイス: 「variantって、作成した後削除し忘れてつまづきがちなやつ」「ところでblobって言葉ここに限らずいろんなところで見かけるんですが、どんな意味でしたっけ」「だいたいバイナリを表すことが多いっすね」「バイナリ・ラージ・オブジェクトの略なのか」「オブジェクト指向のオブジェクトではないw」

Railsのblobについての記述は以下にありました。

blobは、そのサービス上にあるファイルの位置を示すファイルとキーについてのメタデータを含むレコードです。
rails/activestorage/app/models/active_storage/blob.rbより大意

Railsの起動メッセージがきびきび表示されるようになった

起動直後に=> Booting Railsを表示するようになりました。

# railties/lib/rails/generators/rails/app/templates/config/boot.rb.tt#3
require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
+
+if %w[s server c console].any? { |a| ARGV.include?(a) }
+  puts "=> Booting Rails"
+end

つっつきボイス: 「これは地味にうれしい」「今までなかったのが不思議ですね」

foreign_keysinformation_schemaを修正

初コミットおめでとうございます。

# activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#406
-          FROM information_schema.key_column_usage fk
-          JOIN information_schema.referential_constraints rc
+          FROM information_schema.referential_constraints rc
+          JOIN information_schema.key_column_usage fk
           USING (constraint_schema, constraint_name)
           WHERE fk.referenced_column_name IS NOT NULL
             AND fk.table_schema = #{scope[:schema]}
             AND fk.table_name = #{scope[:name]}
+            AND rc.constraint_schema = #{scope[:schema]}
             AND rc.table_name = #{scope[:name]}

つっつきボイス: 「これはMySQL向けだな: information_schemaがあるからすぐわかる」「JOINの方向を逆にしたのか」

SQLを発行したコードの行をオプションで出力

development.rbでconfig.active_record.verbose_query_logs = trueとすることでapp/views/news/show.html.erb:9:inのように出力されるようになりました。

# 26815より
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'

つっつきボイス: 「これはマジありがたい!: eager-loadingしまくってるとどうなるかというのはあるけれど」「そういえばこの間joker1007さんが『activerecord-cause gemは役割を終えた』みたいなことを言ってたのはこの修正のことかな?」「ちょうどこの間のウォッチでactiverecord-cause取り上げたところだった」

nobuさんの珍しいコミット

# 81b99b2 と 2d5700b
-    @cache_path = Tempfile.create(%w{tmp cache}, Dir.tmpdir)
+    @cache_path = Dir.mktmpdir(%w{tmp cache})

# bff3ee8 と 4022f33
-    @cache_path = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname('tmp', 'cache')
+    @cache_path = Tempfile.create(%w{tmp cache}, Dir.tmpdir)

つっつきボイス: 「Rails勉強会@東京で話題になってたので」「nobuさんはRubyのコミッターだからRailsにコミットするのは確かに珍しいかも」「それ用のメソッドがRubyにあるから使おうよ、ってことですね」

なおRubyとRailsの両方で活発に活動している方はAaron Patterson氏を始め結構います。

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

Rails

active_record-mti: PGネイティブの継承テーブルをARで使うgem

Rails勉強会@東京で評判がよかったやつです。

# TwilightCoders/active_record-mtiより
ActiveRecord::Schema.define(version: 20160910024954) do

  create_table "accounts", force: :cascade do |t|
    t.jsonb    "settings"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "users", inherits: "accounts" do |t|
    t.string "firstname"
    t.string "lastname"
  end

  create_table "developers", inherits: "users" do |t|
    t.string "url"
    t.string "api_key"
  end

end

つっつきボイス: 「MTI: Multiple Table Inheritance」「PostgreSQLにテーブル継承できる機能があるのは知ってたし、6系の頃に使ったことあったけど多重継承できるのか↓: READMEのサンプルでは多重継承してないけど↑」「いやほんと、ぽすぐれのテーブル継承、普通に使えるし便利やで?」「STIだとカラム増えすぎるんですよね」

CREATE TABLE boatcar () INHERITS (boat,car);

「やっぱりぽすぐれいいな」「ビュー(データベースビュー)もいいし: Railsにcreate view、ほすぃ~」「同意」「同意」

ついでに9.6でこんな記述を見つけました。

親テーブル上の検査制約と非NULL制約はその子テーブルに自動的に継承されます。 他の種類の制約(一意性制約、プライマリキー、外部キー制約)は継承されません
PostgreSQL 9.6 – 5.8. 継承より

Rails: STI(Single Table Inheritance)でハマったところ

active_mocker: ARのモックを生成するgem

これもRails勉強会で話題でした。

# 同リポジトリより
require 'rspec'
require 'active_mocker/rspec_helper'
require 'spec/mocks/person_mock'
require 'spec/mocks/account_mock'

describe 'Example', active_mocker:true do

  before do
    Person.create # stubbed for PersonMock.create
  end

end

つっつきボイス: 「schema.rbが更新されるとモックがfailする、と」「ARでDBアクセスしないなら当然速くなるな」「ただDBMSに入れてloadし直すことによって挙動変わる場合はどうなるんだろう」「fasterなテストとして普段はactive_mocker: trueを回しておいて、定期実行では普通にDB使って回す方が、DBMSが型変換したりするケースとかも考えれば安全そう」

RubyWeeklyの「2017年人気記事リスト」(Ruby Weeklyより)

年の瀬を感じる企画ですね。TechRachoで翻訳した記事もいくつか見当たりました。


rubyweekly.comより


つっつきボイス: 「英語圏のネット系マガジンは軒並み1月までお休みですね: さすがクリスマス最優先な文化圏」「TechRachoでもこの企画やればいいのに」「来週やりましょう!」

get_schwifty: ActionCableでRailsビューの一部をバックグラウンドレンダリング(Ruby Weeklyより)

# 同リポジトリより
# app/cables/calculator_cable.rb
class CalculatorCable < BaseCable
  def fibonacci
    n = SecureRandom.rand(30..40)
    calculated = calculate_fibonacci(n)
    stream partial: "calculator/fibonacci", locals: { calculated: calculated, n: n }
  end

  private

  def calculate_fibonacci(n)
    return n if n <= 1
    calculate_fibonacci( n - 1 ) + calculate_fibonacci( n - 2 )
  end
end

つっつきボイス: 「これとよく似たgemがあったなー: render_asyncだ」「TechRachoで記事にしてました↓」

Rails: render_async gemでレンダリングを高速化(翻訳)

「render_asyncは素直にjQueryで遅延loadingしていてとてもわかりやすい: その代わりStreamとかはできない」
「そしてこちらのget_schwiftyはActionCable使ってStream APIで転送するので、render_asyncよりもさらにいい感じに出せる: ただしTCPセッション消費するからworker枯渇が怖いけど」

技術的負債調査のポイント10個


codeclimate.comより


つっつきボイス: 「こちらCode Climateブログの記事です」「お、Code Climateブログって記事本数は少ないけど質がとっても高いんで信頼できる: だいたいほぼ文句付けようのないレベル」「私も読んでて同じこと思ったので次で過去記事掘り起こしてみました」

古典技術記事探訪: CodeclimateやSemaphore.ciブログ

いずれも2014年の記事ですが、今も通用しそう。


つっつきボイス: 「1は例の定番記事『肥大化したActiveRecordモデルをリファクタリングする7つの方法』で一番多かった質問『どうしてクラスメソッドでできることをわざわざインスタンスメソッドにするの?』に答えたものだそうです」「前にも話したけど、クラスメソッドで作ったものを後でインスタンスメソッドに変えるのはほんとつらい」

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

「4の図↓もよかったので」「さっきのactive_mockerとかは、まさにこのピラミッドの左下を目指すためのもの」「単体テストほど軽く速くするということですね」


codeclimate.comより

Faktory: バックグラウンドジョブサーバー

ちょっと検索しにくい名前ですが。


contribsys/faktoryより

Q. productionで使えますか?
A. なにぶん新しいプロジェクトなので、リスクを許容できるのならば。APIが安定したら1.0をリリースする予定で、自分では安定していると思います。

Q. FaktoryにはRedis必要ですか?
A. 不要です。Faktoryはスタンドアロンの64ビットLinuxバイナリであり、ジョブを回すのにFaktoryワーカープロセスが必要です。「Redis -> Sidekiq」 == 「Faktory -> Faktory worker」という関係です。

Changelog: 技術系Podcast

上のPodcastもここです。Rails勉強会でこのPodcastをチェックしている人が結構いたので。


changelog.comより


つっつきボイス: 「みっちり文字起こししているところが凄いんですよ」「こういうのがありがたいっすね: 検索でも見つけやすいし」

Decoratorを比較


  • 素のdecorator
  • Module + Extend + Super decorator
  • Plain Old Ruby Object decorator
  • Class + Method Missing decorator
  • SimpleDelegator + Super + Getobj decorator

thoughtbotの記事です。2015年ですがまとまりがよかったので。


つっつきボイス: 「ちょっと前に見つけた別のDecorator記事が構成的にちょっと残念だったので」「うんうんそういうのよくあるw」

Railsガイドで怖気づいた人向けの攻略方法


sihui.ioより


つっつきボイス: 「いっぺんに読もうとすると挫折するので最初に全体像を把握しよう的なアドバイスですね」「まーでもわかる: 最近のRailsガイドは情報てんこ盛りでガイドという感じでなくなりつつあるかなぁ」

モブプログラミング


codeclimate.comより

codeclimate.comの2014年の記事です。公式サイトには来年4月にマサチューセッツ州でカンファレンスもあるそうです。


つっつきボイス: 「ペアプログラミングの次はモブプログラミング」「モブプロって、書いている人は自分の意見を交えないルールだったかな」「集団二人羽織的な?」

参考: モブプログラミング – Woody Zuill氏とのインタビュー

Ruby trunkより

Kernel#ppが2.5で標準に

11月のウォッチでお伝えしたKernel#ppがその後本採用になっていました。

net/protocol、net/smtp、tempfile、tmpdirを誰もメンテしないなら自分がやる

_ko1さん激賞のnormalpersonさんです。


つっつきボイス:net/protocolって初めて見た」

参考: Ruby HTTPクライアントの比較表

Ruby

awesome-ruby.com: 定番gemまとめ情報


markets/awesome-rubyより

1ページに全部書いてあるところが便利そうです。
同じ名前のニュースサイトがあって少々紛らわしいですが。

Light Cable: Railsなしで使えるActionCable実装

# 同リポジトリより
Rack::Builder.new do
  map '/cable' do
    # You have to specify your app's connection class
    use LiteCable::Server::Middleware, connection_class: App::Connection
    run proc { |_| [200, { 'Content-Type' => 'text/plain' }, ['OK']] }
  end
end

つっつきボイス: 「こういうAC実装が出てくるのは理解できる: Rackだけ使いたいときとか」「例のEvil Martiansがスポンサーになってますね」

Rubyからsymbolをなくせるか(Ruby Weeklyより)

# 同記事より
{"foo" => 1}[:foo] == 1 # trueだったらいいのに
{foo: 1}["foo"]    == 1 # trueだったらいいのに

つっつきボイス: 「だからHashWithIndifferentAccessが欲しくなる」「記事で引用されてるこれほんに↓: SmalltalkだとSymbolはStringを継承しているのにRubyはそうじゃない」

Rubyコードを関数型プログラミングっぽく書いてみた

# 同記事より
module APIDataCommons
  extend self

  def band_names(data)
    user_data(data)
      .fetch('favorite_bands')
      .map { |b| b['name'] }
  end

  def name(data)
    user_data(data).fetch('name')
  end

  private
    def user_data(data)
      data.fetch("user")
    end
end

つっつきボイス: 「あまり関数型っぽいコードには見えないかな?: あえて言うならmethod chainingをふんだんに使ったPromise的なコード」「社内Haskell勢のツッコミが待たれる」

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

Rubyデザインパターンとサンプルコード総ざらえ

説明は抑えめで、図とRubyコード中心です。


bogdanvlviv.github.ioより

class Task
  attr_accessor :name, :parent

  def initialize(name)
    @name = name
    @parent = nil
  end

  def get_time_required
    0.0
  end
end

つっつきボイス: 「1ページに収まっているので辞書的に便利そう」

JSON仕様「RFC 8259」と「ECMA-404 2nd Editon」リリース、UTF-8必須に


つっつきボイス: 「今話題のやつ」「404だとNot Foundに見えてしまうと誰かツイートしてた」「RFC 8259の方もどことなく蟹さんマークのNICチップっぽい名前🦀


卜部さんがこの仕様を元にすごい勢いでJSONパーサー作ってました。

Rubyでリサジュー曲線


つっつきボイス: 「何だか懐かしい感」「リサジュー曲線を見たのがとても久しぶりだったので」

参考: リサジュー図形

ずっと「リサージュ」と思い込んでました(´・ω・`)

SQL

Postgres Weeklyも1月までお休みだそうです。

PostgreSQL 10の互換性のない変更点(Postgres Weeklyより)

記事の日付は10リリース前の「16 May 2017」ですが、リリースノートと見比べるときによさそうです。


つっつきボイス: 「これは見ておくべき情報!」「ありがたい🙏

PostgreSQLのインデックス(Postgres Weeklyより)


citusdata.comより


  • B-Tree
  • Generalized Inverted Index (GIN)
  • Generalized Inverted Seach Tree (GiST)
  • Space partitioned GiST (SP-GiST)
  • Block Range Indexes (BRIN)
  • Hash

つっつきボイス: 「これも大事っすね: B-Treeで間に合うことが多いけど、データの種類や性質に応じたインデックスを選ばないとインデックスろくに効かなくなったりする」

俺のPostgreSQLチートシート(Postgres Weeklyより)


つっつきボイス: 「チートシートというほど網羅的ではないかな」「『俺のチートシート』ですね」「ポスグレって\で始まるコマンド体系がとっつき悪くって」「sudo -u postgres createuser --interactiveみたいに、名前にpgが入っていないcreateuserとかがシステム系コマンドっぽく見えてしまうのも残念」
「正直、MySQLのコマンド体系の方が親切だった分、PostgreSQLの普及が遅れた気がします」「まあ慣れればぽすぐれの方がコマンド短いから入力速いし: 結局ググるけどな!」

JavaScript

GrimmerとReactを公平に比較してみた

<!--元記事より-->
<button onclick={{action setRandomAnimal}}>Set Random Animal</button>
<ul>
  {{#each randomAnimals key="@index" as |animal| }}
    <li>{{animal}}</li>
  {{/each}}
</ul>
// 元記事より
import Component, { tracked } from '@glimmer/component';

const animals = ["Cat", "Dog", "Rabbit"];

export default class extends Component {
  @tracked randomAnimals = [];

  setRandomAnimal() {
    const animal = animals[Math.floor(Math.random() * 3)];

    this.randomAnimals = this.randomAnimals.concat(animal);
  }
}

つっつきボイス: 「GrimmerだとVue.jsっぽく書けるのか: コンパクトなのはちょっとよさそう」

npmパッケージを自作する(JavaScript Liveより)

{
  "name": "masks-js",
  "version": "0.0.1",
  "description": "A NPM package that exports functions to mask values.",
  "main": "build/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/brunokrebs/masks-js.git"
  },
  "keywords": [
    "npm",
    "node",
    "masks",
    "javascript"
  ],
  "author": "Bruno Krebs",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/brunokrebs/masks-js/issues"
  },
  "homepage": "https://github.com/brunokrebs/masks-js#readme"
}

つっつきボイス: 「npm作るコマンドとかあるのでgem作るのと同じぐらいの感覚: ↑こういうdescription書くのが面倒だけど」「gemspecもいろいろ書かないといけないですしね」

CSS/HTML/フロントエンド

HTTP Early HintsがRFC 8297に

Early Hintsの生みの親のkazuhoさん自らのツイートです。心なしかアバターが微笑んで見えます。

アクセシビリティのテストツール

aXepa11yGoogleChrome accessibility-developer-toolsなどを紹介しています。

子要素にフォーカスしたまま親要素を表示する

See the Pen :focus-within helpful a11y thing by Chris Coyier (@chriscoyier) on CodePen.

フロントエンドテクの紹介記事です。上のCodePenでマウスオーバーするとわかります。

Firefox Quantumが速くなった理由


hacks.mozilla.orgより


つっつきボイス: 「FirefoxというかMozilla組の追い上げ半端ないですね」「Mozilla Foundationそのものは緩く統括しているだけですけどね」

参考: 爆速進化したブラウザ「Firefox Quantum」は何がどう変化したのか?

その他

GeForce/TITANのデータセンター利用について

来年2月にChromeに広告ブロック機能を実装


リモートつっつきボイス: 「Googleが広告ブロック機能を提供しちゃうのか…」

CVEは誰でも出せる

番外

英米の名門校がずらり

体重も測れる超小型計測センサー

もしかしてエネルギー問題解決?

10平方ミクロン程度のグラフェン(原子一個分の薄さの黒鉛)から、腕時計を駆動できるほどの電力が得られる可能性があるそうです。

なお、グラフェンの製造が難しくて研究が遅れていたのが、あるとき黒鉛にセロテープを貼って引っぺがすだけで簡単に作れることがわかって一気に研究が進んだそうです。

参考: 驚異の素材グラフェンの「ゆらぎ」が、無尽蔵のクリーンエネルギーを生むかもしれない:米研究結果

これは凄い


つっつきボイス: 「おおぉ」「MIDIキーボードって実はこういうことするのにとても向いていますね」「物理コントローラが豊富にあるし」「MIDIもプロトコルとして成熟してるし」

藁で作った創作動物

https://layer13.tumblr.com/post/168746265269/ithelpstodream-in-northern-japan-the-wara-art


今週は以上です。

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

週刊Railsウォッチ(20171215)Ruby 2.5.0-rc1リリース、Ruby 2.4.3セキュリティ修正、Ruby 3.0で変わるキーワード引数、HTML 5.2 RECリリースほか

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

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

Rails公式ニュース

Ruby Weekly

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Railsのルーティングで多数のHTTP OPTIONSをうまく扱う方法(翻訳)

$
0
0

概要

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

: 本記事のようにルーティングでアスタリスク*を使うと、rails routesの出力ですべてのURLの網羅が保証されなくなります*の部分は展開なしで出力される)。大規模案件などでルーティングの見落としなどにつながる可能性もあるため、採用の際は今後の改修上のリスクなどの考慮が必要と考えられます。

Railsのルーティングで多数のHTTP OPTIONSをうまく扱う方法(翻訳)

Photo by Natalia Y on Unsplash

RailsのAPIセットアップ周りがいかに強力であっても、OPTIONSHEADといった他のHTTPメソッドのハンドラを実装しようとすると困ってしまいます。ルーティングするエンドポイントごとにこういうあまり使われないHTTPメソッドを指定するときは特にそうです。たとえばRailsのルーティングにsessionsというAPIリソースがあるとします。

namespace :api do
  namespace :v1 do
    resources :sessions
  end
end

Railsでは以下のルーティングが定義されます(ルート名やヘルパに応じて)。

GET     /api/v1/sessions(.:format)
POST    /api/v1/sessions(.:format)
GET     /api/v1/sessions/:id(.:format)
PATCH   /api/v1/sessions/:id(.:format)
PUT     /api/v1/sessions/:id(.:format)
DELETE  /api/v1/sessions/:id(.:format)

Railsでは、これ以外のマイナーなHTTPメソッド(OPTIONSHEADなど)はデフォルトですべてスキップします。しかしそれももっともです。マイナーなHTTPメソッドはめったに使われませんし、その必要が生じるとしても1つか2つのエンドポイントぐらいしかないので、既存のRailsルーティングAPIで問題は生じません。こうしたマイナーなHTTPメソッドの追加は次のように簡単にできます。

namespace :api do
  namespace :v1 do
    resources :sessions do
      collection do
        match '', via: :options, action: 'options'
      end
    end
  end
end

上で生成されるルーティングは次のようになります。

GET     /api/v1/sessions(.:format)
POST    /api/v1/sessions(.:format)
GET     /api/v1/sessions/:id(.:format)
PATCH   /api/v1/sessions/:id(.:format)
PUT     /api/v1/sessions/:id(.:format)
DELETE  /api/v1/sessions/:id(.:format)
OPTIONS /api/v1/sessions(.:format)

しかし、すべてのルーティングでHTTP OPTIONSHEADなどのハンドラが欲しいときはどうするのでしょうか?ルーティングひとつひとつにOPTIONSエンドポイントを追加して回るなど、とうていやってられません。

私たちの場合、クライアントへの通知用にAPIのリクエスト/レスポンスのJSON(hyper)schemaをHTTP OPTIONSで公開したいと思っていました。私たちが公開したいこの同じスキーマは、APIのテストにも使われるので、スキーマは(ドキュメントよりも)常に更新されます。

インターフェイスAPIを設計する

私はインターフェイスを新しく設計する場合、常にインターフェイスのAPIを最初に設計します。どんなAPIが理想的でしょうか。

class Api::V1::UsersController < ApplicationController
  before_action :authenticate_user!

  def create
    # POST /api/v1/usersのレスポンスハンドラ
  end

  def show
    # GET /api/v1/users/:idのレスポンスハンドラ
  end

  options do
   # OPTIONS /api/v1/usersのオプション本体を公開する
   # OPTIONS /api/v1/users/:idのオプション本体を公開する
  end
end

ブロックを受け取るクラスメソッドが1つあればよさそうです。最終的にそのメソッドがほとんどの静的データを扱うことになります。ブロック内部では、collection(/users)とresource(/users/:id)のルーティングを区別できる必要があります。

options do
  if is_index?
    # OPTIONS /api/v1/usersのオプション本体を公開する
  else
    # OPTIONS /api/v1/users/:idのオプション本体を公開する
  end
end

インターフェイスAPIを実装する

ありがたいことに、Railsには制限を加えながらすべてのルーティングにマッチさせる方法が用意されています。この場合、OPTIONSのHTTPメソッドを持つすべてのルートにマッチさせたいと思います。これを行うには、Railsルートに以下を追加するだけです。

match '*path', {
    controller: 'application',
    action: 'options',
    constraints: { method: 'OPTIONS' },
    via: [:options]
  }

次にapplicationコントローラで、このルーティングを扱うメソッドを定義し、URLに応じて適切なcontroller#optionsに委譲する必要があります。

def options
  # 適切なcontroller#optionsブロックに委譲する
end

今回の作業で最も面倒なのは、URLで示されたリソースをURLに基いてどのコントローラで扱うかを特定する部分です。Railsのrecognize_pathでは、関連付けられるメソッドを特定するのに2つのパラメータ(URLと、URLで使う実際のHTTPメソッド)が必要だからです。しかしこの場合は異なるHTTPメソッド(OPTIONS)を使うので、それなりに試行錯誤が必要です。

def route_details_for(url)
  [@route_details](http://twitter.com/route_details "Twitter profile for @route_details") ||= begin
    methods = [:get, :post, :put, :patch, :delete]
    method = methods[0]
    tries ||= 0
    route_details = nil
    begin
      route_details = Rails.application.routes.recognize_path(url, method: method)
      raise ActionController::RoutingError, '' if route_details[:action] == 'route_not_found'
      rescue ActionController::RoutingError => _
      method = methods[tries]
      retry unless (tries += 1) == 5
    else
      return route_details
    end
  end
end

試行錯誤の結果はそこそこシンプルになりました。URLを受け取り、それに関連付けられるコントローラとメソッドの情報をrecognize_pathで得られるまでループします。

その結果、{:controller => "api/v1/searches", :action => "create" }という形になり、関連するコントローラに変換して定数化しやすくなりました。

def controller_for(url:)
  route_details = route_details_for(url)
  name = route_details[:controller].titleize.gsub('/', '::').gsub(' ','')
  return "#{name}Controller".constantize
end

当初のAPI設計の実装の最後の部分は、関連付けられるコントローラ(controller_forで見つかります)からのオプションブロックを呼び出すことです。コントローラのこのメソッドをそのまま呼び出すこともできますが、より望ましいのはブロックのコンテキストを変更することです。それなら、ブロック内部のparamsなどに基いてリクエストコンテキストにアクセスできるようになります。これはRubyのBasicObjectメソッドであるinstance_execを使ってできます。これは、呼び出し元のコンテキストでlambdaprocやブロックを呼び出すことができます。このささやかなマジックについて詳しくはこちらをご覧ください。

def options
  return head :ok if controller_for(url: request.url).options.nil?

  return render({
    json: instance_exec(
      route_details_for(request.url),
      &controller_for(url: request.url).options
    )
  })
end

ここでは、recognize_pathから得たroute_detailsもパラメータとして渡していますが、これはブロックで必要になった場合に便利だと思って念のため渡しただけです。最終的に、当初の設計に極めて近い結果が得られました。

options do |route_details|
  if route_details.dig(:id)
    # /api/v1/usersのJSON (Hyper) schemaを公開する
  else
    # /api/v1/users/:id\のJSON (Hyper) schemaを公開する
  end
end

gemのバージョンについて

私たちのアプリでコンポーネント間コミュニケーション方法としてAPIが標準になるに連れて、このパターンがよく使われるようになりました。そこでこれをrails_http_optionsというgemにまとめました。

次の3つの手順で、好みのメタデータ(スキーマなど)をクライアントに公開できます。

  1. ApplicationControllerrails_http_optionsincludeします。これによって、HTTP OPTIONSリクエストの扱いを想定したoptionというpublicメソッドが追加され、その他にprivateメソッドもいくつか追加されます。
  2. 使いたいHTTP OPTIONSをrails_http_optionsで扱えるルーティングを追加します(必要ならconstrainsをもっと厳しくすることもできます)。
match '*path', {
  controller: 'application',
  action: 'options',
  constraints: { method: 'OPTIONS' },
  via: [:options]
}
  1. HTTP OPTIONSが意味のあるbodyを持つresourceにoptionsブロックを追加します。
options do
  {
    schemas: {
      accepts: Company.json_schema,
      returns: Company.json_schema
    },
    meta: { max_per_page: 100 }
  }
end

ブロック内では、リクエストされたリソースを指す関連スキーマを見つける方法を特定する必要があります。通常は、モデルのクラスメソッドやService Objectを使えばよいでしょう:)

関連記事

Railsのルーティングを極める(前編)

Railsのルーティングを極める (後編)

Railsのsecret_key_baseを理解する(翻訳)

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

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

$
0
0

概要

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

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

前書き

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

Part 2Part 3もご覧ください。

混乱しがちな部分

今新たに実地のRailsフルスタック開発者になるということは、混乱の渦中で頑張るということです。Asset Pipeline/Sprockets/CoffeeScript/Sassを用いてフロントエンドを扱う「昔ながらのRails」wayは、2017年ともなると色褪せて見えます。Rails 3.1時代に選択された技術の多くは、現代の想定に応えられていません。昔ながらの手法にこだわり続ければ、この5年間にフロントエンド界隈で起こったあらゆる新技術を見送ることになってしまいます。全てを統べるJavaScriptパッケージマネージャnpmの台頭、頼もしいJavaScript文法であるES6の隆盛、連勝を重ねるBabelトランスパイラビルドツールかつてないほど成長を続ける CSSプリプロセッサPostCSS。フロントエンドのコードを「ページ」から「コンポーネント」にパラダイム変換したReactやVueなどのフロントエンドフレームワークが驚異的な成功を収めていることは申し上げるまでもありません。

これほどまでに複雑になったフロントエンド技術を1人の開発者の頭に押し込めようとすれば(キャリアの浅い人はなおさら)、認知機能が悲鳴を上げていわゆるJavaScript疲労になるのがオチです。

しかし、確立されたワークフローに疑問を感じるのは、時代に取り残された気持ちになるからとか、フロントエンド屋との技術トークがどんどん通じなくなるからとか、将来の見通しに不安を感じるからという理由だけではありません。つまるところ、プログラマーとは理性的な人間なのです。

Asset Pipelineの何が問題だったのか

昔ながらの方法が今も使えることについては触れないことにしましょう。今でも、Rails標準のフロントエンド向けセットアップ(とCoffeeScript)を使って成果を出せます。ビューテンプレート、スクリプト、スタイルシートは従来同様Asset Pipelineで結合/最小化されて配信されます。本番環境では2つの(少なくとも人間には)読めないファイルの形を取るので、重要性は変わりません。

しかし、開発者は普通次のような点に心を配っています。

  • 分離され、再利用可能かつテスト可能な、理解しやすいコードであること
  • コードの変更->結果の表示を短いサイクルで繰り返せること
  • 依存関係の管理が面倒でないこと
  • ツールがちゃんとメンテされていること

言うまでもなく、「昔ながらの」Railsは私たちのコードに何らかの構造を与えます。ビューテンプレート、JavaScript、スタイルシート、画像ごとに個別のフォルダが用意されます。しかしフロントエンドが複雑になればなるほど、これらを追っているうちに認知能力が低下(訳注: 参考)してしまいます。

「昔ながらの」フルスタックRails wayをおそろかにすれば、たちまちCSSやJSは死んだコードのグローバルなゴミ捨て場と化してしまうでしょう。

乗り換えを検討する理由の中には「スピード」もあります。Sprocketが遅いという問題については山ほどドキュメントがありますし、Herokuにいたっては、Asset Pipelineのパフォーマンス最適化方法に特化したガイドまで公開しています。そこではRailsアプリのデプロイで最も時間がかかるのがアセットの扱いであることを認めています。曰く「平均すると、依存関係をbundle installでインストールするときよりも20倍以上遅い」。

開発中にCSSを1行変更してページを再読み込みするときも、結果が表示されるまでに多少待たされます。この待ち時間はすぐに長くなります。

依存関係についてはどうでしょうか。Asset Pipelineを常に最新に保つのは大仕事です。プロジェクトにJavaScriptライブラリを1つ追加する場合、CDNから読み込んだコードをコピペしてapp/assetslib/assetsvendor/assetsに置くか、誰かがgem化してくれるまでぼんやり待つ方法があります。その間にも、JavaScriptコミュニティは同じことをnpm installコマンド、今ならyarn addコマンド一発で管理しています。アップデートも同様です。YarnはJavaScriptをBundlerのように便利に扱うことができます。

最後は、Asset Pipelineを支えるビルドツールであるSprocketsです。Sprocketsの最近のメンテ状況ははかばかしくありません。

Sprocketsはかれこれ5年以上ほこりを被ったままです(左)。同じ時期のWebpackは頻繁に更新されています(右)。

風向きが変わった

2017年、DHHとRailsコミュニティはフロント周りの変更に手を付け始めました。Rails 5.1ではwebpacker gemによるWebpack統合、Yarnを介したnode_modules、すぐに使えるBabel/React/Vue/PostCSSが(その気になればElmも)導入されました

しかしAsset PipelineとCoffeeScriptは今も彼らがメンテしています。rails newだけでプロジェクトを開始すると、昔ながらのRailsになります。JavaScript関連のトピックをググるときには、相変わらずコード例を脳内トランスパイルして隅々まで理解しなければなりません。

くよくよすることはありません。今日のRailsアプリではあらゆる現代的な手法を利用可能です。本シリーズで私たちと一緒に基礎を固めましょう。Rails/JavaScript/CSSの基本的な知識が多少あれば十分始められます。本シリーズでは設定やツールを最小限に抑えるため、最新のRails 5.1以降の機能も積極的に使います。

本チュートリアルのシリーズでは、Evil Martiansで培われた現代的で練り上げられたフロントエンド構築ベストプラクティスの一部を皆さまにご紹介いたします。

心の壁

Reactは私たちにコンポーネントで思考するよう指導します。その他の現代的なフロントエンドフレームワークもこれに準じています。モジュラリティは、BEMをはじめとする主なCSS方法論を支える哲学です。モジュラリティのコンセプトは「UIを構成するあらゆる論理的な部品は自己完結(self-contained)すべきである」というシンプルなものです。

Railsでは、ビューを論理的な部品に分割する方法が組み込まれています(ビューのパーシャル)。しかしパーシャルがJavaScriptに依存すると、おそらく他の現代的なコンポーネントと同様にapp/assets/javascriptsの下にある深いフォルダにアクセスしなければならなくなります。

パーシャルを使うときに一切合財をまとめることができれば、各パーシャルのスクリプトやスタイルシートを1箇所にまとめられるのではないでしょうか?

これからご紹介するアプローチは、React/Vue/Elmのアーキテクチャに依存していません。そしてそのように作られています。自分が使うツールの利用法を気兼ねなく学ぶことができますが、急いで学ぶ必要はありません。既にRailsで使えるツールを現代的なフロントセットの思考様式に徐々に合わせていけばよいのです。

Sass vs. PostCSS

SassはRailsに愛されています。しかし私たちの心はPostCSSに傾きつつあります。何より、PostCSSはRailsでCSSを処理するRuby組み込みのSassより36.4倍高速です。PostCSSは100%純粋なJavaScriptで書かれており、多くのプラグインを使って簡単に拡張/カスタマイズできます。cssnextというプラグインは、ブラウザでサポートされていない機能のポリフィルをすぐ使うことができますが、必要がなければ使わなくてよいのです。理由があれば、好みのプリプロセッサの上でPostCSSを使うこともできます。

私たちは何を作っているのか?

そろそろ実際に手を動かさないといけませんね。フロントエンドの新しいアプローチをご紹介するために、最小限の認証とActionCableを用いた何の変哲もない標準的なチャットアプリを作ることにします。アプリ名はevil_chatとしました。このサンプルアプリは複雑ではありませんが、それでも「フルスタック」の経験に十分なぐらいに洗練されています。

私たちのプロジェクトでは、Asset PipelineやデフォルトのRailsジェネレータが生成する大量の.scssファイルや.coffeeファイルたちとおさらばすることにします。テンプレートエンジンは従来どおりERBを使い、好みに応じてSlimやHamlを使える余地を残しています。

A new frontend folder in your app

左に見えるのがEvil Frontのフォルダ構造です

後でまたこのフォルダ構造を振り返ります。アプリのトップレベルにあるfrontendフォルダの中ですべてのことを行います。app/assetsは完全に置き換えられます。

一発ですべて理解できなくても問題ありません。ひとつずつ手順を追って進めましょう。

プロジェクトの開始方法

rails newだけでは不要なものを切り落とせないので、次の新しいマジックコマンドを使います(アプリ名はevil_chatとします)。

$ rails new evil_chat --skip-coffee --skip-sprockets --skip-turbolinks --webpack --database=postgresql -T

ご覧のとおり、CoffeeScriptやSprockets関連の機能は不要になります。本チュートリアルではテスティングまではカバーしないので、-Tオプションでテストファイル作成をスキップしています。作成後Herokuにすぐデプロイできるよう、--database=postgresqlでPostgreSQLを指定しています。

一番肝心なのは--webpackオプションです。これを指定することで、Webpackですべてのアセットをバンドルするwebpacker gemがRailsで使われるようになります。

  • node_modulesフォルダにはJS依存ファイルがすべて含まれます(誤って余分なファイルを大量にリポジトリにコミットしないよう、.gitignoreにも追記されます)。
  • package.jsonはすべての依存関係を宣言します。yarn.lockも同様なので、npm installではなく(より機能豊富な)yarn addでパッケージを追加できます。
  • .babelrcファイルは、ES6を、マーケットシェア1%以上のあらゆるブラウザに準拠するJavaScriptコードに変換します。
  • .postcssrc.ymlpostcss-smart-importプラグインやpostcss-cssnextプラグインで設定済みです。これらのプラグインのおかげで、cssnextに記載されている全機能を利用できます。

まだ何か忘れているような気がします。特に、Autoprefixerなどのツールのbrowserslistグローバル設定は、今後コードを正しくクロスブラウザ互換処理するうえで必要です。ありがたいことに、プロジェクトのルートディレクトリに以下のファイルを作成すれば簡単に修正できます。

$ touch .browserslistrc

おかげで要求される知識が随分少なくなりました(まったくというわけにはいきませんが)。

それではこのファイルを開いて> 1%という1行を追加しましょう。これさえ知っておけばブラウザの互換性を保てます。

最初の段階で正しくやっておきたいことがもうひとつあります。Railsジェネレータのデフォルトの振る舞いの再設定です。既にネタバレしていますが、app/assetsには今後何も置く必要がないので、以下の手順でこのフォルダを削除します。application.rbを開いて以下の行を追記します。

# config/application.rb
config.generators do |g|
  g.test_framework  false
  g.stylesheets     false
  g.javascripts     false
  g.helper          false
  g.channel         assets: false
end

Asset Pipelineに引導を渡すときが来ました。app/assetsフォルダを削除します。

しかし置き換え方法はどうすればよいのでしょうか。次の手順を実行します。

  • rails new--webpackオプションを追加したことでapp/javascriptというフォルダが作成されています。このフォルダをプロジェクトのルートに移動してフォルダ名をfrontendに変更します(名前は好みで構いませんが、frontendが一番わかりやすいと思います)。移動の際フォルダの中身は変更しないようにします。frontend/packsの中にあるapplication.jsは、このアプリのWebpack「エントリ」ポイントとして使われます。
  • application.html.erbを開き、javascript_include_tag "application"javascript_pack_tag "application"に置き換えます。メソッド名の単語が1つ変わることですべての違いが生じます。include_tagは、SproketsでコンパイルされたアプリレベルのJavaScriptファイルに参照を1つ挿入する昔ながらの方法ですが、pack_tagは先のエントリポイント(つまりfrontend/packs/application.js)から生成されたWebpackバンドルが使える新しい方法です。この段階で、<head>にあるpack_tag<body>の末尾、つまりyieldステートメントの直前に移動します。

  • stylesheet_link_tag 'application', media: 'all'stylesheet_pack_tag 'application'に置き換えます。WebpackとES6のimportステートメントの助けを借りて、今後はCSSをコンポーネント単位で使うことにします。これにより、CSSもすべてWebpackで管理されます。

  • 次に、バンドルするファイルを探索する場所をWebpackerに指定する必要があります(デフォルトのフォルダをリネームしたので)。Webpacker 3.0によると、Railsのconfigフォルダ内にあるwebpacker.ymlで設定できます。このファイルの最初の数行に、以下の要領でアプリのフォルダ構造の変更を反映します。

default: &default
  source_path: frontend
  source_entry_path: packs
  public_output_path: packs
  cache_path: tmp/cache/webpacker
  • ERBのパーシャルもfrontendフォルダ配下に置かれるので、application_controller.rbで次のように指定しないとコントローラから見つけられません。
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  # 以下を書くだけでよい
  prepend_view_path Rails.root.join("frontend")
end
  • Webpackerが3.0になったことで、開発中にオンデマンドでアセットをコンパイルするための別プロセスが不要になりました。しかしJSコードやCSSコードの変更時にページを自動更新するには、rails sするときに従来同様webpacker-dev-serverを実行しておく必要があります。そのためのProcfileが必要なので作成しましょう。
$ touch Procfile

このファイルに以下の設定を書きます。

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

このProcfileを置くことで、Foremanなどのツールを使ってすべてのプロセスをコマンド一発で起動できるようになりますが、Evil MartiansのHivemindをぜひおすすめいたします。Hivemindの兄貴分であるOvermindは実行中のプロセスを一時停止せずにpryでデバッグできるツールですので、こちらもご覧ください。

訳注: 同社は昔のSFになぞらえた命名を好んでいるようです。Hivemindは集合精神、Overmindは「幼年期の終わり」に登場する神っぽい宇宙規模の集合知性を指します。

スモークテスト

それでは新しいセットアップが正しいかどうかテストしましょう。application.jspacksの下にあります)にシンプルなDOM操作コードを少し足し、Webpackerでちゃんと動くようにします。最初に、基本となるコントローラとデフォルトルーティングをそれぞれ生成する必要があります。

$ rails g controller pages home
# config/routes.rb
Rails.application.routes.draw do
  root to: "pages#home"
end

views/pages/home.html.erbの中身は完全にからっぽにしてください。次にapplication.jsの中身もからっぽにし、以下のコードに置き換えます。

// frontend/packs/application.js
import "./application.css";

document.body.insertAdjacentHTML("afterbegin", "Webpacker works!");

同じフォルダにapplication.cssファイルを作成し、CSSも効いていることを確認できるようにします(CSSはPostCSSで処理されます)。

/* frontend/packs/application.css */
html, body {
  background: lightyellow;
}

いよいよアプリの初立ち上げです。Hivemindをインストール済みであることが前提です。インストールしない場合はforemanなどのプロセス管理ツールをお使いください(私たちとしてはぜひHivemindの素晴らしさをご検討いただければと思います)。

$ hivemind

ではhttp://localhost:5000を開きましょう(Hivemindは$PORTに5000を設定するので、Railsも同じ環境変数で実行ポートを決定します)。次のように表示されるはずです。

A smoke test for our app

真っ赤なエラー画面が出ていなければ成功です

ここでWebpackのクールな点を1つご紹介します。application.jsファイルで"Webpacker works!"の部分を変更して保存すると、ブラウザの[更新]ボタンを押さなくても画面が自動更新されます。

実際のコードを書き始める前に、コーディングスタイルを定めましょう。

ところでJSのlintはどうする?

Prettierには著名なエディタがすべて統合されているので、ボタンひとつでコードを整形できます。ESLintにもあらゆる主要なエディタ向けのプラグインがあるので、即座に結果をビジュアル表示できます。

JavaScriptの文法は年単位で更新を繰り返すので、コーディングスタイルは多岐にわたり、書き始める前からスタイルが混乱しがちです。たとえばセミコロン使う派と使わない派の争いは終わりそうにありません。StandardPrettierのように、独自の色を出すコードフォーマッタもあります。私たちはPrettierにしました(Prettierはデフォルトでセミコロンを使いますが、いつでもオフにできます)。

lintはESLintである程度自動化するので、コーディングスタイルは常にチェックされます。また、メンテしやすいJSコードを書くベストプラクティスを豊富に収録しているAirbnbのJavaScriptスタイルガイドにも頼ることにします。

訳注: AirbnbのJavaScriptスタイルガイド日本語版は以下をどうぞ

JavaScriptスタイルガイド 1〜8: 型、参照、オブジェクト、配列、関数ほか (翻訳)

現時点ではwebpack-dev-serverしか含まれていないpackage.jsondevDependenciesを少々追加します。JavaScriptのlintに必要な部分をカバーすると、以下のような感じになるはずです。

{
  "name": "evil_chat_codealong",
  "private": true,
  "dependencies": {
    "@rails/webpacker": "^3.0.1"
  },
  "devDependencies": {
    "webpack-dev-server": "^2.9.1",
    "babel-eslint": "^8.0.1",
    "eslint": "^4.8.0",
    "eslint-config-airbnb-base": "^12.0.1",
    "eslint-config-prettier": "^2.6.0",
    "eslint-import-resolver-webpack": "^0.8.3",
    "eslint-plugin-import": "^2.7.0",
    "eslint-plugin-prettier": "^2.3.1",
    "lint-staged": "^4.2.3",
    "pre-commit": "^1.2.2",
    "prettier": "^1.7.3"
  }
}

lint-stagedpre-commitは、後でgit addgit commitなどにフックするときに便利です。これは、残念なコードを誤ってリポジトリに登録しないようにするためのものです。

最後の仕上げに、ルートフォルダに.eslintrcファイルが必要です。適用するルールをESLintに指示するのに使います。

$ touch .eslintrc

.eslintrcに以下を書きます。

{
  "extends": ["eslint-config-airbnb-base", "prettier"],

  "plugins": ["prettier"],

  "env": {
    "browser": true
  },

  "rules": {
    "prettier/prettier": "error"
  },

  "parser": "babel-eslint",

  "settings": {
    "import/resolver": {
      "webpack": {
        "config": {
          "resolve": {
            "modules": ["frontend", "node_modules"]
          }
        }
      }
    }
  }
}

"extends"キー以下の要素の順序は重要です。ここで、最初にAirbnbのルールを適用し、Prettierのフォーマットガイドとコンフリクトするものがあれば常に最新のものを使うようESLintに指示しています。eslint-import-resolver-webpackへの依存を指定する"import/resolver"キーも必要です。これによって、JSファイル内のimportで指定したライブラリが、Webpackが管理するフォルダ(このアプリの場合はfrontendフォルダ)内に実際に存在するようになります。

CSSはどうする?

CSSにも何かlintツールが必要です。評判のよいツールであるnormalize.cssをnormalizeに使うことにします。CSSのエラーやスタイル違反の検出用にstylelintも使います。package.jsonに開発用の依存ライブラリを2つ追加しましょう。

"devDependencies": {
    ...
    "stylelint": "^8.1.1",
    "stylelint-config-standard": "^17.0.0"
  }

プロジェクトのルートフォルダに.stylelintrcを置いてlinterに指示する必要もあります。

$ touch .stylelintrc

ファイルの中身は次のとおりです。

{
  "extends": "stylelint-config-standard"
}

package.json(今度はdevDevdependenciesではありません!)の"dependencies"キーの配下に次のようにnormalize.cssを追加します。

"dependencies": {
    "@rails/webpacker": "^3.0.1",
    "normalize.css": "^7.0.0"
  },
  ...

次はgit hooksをいくつか導入し、git commit時に自動チェックが走るようにしましょう。package.json"scripts"を追加します。

...
"scripts": {
    "lint-staged": "$(yarn bin)/lint-staged"
  },
  "lint-staged": {
    "config/webpack/**/*.js": [
      "prettier --write",
      "eslint",
      "git add"
    ],
    "frontend/**/*.js": [
      "prettier --write",
      "eslint",
      "git add"
    ],
    "frontend/**/*.css": [
      "prettier --write",
      "stylelint --fix",
      "git add"
    ]
  },
  "pre-commit": [
    "lint-staged"
  ],
  ...

これで、コミット時にstagedファイルのエラーがすべてチェックされ、自動整形されます。

最終的なpackage.jsongistにあるような形になるはずです。ターミナルでyarnを実行して依存ライブラリをインストールします。

自動lintを早く使ってみたくてウズウズしているかと思います。frontend/packs/application.jsを開いてセミコロンを削除してから、git add . && git commit -m "testing JS linting"を実行すると、ただちにセミコロンが復元されます。だらしないコーディングスタイルともこれでお別れです。

Linterの設定

設定がすべて正しければ、プロジェクトのルートファイルにこれらのファイルがすべてあるはずです

最初のコンポーネント(React未使用)

本ガイドのPart 2で扱う内容の一部を軽くご紹介します。最初のコンポーネントを作ってみましょう。

最初にapplication.cssを削除します。これはスモークテスト以外では不要です。同様にapplication.jsのコードもすべて削除します。ここからは、application.jsにはimportステートメントだけを書きます。このエントリポイントにすべてが集約されます。アプリ全体で使うCSSやJavaScriptの置き場所が必要なので、作ってみましょう。このフォルダの名前はinitにしました。

$ mkdir frontend/init
$ touch frontend/init/index.js
$ touch frontend/init/index.css

続いてエントリポイント内部の新しいフォルダの登録が必要です。packs/application.jsに以下を追加します。

// frontend/packs/application.js
import "init";

新しいファイルに入れるコードも必要です。以下はinit/index.jsです。

// frontend/init/index.js
import "./index.css";

以下はinit/index.cssです。

/* frontend/init/index.css */
@import "normalize.css/normalize.css";

body {
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 16px;
  line-height: 24px;
}

ここでは、アプリのすべてのフォントに一般的なスタイルをいくつか適用しています。initフォルダはバンドル時に最初にチェックされる場所なので、ここにnormalize.cssをインクルードするのが自然です。同じフォルダを使って、後でポリフィルやエラー監視など、私たちのコンポーネントに直接関係のない機能をセットアップして必要が生じたら可能な限り読み込むこともできます。

initは特殊ケースです。このコンポーネントについてはどうでしょうか。

各コンポーネントは、3つのファイル(ERBパーシャルとそれ用のJS/CSSファイル)を含む1つのフォルダで構成されます。

私たちのコンポーネントはすべて、frontend配下のcomponentsフォルダ内に置かれます。ここに最初のコンポーネントを作成しましょう。レイアウトのテンプレートとみなしてこのコンポーネントをpageと呼ぶことにします。

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

私たちがこのコンポーネントのJSファイルをindex.jsと呼んでいないことにご注意ください。この名前はinitフォルダで予約されているからです。後でエディタでタブをいくつも開いたときに見つけやすいよう、JSファイルにはコンポーネントと同じ名前を付けることにしています。他のチュートリアルではindex.jsを使っていることが多いので、この手法はあまり見かけないかもしれませんが、後でコードを書くときに時間を大きく節約できます。

まだ私たちのコンポーネントに関連するJSロジックがないので、page.jsにはCSSファイルをimportする行しかありません。

// frontend/components/page/page.js
import "./page.css";

page.cssの方にはコンポーネントに関連するスタイルがあります。

/* frontend/components/page/page.css */
.page {
  height: 100vh;
  width: 700px;
  margin: 0 auto;
  overflow: hidden;
}

最後の_page.html.erbにはマークアップが含まれています。ERBや、コンポーネント同士をネストするのに使うyieldステートメントもすべて使える点にご注目ください。

<!-- frontend/components/page/_page.html.erb -->
<div class="page">
  <%= yield %>
</div>

application.jsimport "components/page/page";を追加して、新しいコンポーネントを参照できるようにするのをお忘れなく。

A structure for the first component

この時点でのfrontendフォルダ構造

それではhome.html.erbビューにERBコードを書いてみましょう。

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

最初のコンポーネントが実際に動作することを確認しましょう。サーバーを再び起動してページを更新します。うまく行けば、以下のように表示されるでしょう。

A structure for the first component

最初のコンポーネントのブラウザ表示とコンソール出力

おめでとうございます。チュートリアルPart 1はこれでおしまいです。ぜひPart 2もご覧いただき、アプリの体裁を整えてチャット関連の機能に必要なコンポーネントを導入するところまでやってみてください。少ないコード量でコンポーネントをレンダリングするヘルパーや、コンポーネント作成を自動化するジェネレータも追加します。


Part 1 | Part 2 | Part 3

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

関連記事

Rails 5.2ベータがリリース!内容をざっくりチェックしました

5.1 beta1リリースノートに見るRails 5.1の姿

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

$
0
0

概要

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

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

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

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

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

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

PostgreSQL拡張を有効にする

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

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

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

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

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

以後のマイグレーション

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

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

UUIDを使う理由

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

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

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

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

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

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

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

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

関連記事

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

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

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

$
0
0

概要

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

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

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

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

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

class Order
  def total
    subtotal + customer.order_fee
  end
end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

class Order
  def total
    subtotal + customer.fee
  end
end

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

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

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

stubを使う意味がある場合

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

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

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

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

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

テストを高速にするstub

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

まとめ

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

関連記事

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

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

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

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

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

$
0
0

概要

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

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

前書き

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

Part 1のおさらい

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

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

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

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

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

アプリを現実に近づける

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

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

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

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

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

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

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

  alias :c :component
end

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

$ rails g controller auth
$ rails g controller chat

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

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

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

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

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

  def new; end

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

  private

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

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

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

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

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

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

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

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

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

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


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

参考: CSS Modules所感

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  private

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

最後はCSSです。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

チャットウィンドウ

チャットウィンドウ

おまけ

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

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

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

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

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

  protected

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

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

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

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


Part 1 | Part 2 | Part 3

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

関連記事

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

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

$
0
0

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

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

Rails: 今週の改修

Ruby 2.5をCIに追加

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

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

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

PostgreSQLでbulk_change_tableをサポート

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

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

MiniMagickでcombine_optionsをサポート

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  attr_accessor :history

  def initialize
    @history = []
  end

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

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

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

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

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

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

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

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

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

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

参考: Wikipedia-ja: UTF-16

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

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

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

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

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

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

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


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


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

Rails

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

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

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

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

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

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

Railsのsystem testをRSpecから使う

# 同記事より
require 'rails_helper'

describe 'Homepage' do
  before do
    driven_by :selenium_chrome_headless
  end

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

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

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

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

$ gem install whatson
$ rubyconf
Upcoming Ruby Conferences:

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

    More @ github.com/planetruby/awesome-events

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

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


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

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


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

Rails 5.2のdeprecation情報

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

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

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


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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

Job.publishable

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


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

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


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

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

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

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

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

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

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

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

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

Ruby trunkより

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

Ruby

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


www.ruby-lang.orgより

Ruby 3とJIT(Ruby Weeklyより)

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


engineering.appfolio.comより

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

HexaPDFを使っています。


gettalong.orgより

Kernel#itselfにRubyの美学を見た

短い記事です。

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

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

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

>> CHello.new.sumThem #=> 4

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

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


blog.rebased.plより

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

JRubyより速い


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

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

Ruby実装の命名の由来

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

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


rubybench.orgより


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


rubybench.orgより

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


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

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

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

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

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

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

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

みっちり長い記事です。

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


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


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

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

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

mruby/c1.1 RC2リリース

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

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

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

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

Graphql-batchとPromise.rb

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


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

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

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

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

asakusa.rb新年会

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

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


kitchen.ciより

driver:
  name: vagrant

provisioner:
  name: chef_zero

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

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

Chefが前提のようです。

SQL

DB設計カタログサイト


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

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

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


db-engines.comより


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

JavaScript

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

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

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

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

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

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

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

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

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

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

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

via GIPHY

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

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

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

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

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


medium.com/@thamizhchelvan2005より

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

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

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

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

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

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


nhnent/tui.editorより

CSS/HTML/フロントエンド

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


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

CSSの:notセレクタを導入


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

その他

技術トークの5つのコツ


reverentgeek.comより

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

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

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

これマジで?


つっつきボイス: 「ダチョウ倶楽部」

Go 1.10 Beta2リリース

番外

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

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

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

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

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

340刷

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


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

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


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

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


今週は以上です。

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

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

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

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

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Github Trending

160928_1701_Q9dJIU


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

$
0
0

概要

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

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

前書き

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

Part 2までのおさらい

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

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

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

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

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

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

ActionCableのRuby部分

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

$ rails g channel chat

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

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

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

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

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

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

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

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

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

  private

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

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

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

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

ActionCableのJavaScript部分

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

$ yarn add actioncable

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

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

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

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

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

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

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

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

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

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

let consumer;

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

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

export default createChannel;

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

$ touch frontend/client/chat.js

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

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

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

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

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

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

export { sendMessage, setCallback };

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

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

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

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

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

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

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

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

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

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

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

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

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

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

受信したメッセージ

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

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

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

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

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

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

scrollToBottom();

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

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

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

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

Herokuにデプロイする

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

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

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

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

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

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

server: bin/rails server

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

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

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

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

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

# Rakefile
require_relative 'config/application'

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

Rails.application.load_tasks

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

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

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

development:
  adapter: async

test:
  adapter: async

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

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

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

config.secret_key_base = ENV["SECRET_KEY_BASE"]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Turbolinks

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

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

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

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

本チュートリアルからヒントを得たプロジェクトの紹介

  • Komponentは、本記事で解説した「コンポーネントベースのアプローチ」をRailsプロジェクトに取り入れやすくするgemです。このgemに含まれるジェネレーターは、frontendフォルダの作成、Webpacker configの変更、コンポーネント作成を単一のコマンドで行なえます。また、パーシャルにふさわしいテンプレートエンジンを検出したり、コンポーネントごとの「プロパティ」やヘルパーの設定に使える.rbファイルでコンポーネントを拡張したりします。

Komponent gemの作成とメンテナンスは、フランスの開発会社OuvragesEtamin Studioが、Evil Martiansとは独立に行っています。


お読みいただきありがとうございました!

本チュートリアル3部作(全貌を理解するにはすべてお読みください)では、Webpackerを完全に採り入れてアセットパイプラインを取り除き、Reactなどのフロントエンドフレームワークについて学ばずに、できるだけRailsの組み込みツールを用いて「コンポーネント」のコンセプトに基づいてRailsのフロントエンドコードを編成する方法を学びました。本チュートリアルで作ったシンプルなチャットアプリは、Evil Martiansによって現実のプロジェクトで積極的に用いられている方法でデプロイ可能です。

本チュートリアルを進めるうえで何か問題がありましたら、お気軽にGitHubのissueを開いてお知らせください。


Part 1 | Part 2 | Part 3

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

関連記事

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

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

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

$
0
0

概要

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

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

要点: サービスやコントローラなどのクラスからデータベースクエリのロジックを分離することは間違いなく優れた方法です。ロジックをモデルに置く場合、次の2とおりの方法が使えます。

1. クラスメソッド化する

def self.recent
  order(created_at: :desc)
end

2. ActiveRecordのスコープAPIを使う

scope :recent, -> { order(created_at: :desc) }

どちらにすればよいか

ActiveRecordのスコープはどっちみちクラスメソッドに変換されるので、どちらを選ぶかは見た目の問題に過ぎません。ただし、

スコープはいついかなるときでもチェイン可能である点がポイントです。

次のように、スコープの定義内に条件を含めた場合でもチェインできます。

scope :by_email, -> |email| { where(email: email) if email.present? }

クラスメソッドで同じことをした場合、メソッドをチェインできないことがあります。

def self.by_email(email)
  where(email: email) if email.present?
end

チェインできない理由は、self.by_emailemailがblankの場合にnilを返していることです。

ではどちらにすればよいか

チームの好みに合わせて決めればよいでしょう。その代わり、一度決めたらアプリ全体でその書き方を統一します。

関連記事

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

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

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

$
0
0

概要

原著者の許諾を得て、CC BY-NC-SAライセンスに基づき翻訳・公開いたします。

記事のボリュームが大きいので前編/後編に分割しました。後編は来週公開予定です。

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

Herokuライクなデプロイソリューションの構築方法を解説します。
特定のクラウドプロバイダや、Dockerと無関係なツールを必要としません。

はじめに

本チュートリアルでは、Herokuにデプロイする感覚でソフトウェアをシンプルに自動デプロイするツールの作成方法をご紹介します。デプロイごとのバージョン管理にはDockerを使い、アップグレードやロールバックをやりやすくします。また、アプリの継続的デプロイ(CD)に弊社のSemaphoreも使います。

コンテナは任意のDocker Registryにホスティングできます。アプリの実行に必要なのはDockerがインストールされているホストだけです。

チュートリアルを終えると、サンプルアプリをリモートホストにHerokuと同じようにデプロイできるシンプルなRuby CLIスクリプトが使えるようになります。直前のバージョンへのロールバック、ログの追加、実行中のアプリのバージョントラッキングを行えるコマンドもあります。

本チュートリアルはデプロイ手順を中心に据えていますので、用途に応じて環境を調整すれば任意のアプリで使えます。ここではシンプルなHello WorldをRuby on Railsでblogフォルダに構築します。Railsアプリ構築の初歩については、RailsガイドのRails をはじめようの手順1〜4をご覧ください。

必要なもの

  • Docker: ホストと、アプリをデプロイするすべてのマシンにDockerがインストールされ、動作している必要があります。
  • Docker Registryのアカウント(Docker Hubなど)
  • SSHアクセスが可能でDockerがインストールされているクラウドプロバイダ(AWS EC2など)
  • Ruby 2.3: アプリをデプロイするすべてのマシンにインストールされている必要があります。

デプロイの手順

デプロイの手順は次の5つで構成されます。

  • ビルド: いつでも変更可能なビルド手順を備えた独自のコンテナをアプリごとにビルドします。
  • アップロード: アプリのコンテナのビルドが終わったらDocker Registryに送信する必要があります。初回はコンテナ全体のアップロードが必要なので多少時間がかかりますが、次回からはDockerのレイヤシステムでサイズや帯域を節約できるので速くなります
  • 接続: Docker Registryにコンテナを送信したら、次の手順を行うためにホストに接続します。
  • ダウンロード: ホストに接続したら、コンテナをダウンロードします。
  • 再起動: 最後の手順では、アプリを停止し、続いて停止時と同じ設定(ポート、ログ、環境変数など)で新しいコンテナを起動します。

手順の概要を把握できたので、作業を開始しましょう。

1. コンテナのビルド

この例では、アプリを実行するコンテナを1つ使います(Rubyはコンパイル言語ではないのでいわゆるビルドは不要です)。この場合のDockerファイルは次のとおりです。

FROM ruby:2.3.1-slim

COPY Gemfile* /tmp/
WORKDIR /tmp

RUN gem install bundler &&\
    apt-get update &&\
    apt-get install -y build-essential libsqlite3-dev rsync nodejs &&\
    bundle install --path vendor/bundle

RUN mkdir -p /app/vendor/bundle
WORKDIR /app
RUN cp -R /tmp/vendor/bundle vendor
COPY application.tar.gz /tmp

CMD cd /tmp &&\
    tar -xzf application.tar.gz &&\
    rsync -a blog/ /app/ &&\
    cd /app &&\
    RAILS_ENV=production bundle exec rake db:migrate &&\
    RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000

スクリプトを整理するために、Dockerfileはアプリの1つ上のフォルダ階層に置きます。次のような構成になります。

.
├── Dockerfile
├── blog
│   ├── app
│   ├── bin
... (アプリのファイルやフォルダ)

Dockerfileの各行について解説します。

FROM ruby:2.3.1-slim

これはコンテナのビルドに使うベースイメージです。Rubyがインストールされている必要があるので、自分で全部インストールするよりもプレインストール済みのコンテナを使う方が楽です。

COPY Gemfile* /tmp/
WORKDIR /tmp

ここでは、GemfileとGemfile.lockをコンテナの/tmpディレクトリにコピーし、次のコマンドを実行する/tmpに移動しています。

RUN gem install bundler &&\
    apt-get update &&\
    apt-get install -y build-essential libsqlite3-dev rsync nodejs &&\
    bundle install --path vendor/bundle

このRubyイメージのbundlerは古いので、warning表示を避けるためにアップデートしています。本チュートリアルで使われているのとは別のアプリで作業する場合は、他にもいくつかのパッケージ(多くはコンパイラ)が必要になるでしょう。最後にGemfileのgemをすべてインストールします。

Dockerの各コマンドは(layerなど)、コマンドの結果が同じ場合に再実行を避けるためにキャッシュされます。これで多少時間を節約できます。--pathフラグは、すべてのgemをローカルの定義済みパス(vendor/bundle)にインストールするよう指示します。

RUN mkdir -p /app/vendor/bundle
WORKDIR /app
RUN cp -R /tmp/vendor/bundle vendor
COPY application.tar.gz /tmp

ここでは、bundlerの最終的なインストールパスを作成し、インストールされたgemを前回のビルドキャッシュからすべてコピーしてから、圧縮されたアプリをコンテナ内にコピーします。

CMD cd /tmp &&\
    tar -xzf build.tar.gz &&\
    rsync -a blog/ /app/ &&\
    cd /app &&\
    RAILS_ENV=production bundle exec rake db:migrate &&\
    RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000

このコマンドはdocker runコマンドのときに実行されます。コンテナ内部の圧縮されたアプリを展開し、セットアップ手順(migrate)を実行してアプリを起動します。

Dockerfileの設定どおりに動作していることを確認するには、Dockerfileのあるrootディレクトリに移動して次のコマンドを実行します。

: 以下のmydockeruserはDocker Registryの登録済みユーザー名です。これは後でコンテナのバージョン管理に用います。

注2: Railsをproduction環境で実行する場合は、config/secrets.ymlファイルでSECRET_KEY_BASEなどの環境変数が必要です。ここでは単なるサンプルアプリを使っているので、development環境やtest環境と同様に固定値で安全に上書きできます。

$ cp blog/Gemfile* .
$ tar -zcf application.tar.gz blog
$ docker build -t mydockeruser/application-container .

上を実行すると、Dockerfileの各手順のビルドが以下のように開始されます。

Sending build context to Docker daemon 4.386 MB
Step 1/9 : FROM ruby:2.3.1-slim
 ---> e523958caea8
Step 2/9 : COPY Gemfile* /tmp/
 ---> f103f7b71338
Removing intermediate container 78bc80c13a5d
Step 3/9 : WORKDIR /tmp
 ---> f268a864efbc
Removing intermediate container d0845585c84d
Step 4/9 : RUN gem install bundler &&     apt-get update &&     apt-get install -y build-essential libsqlite3-dev rsync nodejs &&     bundle install --path vendor/bundle
 ---> Running in dd634ea01c4c
Successfully installed bundler-1.14.6
1 gem installed
Get:1 http://security.debian.org jessie/updates InRelease [63.1 kB]
Get:2 http://security.debian.org jessie/updates/main amd64 Packages [453 kB]
...

すべて問題なく完了すると、以下の成功メッセージが表示されます。

Successfully built 6c11944c0ee4

このハッシュ値はDockerによってランダムに生成されるので、コンテナをビルドするたびに異なります。

キャッシュが効いていることを確認するために、同じコマンドを再実行してみましょう。今度はほぼ一瞬で完了します。

$ docker build -t mydockeruser/application-container .
Sending build context to Docker daemon 4.386 MB
Step 1/9 : FROM ruby:2.3.1-slim
 ---> e523958caea8
Step 2/9 : COPY Gemfile* /tmp/
 ---> Using cache
 ---> f103f7b71338
Step 3/9 : WORKDIR /tmp
 ---> Using cache
 ---> f268a864efbc
Step 4/9 : RUN gem install bundler &&     apt-get update &&     apt-get install -y build-essential libsqlite3-dev rsync nodejs &&     bundle install --path vendor/bundle
 ---> Using cache
 ---> 7e9c77e52f81
Step 5/9 : RUN mkdir -p /app/vendor/bundle
 ---> Using cache
 ---> 1387419ca6ba
Step 6/9 : WORKDIR /app
 ---> Using cache
 ---> 9741744560e2
Step 7/9 : RUN cp -R /tmp/vendor/bundle vendor
 ---> Using cache
 ---> 5467eeb53bd2
Step 8/9 : COPY application.tar.gz /tmp
 ---> Using cache
 ---> 08d525aa0168
Step 9/9 : CMD cd /tmp &&     tar -xzf application.tar.gz &&     rsync -a blog/ /app/ &&     cd /app &&     RAILS_ENV=production bundle exec rake db:migrate &&     RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000
 ---> Using cache
 ---> ce28bd7f53b6
Successfully built ce28bd7f53b6

エラーメッセージが表示されたら、Dockerfileの構文やコンソールエラーをチェックしてやり直します。

次はすべて問題ないことを確認するために、コンテナのアプリを実行できるかどうかをテストしたいと思います。以下のコマンドを実行します。

docker run -p 3000:3000 -ti mydockeruser/application-container

これはコンテナを実行し、ホストのポート番号3000をコンテナのポート番号3000にマッピングします。問題が起きなければ次のようなRails起動メッセージが表示されます。

=> Booting Puma
=> Rails 5.0.2 application starting in production on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.8.2 (ruby 2.3.1-p112), codename: Sassy Salamander
* Min threads: 5, max threads: 5
* Environment: production
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

これで、localhost:3000をブラウザで開けばWelcomeメッセージが表示されます。

2. コンテナをDocker Registryにアップロードする

Docker Registryにログインするには以下の手順が必要です。

> docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don\'t have a Docker ID, head over to https://hub.docker.com to create one.
Username: mydockeruser
Password: ########
Login Succeeded

コンテナは完全に動作するので、今度はこれをDocker Registryにアップロードする必要があります。

$ docker push mydockeruser/application-container
The push refers to a repository [docker.io/mydockeruser/application-container]
9f5e7eecca3a: Pushing [==================================================>] 352.8 kB
08ee50f4f8a7: Preparing
33e5788c35de: Pushing  2.56 kB
c3d75a5c9ca1: Pushing [>                                                  ] 1.632 MB/285.2 MB
0f94183c9ed2: Pushing [==================================================>] 9.216 kB
b58339e538fb: Waiting
317a9fa46c5b: Waiting
a9bb4f79499d: Waiting
9c81988c760c: Preparing
c5ad82f84119: Waiting
fe4c16cbf7a4: Waiting

コンテナはレイヤごとにアップロードされますが、中には巨大なものもあります(100MB以上)初回に巨大なレイヤがアップロードされるのは問題ありません。今後はDockerのレイヤシステムを用いてアプリの変更分だけをアップロードし、ディスク容量や帯域を節約します。docker pushやレイヤについて詳しくお知りになりたい方は、公式ドキュメントをご覧ください。

pushが終わると成功のメッセージが表示されます。

...
9f5e7eecca3a: Pushed
08ee50f4f8a7: Pushed
33e5788c35de: Pushed
c3d75a5c9ca1: Pushed
0f94183c9ed2: Pushed
b58339e538fb: Pushed
317a9fa46c5b: Pushed
a9bb4f79499d: Pushed
9c81988c760c: Pushed
c5ad82f84119: Pushed
fe4c16cbf7a4: Pushed
latest: digest: sha256:43214016a4921bdebf12ae9de7466174bee1afd44873d6a60b846d157986d7f7 size: 2627

Docker Registryコンソールで新しいイメージを確認できます。

イメージを再度pushしてみると、すべてのレイヤが既に存在することがわかります。Dockerは再アップロードを回避するために、各レイヤのハッシュを照合してレイヤが既にあるかどうかをチェックします。

$ docker push mydockeruser/application-container
The push refers to a repository [docker.io/mydockeruser/application-container]
9f5e7eecca3a: Layer already exists
08ee50f4f8a7: Layer already exists
33e5788c35de: Layer already exists
c3d75a5c9ca1: Layer already exists
0f94183c9ed2: Layer already exists
b58339e538fb: Layer already exists
317a9fa46c5b: Layer already exists
a9bb4f79499d: Layer already exists
9c81988c760c: Layer already exists
c5ad82f84119: Layer already exists
fe4c16cbf7a4: Layer already exists
latest: digest: sha256:43214016a4921bdebf12ae9de7466174bee1afd44873d6a60b846d157986d7f7 size: 2627

3. リモート接続を開く

コンテナのアップロードが終わったので、リモートサーバーにダウンロードして実行する方法を見てみましょう。最初に、コンテナを実行するリモート環境の準備が必要です。ホストマシンで行ったときと同様に、DockerをインストールしてDocker Registryにログインしなければなりません。SSHでリモート接続するには、以下のコマンドを実行します。

ssh remoteuser@35.190.185.215
# 認証が必要な場合は以下を実行
ssh -i path/to/your/key.pem remoteuser@35.190.185.215

4. ダウンロード

リモートマシンでの設定をすべて終えた後は、ターミナルでのアクセスは不要になります。各コマンドはその環境で実行されます。コンテナをダウンロードしましょう。必要な場合はキーのフラグを指定することもお忘れなく。

$ ssh remoteuser@35.190.185.215 docker pull mydockeruser/application-container
Using default tag: latest
latest: Pulling from mydockeruser/application-container
386a066cd84a: Pulling fs layer
ec2a19adcb60: Pulling fs layer
b37dcb8e3fe1: Pulling fs layer
e635357d42cf: Pulling fs layer
382aff325dec: Pulling fs layer
f1fe764fd274: Pulling fs layer
a03a7c7d0abc: Pulling fs layer
fbbadaebd745: Pulling fs layer
63ef7f8f1d60: Pulling fs layer
3b9d4dda739b: Pulling fs layer
17e2d6aad6ec: Pulling fs layer
...
3b9d4dda739b: Pull complete
17e2d6aad6ec: Pull complete
Digest: sha256:c030e4f2b05191a4827bb7a811600e351aa7318abd3d7b1f169f2e4339a44b20
Status: Downloaded newer image for mydockeruser/application-container:latest

5. 再起動

コンテナを初めて実行したので、他のコンテナを停止する必要はありません。ローカルホストのときと同じコマンドを使って次のようにコンテナを実行できます。

$ ssh remoteuser@35.190.185.215 docker run -p 3000:3000 -d mydockeruser/application-container
f86afaa7c9cc4730e9ff55b1472c5b30b0e02055914f1673fbd4a8ceb3419e23

ここでは-tiフラグの代わりに-dフラグを与えているので、コンテナのハッシュだけが出力されます。これはコンテナをdetachedモードで動かす(出力をターミナルにアタッチしない)ことを表します。

ブラウザでリモートホストアドレス(ここでは35.190.185.21:3000を開いて、アプリが実行されているかどうかをチェックします。

(後編に続きます)

関連記事

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

Dockerでsupervisorを使う時によくハマる点まとめ

PrometheusでDockerホスト + コンテナを監視してみた

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

$
0
0

概要

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

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

Code Climate編集者メモ: 今回はゲストとしてMarko Anastasovの記事もご紹介します。Markoは開発者であると同時に、CI/デプロイサービスで知られるSemaphoreの共同設立者であり、Code ClimateのCIパートナーでもあります。


単体テストを書くという行為は、検証よりも設計という行為に近い — Bob Martin

テスト駆動開発(TDD)はテストのためのものであるという思い違いを未だによく見かけます。TDDを遵守することで開発が迷走する可能性を最小限に抑えることができ、最初にテストを書くことを義務付けることでテストの書き忘れも最小限に留められます。いつもの私は、超人であり続けなければとてもなしえないようなソリューションではなく、普通の人間のために設計されたソリューションを選びますが、ここでは少し違います。TDDは自動化テストを一種の乗り物のように用いて、私たちがコードを書く前にコードのことをいやでも考えざるを得ないように設計されています。なおこの方法は、特定の機能に接続されるすべてのコードが期待どおり動作していることを確認するのにデバッガを起動するよりもずっとよい方法です。TDDの目的はソフトウェア設計の改良であり、テストコードはその副産物のひとつです。

テストを必ず最初に書くことで、テストされるオブジェクトのインターフェイスについてじっくり考えるようになります。必要だがまだ存在しないオブジェクトについても同様です。作業は制御可能な小さな範囲で少しずつ進められます。テストが初めてパスしてもそこで作業は終わりではありません。再び実装に立ち戻ってコードをリファクタリングし、コードを美しく保ちます。コードが正しく動作していることを担保するテストスイートが存在するおかげで、自信を持ってコードを変更できます。

TDDの経験者なら誰でも、コードの設計力を問われ、そして磨かれることに気づくようになります。開発しながら常に「むー、このコードはprivateのままではまずそうだな」とか「このクラスの責務が増えすぎてしまった」という風に考えるようになるのです。

テスト駆動リファクタリング

あるコードのテストをどう書けばよいかわからなくなってくると、「red-green-refactor」というサイクルが止まってしまうこともあるでしょうし、たとえ書けたとしてもかなりつらい作業に思えることでしょう。テストを書くのがつらい部分は、しばしばコードの設計に問題があることを示します。あるいは、その部分のコードがTDDアプローチに沿って書かれていなかっただけかもしれませんが。テストコードの「匂い」は多くの場合アンチパターンと呼ぶのがふさわしく、テストとアプリコードの両方についてリファクタリングする機会であることを示します。

例として、Railsのcontroller specでの複雑なテストセットアップを見てみましょう。

describe VenuesController do

  let(:leaderboard) { mock_model(Leaderboard) }
  let(:leaderboard_decorator) { double(LeaderboardDecorator) }
  let(:venue) { mock_model(Venue) }

  describe "GET show" do

    before do
      Venue.stub_chain(:enabled, :find) { venue }
      venue.stub(:last_leaderboard) { leaderboard }
      LeaderboardDecorator.stub(:new) { leaderboard_decorator }
    end

    it "venueをidで検索して@venueに代入する" do
      get :show, :id => 1
      assigns[:venue].should eql(venue)
    end

    it "@leaderboardを初期化する" do
      get :show, :id => 1
      assigns[:leaderboard].should == leaderboard_decorator
    end

    context "userはpatronとしてログインしている" do

      include_context "patronがログインしている"

      context "patronはトップ10にいない" do

        before do
          leaderboard_decorator.stub(:include?).and_return(false)
        end

        it "leaderboardからpatronのstatsを取得" do
          patron_stats = double
          leaderboard_decorator.should_receive(:patron_stats).and_return(patron_stats)
          get :show, :id => 1
          assigns[:patron_stats].should eql(patron_stats)
        end
      end
    end

    # 簡単のため以後のテストケースは省略
  end
end

このコントローラのアクションは、技術的にはさほど長くありません。

class VenuesController < ApplicationController

  def show
    begin
      @venue = Venue.enabled.find(params[:id])
      @leaderboard = LeaderboardDecorator.new(@venue.last_leaderboard)

      if logged_in? and is_patron? and @leaderboard.present? and not @leaderboard.include?(@current_user)
        @patron_stats = @leaderboard.patron_stats(@current_user)
      end
    end
  end
end

ここでお気づきいただきたいのは、specセットアップのコードが長いと、たとえばVenue.enabled.findが呼び出されるというexpectationや、LeaderboardDecorator.newに正しい引数が渡されるというexpectationを開発者が書き忘れてしまいがちであるという点です。代入された@leaderboardの元は代入されたvenueであるかどうかがまったく明確になっていません。

MVCパラダイムに囚われてしまった開発者は(私も含めてですが)、ついコントローラにビジネスロジックを長々と書き連ねてしまい、よいspecを書くこともコードやspecのメンテも困難になってしまいます。この困難は、Railsのコントローラのたった1行のメソッドですら多くのことを行っていることが原因です。

def show
  @venue = Venue.find(params[:id])
end

上のメソッドはこれだけの作業を行っています。

  • パラメータを取り出す
  • アプリ固有のメソッドを呼び出す
  • ビューテンプレートで用いられる変数へ代入する
  • レスポンステンプレートのレンダリング

データベース内部やビジネスルールの奥深い部分に到達するコードを書き足すと、コントローラのメソッドがカオスになるだけです。

上のコントローラには、4つの条件を持つif文が隠れています。完全なspecでは、これをカバーするためだけに15とおりの組み合わせを記述しなければなりませんが、もちろんそのようなものは書かれていません。しかし、コードがコントローラの外に置かれる場合は事情が変わってきます。

改良版のcontroller specが次のようになっているとしましょう。外部から受け付けるリクエストを処理してレスポンスを準備するという作業を実行するためにはどのようなインターフェイスが望ましいでしょうか。

describe VenuesController do

  let(:venue) { mock_model(Venue) }

  describe "GET show" do

    before do
      Venue.stub(:find_enabled) { venue }
      venue.stub(:last_leaderboard)
    end

    it "有効なvenueをidで検索する" do
      Venue.should_receive(:find_enabled).with(1)
      get :show, :id => 1
    end

    it "見つかった@venueを代入する" do
      get :show, :id => 1
      assigns[:venue].should eql(venue)
    end

    it "venueのleaderboardをデコレーションする" do
      leaderboard = double
      venue.stub(:last_leaderboard) { leaderboard }
      LeaderboardDecorator.should_receive(:new).with(leaderboard)

      get :show, :id => 1
    end

    it "@leaderboardを代入する" do
      decorated_leaderboard = double
      LeaderboardDecorator.stub(:new) { decorated_leaderboard }

      get :show, :id => 1

      assigns[:leaderboard].should eql(decorated_leaderboard)
    end
  end
end

他のコードはどこに行ってしまったのでしょうか?ここではモデルを拡張して検索ロジックを単純化しています。

describe Venue do

  describe ".find_enabled" do

    before do
      @enabled_venue = create(:venue, :enabled => true)
      create(:venue, :enabled => true)
      create(:venue, :enabled => false)
    end

    it "有効なスコープ内で検索する" do
      Venue.find_enabled(@enabled_venue.id).should eql(@enabled_venue)
    end
  end
end

さまざまなif文は次のように単純化できます。

  • if logged_in?: 結果の違いはビューテンプレートで決定できる
  • if @leaderboard.present?: (古いコード)falseの場合の動作はビューで決定できる
  • その他のコードはdecoratorクラスに移動して新しいメソッドで詳しく記述できる
describe LeaderboardDecorator do

  describe "#includes_patron?" do

    context "userがpatronではない" { }

    context "userがpatronである" do
      context "userがリストにいる" { }
      context "ユーザーがリストにいない" { }
    end
  end
end

この新しいメソッドは、@leaderboard.patron_statsをレンダリングするかどうかをビューで決定できるようにします。この部分の変更は不要です。

# app/views/venues/show.html.erb
<%= render "venues/show/leaderboard" if @leaderboard.present? %>
# app/views/venues/show/_leaderboard.html.erb
<% if @leaderboard.includes_patron?(@current_user) -%>
  <%= render "venues/show/patron_stats" %>
<% end -%>

これで、コントローラのメソッドがかなりシンプルになりました。

def show
  @venue = Venue.find_enabled(params[:id])
  @leaderboard = LeaderboardDecorator.new(@venue.last_leaderboard)
end

このコードを次回使うときには、LeaderboardDecoratorに与える正しい引数とは何かをコントローラ側で把握する必要がある点がちょっと残念かもしれません。venue用の新しいdecoratorを1つ導入して、デコレーションされたleaderboardを返すようにしてもよいでしょう。この部分の実装は読者の練習用に残しておきます ;)

最後に

もっと詳しくお知りになりたい方は、SemaphoreブログでMarkoのRailsアプリのテスティングアンチパターン記事をご覧ください。

関連記事

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

Railsで重要なパターンpart 2: Query Object(翻訳)

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

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

$
0
0

概要

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

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

belongs_toリレーションはRailsアプリで最もよく使われる関連付けなので、皆さまのアプリでも多数使われていると確信しています。さて、JobCategoryという2つのモデルがあるとしましょう。1つのjobは1つのcategoryに属し、1つのcategoryには多くのjobがあるというシンプルな関連付けがなされています。

Categoryモデルにはpublished:booleanという属性があり、そのcategoryのjobを表示してよいかどうかを指定します。ここでの目的は、publishされたカテゴリに割り当てられているjobだけを返すクエリを作成することです。

普通の方法

通常は、以下のような方法を使います。

Job.joins(:category).where(categries: {published: true})

これに何かまずい点があるのでしょうか?別にありません。しかしここでは「ロジックの分離」に着目したいと思います。Jobモデルで何か操作を行う場合、publishされたcategoryだけを取り出すという条件を満たすことを気にかけるべきではありません。これはCategoryモデルに関連するロジックなので、このロジックをそちらに移動しましょう。

ロジックの分離

class Category < ActiveRecord::Base
 has_many :jobs

  def self.publishable
    where(published: true)
  end
end

ここではクラスメソッドの代わりにスコープを使うこともできます(スコープかクラスメソッドかについては別記事をご覧ください【原文リンク切れ】)。今回の場合、クラスメソッドの方が私にとって明確に思えたのでクラスメソッドを使うことにします。これによってJobモデルは次のようになります。

class Job < ActiveRecord::Base
  belongs_to :category

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

これで、次のように呼び出せるようになりました。

Job.publishable

改善の理由

メンテナンスするコードがもっとたくさんある場合になぜ2番目のソリューションの方がよいかについて疑問をお持ちの方もいらっしゃるかもしれません。理由は次のとおりです。

  1. ロジックを分離できます。categoryに関連するものはCategoryモデルに配置され、jobに関連するものはJobに配置されています。あなたの同僚は、ロジックをチェックして正確にはどんなクエリが使われるべきかを調べなくても、Job.publishableを呼び出すだけで済みます。
  2. 最初のバージョンのクエリだと、アプリのあちこちにJob.joins(:category).where(categries: {published: true})がばらまかれてしまいます。そのcategoryがpublishされているかどうかを調べるために条件をもっと詳しくチェックしなければならないとしたらどうしますか?ばらまかれているコードをすべて見つけ出すという残念な方法を取らざるを得なくなります。しかし2番目の方法なら、メソッドに変更を加えるだけで済みます。他に何も変更する必要はありません。

  3. 人間にとってより読みやすいコードになります。これはチームで若手開発者を抱えている場合に非常に重要な点です。

  4. Categoryモデルに関連付けられているあらゆるモデルでCategory.publishedを使えるようになります。

Railsでお困りの方にお知らせ

twitter または連絡用フォームにてお知らせください。サポート方法をご連絡いたします。

関連記事

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

週刊Railsウォッチ(20180119)derailed_benchmarks gem、PostgreSQLをGraphQL API化するPostGraphile、機械学習でモック画像をHTML化ほか

$
0
0

こんにちは、hachi8833です。Nintendo Laboにいろいろ持ってかれそうで気になってます。


つっつきボイス: 「任天堂のものづくりセンス、パないなー」

それでは今週のウォッチ、いってみましょう。

Rails: 今週の改修

今回はCommit差分から見繕いました。

left_outer_joinsをunscopeできるようになった

# activerecord/lib/active_record/relation/query_methods.rb#351
     VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
                                      :limit, :offset, :joins, :includes, :from,
-                                     :readonly, :having])
+                                     :readonly, :having, :left_outer_joins])

つっつきボイス: 「scopeでleft_outer_joinsできるならunscopeもできないと、ってことかな」

重要度の低いダイジェストにデフォルトでSHA-1を使用するようになった

# railties/lib/rails/application/configuration.rb#103
           if respond_to?(:active_support)
             active_support.use_authenticated_message_encryption = true
+            active_support.use_sha1_digests = true
           end

ETagヘッダーなどの重要でないダイジェストにはMD5ではなくSHA-1を使う。
Rails.application.config.active_support.use_sha1_digests = true
new_framework_defaults_5_2.rb.ttより大意

pg-1.0 gemに対応

pgが0.21から1.0にメジャーバージョンアップしたそうです。

# Gemfile.lock#343
-    pg (0.19.0)
-    pg (0.19.0-x64-mingw32)
-    pg (0.19.0-x86-mingw32)
+    pg (1.0.0)
+    pg (1.0.0-x64-mingw32)
+    pg (1.0.0-x86-mingw32)

つっつきボイス: 「へー、pgはもう永遠に1.0にならないんじゃないかと思ってた」「queue_classicってメンテナ代わったのかな?↓」

# Gemfile#65
-  gem "queue_classic", github: "QueueClassic/queue_classic", branch: "master", require: false, platforms: :ruby
+  gem "queue_classic", github: "Kjarrigan/queue_classic", branch: "update-pg", require: false, platforms: :ruby

savesave!の後でオブジェクトがunfreezeされていたのを修正

破棄したオブジェクトがsave後に変更される可能性があったので修正されました。

# activerecord/lib/active_record/persistence.rb#65
     def create_or_update(*args, &block)
       _raise_readonly_record_error if readonly?
+      return false if destroyed?
       result = new_record? ? _create_record(&block) : _update_record(*args, &block)
       result != false
     end

つっつきボイス: 「これ本当ならエラーをraiseしたいところだろうな: 互換性とかの問題でfalseを返してるのかも」

MySQL: create_databasecollationが指定されている場合にデフォルトのcharsetを追加しないように修正

# activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#250
       def create_database(name, options = {})
         if options[:collation]
-          execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}"
+          execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}"
         else
           execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}"
         end

つっつきボイス: 「そうそう、知らずに|| 'utf8'が効いちゃうとハマるんだよなー」「修正後のテストでは寿司ビール対策でおなじみのutf8mb4_bin使ってますね」

MySQLのencodingをutf8からutfmb4に変更して寿司ビール問題に対応する

リファクタリング: Browserクラスを新設

システムテストのactionpack/lib/action_dispatch/system_testing/driver.rbのオプションがBrowserクラスに引っ越しました。

# actionpack/lib/action_dispatch/system_testing/browser.rb
+module ActionDispatch
+  module SystemTesting
+    class Browser # :nodoc:
+      attr_reader :name
+
+      def initialize(name)
+        @name = name
+      end
...

DHHによる修正2件

# rails/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt#9
   policy.font_src    :self, :https, :data
   policy.img_src     :self, :https, :data
   policy.object_src  :none
-  policy.script_src  :self, :https
+  policy.script_src  :self, :https, :unsafe_inline
   policy.style_src   :self, :https, :unsafe_inline

   # Specify URI for violation reports

つっつきボイス: 「CSP=コンテンツセキュリティポリシー」「unsafe_inlineはW3Cのこれですね↓」

; Keywords:
keyword-source = “‘self'” / “‘unsafe-inline'” / “‘unsafe-eval'” / “‘strict-dynamic'” / “‘unsafe-hashed-attributes'”
CSP3より

-Rails.application.config.content_security_policy do |p|
-  p.default_src :self, :https
-  p.font_src    :self, :https, :data
-  p.img_src     :self, :https, :data
-  p.object_src  :none
-  p.script_src  :self, :https
-  p.style_src   :self, :https, :unsafe_inline
+Rails.application.config.content_security_policy do |policy|
+  policy.default_src :self, :https
+  policy.font_src    :self, :https, :data
+  policy.img_src     :self, :https, :data
+  policy.object_src  :none
+  policy.script_src  :self, :https
+  policy.style_src   :self, :https, :unsafe_inline

つっつきボイス: 「少なくともpはないなー: Kernel.#pがあるから」「そういえば1文字のローカル変数で他にも使えないものがあったような…」

その後思い出しましたが、pryではcなどをローカル変数に使うと怒られるのでした。

[1] pry(main)> c=1
=> 1
[2] pry(main)> c
Error: Cannot find local context. Did you use `binding.pry`?

参考: Pryのコンソールで使えない変数

Rails

Railsチュートリアルが5.1.4に対応


つっつきボイス: 「安川さんたちが継続的翻訳システムを構築しているおかげでRailsチュートリアルもガイドもオープンな差分翻訳ができるようになっててうれしいです: 自分はバッチで翻訳する方が好きですが」

プロセスマネージャ再び

Dogfooding Process Managerの続きだそうです。


つっつきボイス: 「自前でプロセスマネージャをこしらえた話のようなんですが、このプロセスって何だろうと思って」「ざっとしか見てないけど、Unixのプロセスのことではなさそうに見える」「ところで、何とかmanagerってネーミングはたいていアンチパターンですね」「あー確かに」

そういえば野球の世界では監督はmanagerですが、日本だとマネージャーは違う意味に横滑りしてますね。

マイグレーションをpendingしたままRailsを本番で実行しないようにする方法

短い記事です。ActiveRecord::Migration.check_pending!でやれるそうです。

# 同記事より
if ($PROGRAM_NAME.include?('puma') || $PROGRAM_NAME.include?('sidekiq')) && Rails.env.production?
  ActiveRecord::Migration.check_pending!
end

RailsのForm Objectとルーティング(RubyFlowより)

# 同記事より
class NewQuestionnaireForm
   include ActiveModel::Model

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

  def save
    to_model.save
  end
end

Railsのメモリ容量を減らしてHeroku課金を節約(Awesome Rubyより)


同記事より

以下の記事に出てきたjemallocyajl-rubyなどを動員して節約に励んでいます。

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

⭐derailed_benchmarks: Railsアプリのさまざまなベンチマークを取れるgem⭐

上の記事にも使われていたgemで、★1800超えです。ヒープダンプ/メモリリーク調査/stackprofなどさまざまな静的/動的情報を取れます。

# READMEより
$ bundle exec derailed exec perf:stackprof
==================================
  Mode: cpu(1000)
  Samples: 16067 (1.07% miss rate)
  GC: 2651 (16.50%)
==================================
     TOTAL    (pct)     SAMPLES    (pct)     FRAME
      1293   (8.0%)        1293   (8.0%)     block in ActionDispatch::Journey::Formatter#missing_keys
       872   (5.4%)         872   (5.4%)     block in ActiveSupport::Inflector#apply_inflections
       935   (5.8%)         802   (5.0%)     ActiveSupport::SafeBuffer#safe_concat
       688   (4.3%)         688   (4.3%)     Temple::Utils#escape_html
       578   (3.6%)         578   (3.6%)     ActiveRecord::Attribute#initialize
...

つっつきボイス: 「derailed_benchmarksは結構使われている印象っすね」「作者はRichard Schneemanさんでした」

ベテランRubyistがPythonコードを5倍速くした話(翻訳)

今週の⭐を進呈いたします。おめでとうございます。

Stimulus: Turbolinksと相性のよい控えめなJSフレームワーク(Ruby Weeklyより)


stimulusjs/stimulusより

// hello_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  greet() {
    console.log(`Hello, ${this.name}!`)
  }

  get name() {
    return this.targets.find("name").value
  }
}

つっつきボイス: 「Stimulusって、確かDHHのBasecampがやってるやつですよね」「Turbolinksって最近オフにすること多いけど」

Railsを順を追ってアップグレードする(Awesome Rubyより)


つっつきボイス: 「冒頭の1. Stop the world、2. Long-lived upgrade branchとかまさに定番のアプローチ」「こうやって本番を二本立てにして↓リバースプロキシで振り分けながら少しずつ移行するというのもあるある: 検証が不十分なまま切り替えるとえらいことになったりするけど」


同記事より

ActiveRecordに欲しくなるEctoの機能(Awesome Rubyより)


infinum.coより

def registration_changeset(struct, params) do
  struct
  |> cast(params, [:email, :password])
  |> validate_required([:email, :password])
  |> unique_constraint(:email)
  |> put_password_hash()
end

def update_profile_changeset(struct, params) do
  struct
  |> cast(params, [:first_name, :last_name, :age])
  |> validate_required([:first_name, :last_name])
end

|>はElixirの「パイプライン演算子」だそうです。


つっつきボイス: 「Ectoって、ElixirのPhoenixフレームワークで使うやつか」「EctoはORMではない、って書いてますね」「この種のフレームワークを業務で使う動機は今のところ見えないなー」

参考: Rails使いがElixirのEctoを勉強した時のまとめ

Railsアプリの災害復旧プラン


engineyard.comより


つっつきボイス: 「disaster recovery planはRailsアプリに限らず重要っすね」「そういえば最近Engine Yardってひと頃ほど見かけない気がしてきた」

あるRails請負開発者の一日(Hacklinesより)

Planet Argonさん(@planetargon)がシェアした投稿


つっつきボイス: 「相当昔ですが、Oracle日本支社では犬飼ってるって話を思い出しました」「今のオフィス事情だと難しそう」

その他Rails小粒記事


つっつきボイス: 「JavaScriptのテスティングフレームワークというとmocha以外にJestもあるのね」
「最後のパーシャルレンダリング記事、でかいデータで素朴にeach回したら確かに遅い↓」

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

「こうやってcollection使う方が確実に速いけど、油断するとつい上みたいに書いちゃうことはあるな」「社内でもたまに見ますね」

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

参考: Railsガイド 3.4 パーシャルを使用する

Ruby trunkより

提案: Hash#transform_keys!recursive: trueオプション(継続)

config = MyAwesomeFormat.load(file); config.transform_keys!(recursive: true, &:to_sym)みたいに書きたいという主旨です。

def transform_keys!(recursive: false, &block)
  # do original transform_keys! here
  values.each do |v|
    if v.respond_to?(:each)
      v.each{|i| i.transform_keys!(recursive: true, &block) if i.respond_to?(:transform_keys!) }
    else v.respond_to?(:transform_keys!)
      v.transform_keys!(recursive: true, &block)
    end
  end if recursive
end

提案: GC速度と少々引き換えにメモリを削減(継続)


#14370より

Aaron Pattersonさんからのissueです。


つっつきボイス: 「最初にレス付けてるnormalpersonさんは昨年のRubyKaigiのあちこちで名前が出てきてた、普通じゃない人」「凄い名前w」

週刊Railsウォッチ(20170922)特集: RubyKaigi 2017セッションを振り返る(1)、Rails 4.2.10.rc1リリースほか

入れ違いで修正

Ruby

Ruby 2.5の陽の当たっていない新機能(Hacklinesより)

行カバレッジやブランチカバレッジ機能などを紹介しています。

Y -> 1: def hello(number)
Y -> 2:   if number == 1
Y -> 3:    'world'
Y -> 4:   else
N -> 5:     'mars'
Y -> 6:   end
Y -> 7: end
Y -> 8:
Y -> 9: hello(1)

つっつきボイス: 「この機能がRubyMineみたいなIDEと連携したらすごくうれしい」「名前忘れたけどこういうカバレッジのgemあった: 絶対に通過しないコードをあぶり出したりとかできる」

なお2.5のカバレッジについては以下でChangelogをざっくり訳してあります。

Ruby 2.5.0リリース!NEWSを読んでみた

Rubyの継承で動的に引数を渡す(RubyFlowより)

# 同記事より
class Render
  def self.engine; end

  def self.inherited(subclass)
    puts subclass        #=> Memo::Render
    puts subclass.engine #=> nil !!!
  end
end

つっつきボイス: 「Rubyってここまでエグいコードも書けるんだなって思いますね」「何でもアタッチできちゃうとか、ここまでくるともうオブジェクト指向言語というよりオブジェクト指向スクリプトみたいw」

Ruby 2.5のFrozenErrorクラス

2.5.0 :001 > NAME = 'Atul'.freeze
 => "Atul"
2.5.0 :002 > NAME << 'Joy'
Traceback (most recent call last):
        2: from /home/atul/.rvm/rubies/ruby-2.5.0/bin/irb:11:in `<main>'
        1: from (irb):2
FrozenError (can't modify frozen String)

つっつきボイス: 「今までRuntimeErrorだったのがFrozenErrorに変わるのはありがたい」「frozen_string_literalが完了するまでの混乱を少しでも軽くするためでもあるんでしょうね」

参考: frozen_string_literalが入って気づいた、メソッド設計の原則

Kernel.method_added


つっつきボイス:Kernel.method_addedなんてのがあるのか: 特定のメソッド追加にフックかける」「実体はModuleにあった↓」

# docs.ruby-lang.org/ja/2.5.0/method/Module/i/method_added.html
class Foo
  def Foo.method_added(name)
    puts "method \"#{name}\" was added"
  end

  def foo
  end
  define_method :bar, instance_method(:foo)
end

=> method "foo" was added
   method "bar" was added

参考: Module#method_added

aruba: CLIアプリをRSpecやMiniTestでテストするgem(Awesome Rubyより)


つっつきボイス: 「あのCucumberがやってるんですね」「バッチ処理のテストをRSpecとかで書けるし、Ruby以外に任意のCLIに使えるのがいいな」


app.cucumber.pro

Rubyのシンボル話その後

#14347はちょうど前回のウォッチで取り上げました。


つっつきボイス: 「あちこちで話題になってるやつですね」「途中まで読んでた」「やっとシンボルは文字列ではないということになったと」

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

Rubyはやっぱり死んでない(Ruby Weeklyより)

こちらもEngine Yardのブログです。

ネストしたハッシュをlambdaでリファクタリング

同記事より
pub.doc[‘programs’].each &remove_icons(‘grades’, &remove_icons(‘units’))
def remove_icons value_key=nil, &block
  lambda do |key, value|
    next if key == ‘_order’
    value.delete ‘icons’
    if value_key
      value[value_key].each(&block) if block_given?
      value[value_key].each(&remove_icons) unless block_given?
    end
  end
end

つっつきボイス: 「うんうんよくあるやつ:再帰で書きましょう!みたいな」「Hashの再帰って何かと面倒ですよね」「せいぜい3階層ぐらいしか潜らないことをわかって書いてるのに、RuboCopに怒られたりとか」

高木さん

SQL

PostgreSQLにはmeltdownパッチは不要だが少し遅くなる(Postgres Weeklyより)


つっつきボイス: 「meltdownは基本的にカーネルの問題だからアプリにパッチが必要になることはそうないかと」「パッチで遅くなるのはもうしゃーない」「AWSもいろいろ言ってるけど対策すれば遅くなるっしょ」

参考: CPU脆弱性Meltdownのパッチ適用でベンチマークスコアが25%低下した

PostGraphile: PostgreSQLをGraphQL API化するJSライブラリ(Postgres Weeklyより)


graphile.orgより

以下を実行してhttp://localhost:5000/graphiqlをブラウザで開くといきなりGraphiqlが動きました。N+1クエリも克服しているそうです。これ凄いかも。

npm install -g postgraphile
postgraphile -c postgres://user:pass@host/dbname --schema schema_name

PGLogicalがアップデート(Postgres Weeklyより)

PostgreSQLの動作を知る(Postgres Weeklyより)

Pythonのツールを使います。


つっつきボイス: 「お、Internalとか書いてるけどこれはむしろ入門向け記事ですね: 量は多いけど相当やさしい内容」

JavaScript

(a ==1 && a== 2 && a==3)trueにする知見が続々

BPS社内で盛り上がりました。

// Stackoverflowより
var aᅠ = 1;
var a = 2;
var ᅠa = 3;
if(aᅠ==1 && a== 2 &&ᅠa==3) {
    console.log("Why hello there!")
}

ifに半角のハングル文字を使うという荒業を繰り出したり、C++などでやってみたりしています。

JavaScriptの二進木、再帰、末尾呼び出し最適化

nullとundefinedとは

CSS/HTML/フロントエンド

Web Componentsの秘密

参考: MDN Web Components

フロントエンドのエラー表示を再考する


logrocket.comより

blog.jxck.ioの新着記事


つっつきボイス: 「これは読んでおきたい記事」「そういえばこれと少し似た感じの、ネコのアイコンのブログ記事が話題になってましたね」「ネコのアイコン…?」「あったこれ↓」「あこの人か: アイコンとか全然気にしてなかったw」

参考: ソフトウェアの互換性と僕らのUser-Agent文字列問題

Screenshot-to-code-in-Keras: ニューラルネットワークでモック画像から静的HTMLページを生成(GitHub Trendingより)

アニメーションGIFが巨大すぎるのでここには貼りませんでした。


つっつきボイス: 「すげっ」「Bootstrapにも対応してるみたいですね」「一回こっきりの案件とかならかなりイケそう」「HTMLコーダー界に激震走るか」

その他

YAGNIを実践する


dev.to/gonedarkより


つっつきボイス: 「社内にもYAGNIを愛して止まない人がいるから彼を観察してるとだいたいわかりますよ」

参考: Wikipedia-ja YAGNI

Slackにprivate shared channel機能が追加


つっつきボイス: 「これありがたい: shared channelは前からあるけどpublicにしかできなかったんで」

Windows CLIの改善

WSLのchmod/chownの改良とtar/curlの追加です。

なお、こんなのもありました。

DOS窓の|は大丈夫だそうです。

Docker for macで/etc/localtimeがマウントできない問題

minio: Amazon S3 API互換のオブジェクトストレージサーバー


minio.ioより


つっつきボイス: 「S3互換のこういうのは他にもありますけどね」「GCPやAzureとかいろんなクラウドで使えるのはよさそう」「今さらですがオブジェクトストレージサーバーって何でしたっけ?」「AWS S3みたいなサービスがそれです: WebDAVみたいにRESTfulにオブジェクトにアクセスできるサービス」

HighwayHash: Go言語の爆速ハッシュ生成ライブラリ


つっつきボイス: 「10G/secとか確かに超速い」「ハッシュは速度だけあってもいかんので、ちゃんと分散してるかとかも大事ですね」

データがあれば使えるCloud AutoML VisionをGoogleが発表

一般のニュースにもなってますが一応。


つっつきボイス: 「今はデータサイエンティストやAIエンジニアが明らかに不足してるからどこもカスタマイズとかチューニングに手が回らなくて、こうやってそこそこのものを公開して好きに使ってくれ、みたいな方向に向かってる感じですね」「ユーザーに丸投げですか」

参考: Googleが「Cloud AutoML Vision」を発表、独自のデータセットを使ったカスタム機械学習モデルが簡単に構築できるように

番外

暗算術

a% of b = b% of aは初めて知りました。「25の16%=16の25%」みたいに使うそうです。


つっつきボイス: 「計算すると確かにそうなってるな: 式で見ると一瞬でわかるけど言われるまで気づきにくい」「英語圏なんで単位のフィート換算とかいらなさそうなのも多いです」「ひと頃入社試験でよく出されたフェルミ推定なんかやるときは、こういうのを何となくでも知っておかないと手も足も出なかったりしますね」

参考: 暗記しておくとなにかと便利なプチ公式まとめ

これは欲しい


つっつきボイス: 「Nintendo Laboとどっちが子どもにウケるかなと思って」「今ならポプテピピックっしょw」「あれはもう子どもの反応が面白すぎますね」

学習/プログラミング不要の産業ロボット


つっつきボイス: 「荷物の積み下ろしとかまでやってます」「人間雇う方がまだまだ安いな、今のところは」

Switchエミュレータ(GitHub Trendingより)


つっつきボイス: 「ソフトはともかくハードウェアはそうもいかないか」

その後、GPL V2というライセンスの厳しさや、パチンコの当たり判定システムの話題で盛り上がりました。

ルーシーさん

自己修復コンクリート


今週は以上です。

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

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

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

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

Rails公式ニュース

Ruby Weekly

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Hacklines

Hacklines

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

JSer.info

jser.info_logo_captured

Github Trending

160928_1701_Q9dJIU

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

$
0
0

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

概要

原著者の許諾を得て、CC BY-NC-SAライセンスに基づき翻訳・公開いたします。

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

Herokuライクなデプロイソリューションの構築方法を解説します。
特定のクラウドプロバイダや、Dockerに関連しないツールを必要としません。

ここまでのまとめ

これで自動デプロイの構築に必要な材料がすべて揃いました。以下は最終的なコードであり、deployer.rbという名前でrootフォルダに保存できます。各行の動作を見てみましょう。

# deployer.rb
class Deployer
  APPLICATION_HOST = '54.173.63.18'.freeze
  HOST_USER = 'remoteuser'.freeze
  APPLICATION_CONTAINER = 'mydockeruser/application-container'.freeze
  APPLICATION_FILE = 'application.tar.gz'.freeze
  ALLOWED_ACTIONS = %w(deploy).freeze
  APPLICATION_PATH = 'blog'.freeze

  def initialize(action)
    @action = action
    abort('Invalid action.') unless ALLOWED_ACTIONS.include? @action
  end

  def execute!
    public_send(@action)
  end

  def deploy
    check_changed_files
    copy_gemfile
    compress_application
    build_application_container
    push_container
    remote_deploy
  end

  private

  def check_changed_files
    return unless `git -C #{APPLICATION_PATH} status --short | wc -l`
                  .to_i.positive?
    abort('Files changed, please commit before deploying.')
  end

  def copy_gemfile
    system("cp #{APPLICATION_PATH}/Gemfile* .")
  end

  def compress_application
    system("tar -zcf #{APPLICATION_FILE} #{APPLICATION_PATH}")
  end

  def build_application_container
    system("docker build -t #{APPLICATION_CONTAINER}:#{current_git_rev} .")
  end

  def push_container
    system("docker push #{APPLICATION_CONTAINER}:#{current_git_rev}")
  end

  def remote_deploy
    system("#{ssh_command} docker pull "\
           "#{APPLICATION_CONTAINER}:#{current_git_rev}")
    system("#{ssh_command} 'docker stop \$(docker ps -q)'")
    system("#{ssh_command} docker run "\
             "--name #{deploy_user} "\
             "#{APPLICATION_CONTAINER}:#{current_git_rev}")
  end

  def current_git_rev
    `git -C #{APPLICATION_PATH} rev-parse --short HEAD`.strip
  end

  def ssh_command
    "ssh #{HOST_USER}@#{APPLICATION_HOST}"
  end

  def git_user
    `git config user.email`.split('@').first
  end

  def deploy_user
    user = git_user
    timestamp = Time.now.utc.strftime('%d.%m.%y_%H.%M.%S')
    "#{user}-#{timestamp}"
  end
end

if ARGV.empty?
  abort("Please inform action: \n\s- deploy")
end
application = Deployer.new(ARGV[0])

begin
  application.execute!
rescue Interrupt
  puts "\nDeploy aborted."
end

それでは1つずつ手順を追ってみましょう。

  APPLICATION_HOST = '54.173.63.18'.freeze
  HOST_USER = 'remoteuser'.freeze
  APPLICATION_CONTAINER = 'mydockeruser/application-container'.freeze
  APPLICATION_FILE = 'application.tar.gz'.freeze
  ALLOWED_ACTIONS = %w(deploy).freeze
  APPLICATION_PATH = 'blog'.freeze

ここでは値の重複を避けるためにいくつかの定数をコードで定義しています。APPLICATION_HOSTは実行するサーバーのリモートIPアドレス、HOST_USERはリモートサーバーのユーザー名、APPLICATION_CONTAINERはアプリをラップするコンテナの名前です。APPLICATION_FILEは圧縮したアプリのファイル名なので名前は自由に変えられます。ALLOWED_ACTIONSは許可する操作の配列であり、どの操作を利用可能にするかを簡単に定義できます。最後のAPPLICATION_PATHはアプリへのパスです。今回の例ではblogとしています。

  def initialize(action)
    @action = action
    abort('Invalid action.') unless ALLOWED_ACTIONS.include? @action
  end

  def execute!
    public_send(@action)
  end

上は、(ALLOWED_ACTIONSで)利用できる各メソッドのバリデーションと呼び出しを行うラッパーです。これを用いることで、コードをリファクタリングする必要なしに、呼び出し可能な新しいメソッドを簡単に追加できます。

  def deploy
    check_changed_files
    copy_gemfile
    compress_application
    build_application_container
    push_container
    remote_deploy
  end

上はデプロイ手順です。これらのメソッドは先の例とほぼ同じですが、わずかな変更があります。それぞれの手順を見てみましょう。

  def check_changed_files
    return unless `git -C #{APPLICATION_PATH} status --short | wc -l`
                  .to_i.positive?
    abort('Files changed, please commit before deploying.')
  end

アプリのデプロイにはローカルのコードを使っているので、ファイルが変更されているかどうかをチェックして、変更がある場合はデプロイを行わないようにするのがよい方法です。この手順ではファイルの作成や変更を検出するのにgit status --shortを使っています。-Cフラグはgitでチェックする対象(この例ではblog)を定義します。不要ならこの手順を取り除くこともできますが、おすすめしません。

  def copy_gemfile
    system("cp #{APPLICATION_PATH}/Gemfile* .")
  end

上は、デプロイのたびにblogのルートディレクトリにあるGemfileとGemfile.lockをコピーします。これによって、デプロイが完了する前にすべてのgemがインストールされるようになります。

  def compress_application
    system("tar -zcf #{APPLICATION_FILE} #{APPLICATION_PATH}")
  end

メソッド名からわかるとおり、この手順ではアプリ全体を圧縮して1つのファイルにします。このファイルは後でコンテナに含められます。

  def build_application_container
    system("docker build -t #{APPLICATION_CONTAINER}:#{current_git_rev} .")
  end

このメソッドは、コンテナのビルド手順を実行します。このときに依存ライブラリやgemをすべてインストールします。Gemfileが変更されるたびにDockerでそのことが検出されてインストールが行われるので、依存ライブラリの更新を気にする必要はありません。依存ライブラリが変更されるたびに多少時間がかかります。変更が何もない場合、Dockerはキャッシュを使うので手順の実行はほぼ瞬時に完了します。

  def push_container
    system("docker push #{APPLICATION_CONTAINER}:#{current_git_rev}")
  end

このメソッドは、Docker Registryに新しいコンテナをアップロードします。最新のコミットハッシュをgitで取得しているこのcurrent_git_revメソッドにご注目ください。各デプロイの識別にはこのコミットハッシュを使います。アップロードしたコンテナはすべてDockerHubコンソールで確認できます。

  def remote_deploy
    system("#{ssh_command} docker pull "\
           "#{APPLICATION_CONTAINER}:#{current_git_rev}")
    system("#{ssh_command} 'docker stop \$(docker ps -q)'")
    system("#{ssh_command} docker run "\
             "--name #{deploy_user} "\
             "#{APPLICATION_CONTAINER}:#{current_git_rev}")
  end

ここでは以下の3つを行っています。

  • docker pull: リモートサーバーにアップロードしたコンテナをpullします。ssh_commandメソッド呼び出しは、リモートコマンドの送信が必要になるたびに、コードの重複を避けるための単なるラッパーです。
  • docker stop $(docker ps -q): 新しいコンテナを実行するときにポート番号が衝突しないようにするため、実行中のコンテナをすべて停止します。
  • docker run: 正しいタグを与えて新しいコンテナを起動し、現在のgitユーザーとタイムスタンプに基づいて名前を付けます。これは、現在実行中のアプリをデプロイしたユーザーを知る必要がある場合に便利です。名前を確認するには、リモートサーバーでdocker psコマンドを入力します。
CONTAINER ID        IMAGE                                        COMMAND                  CREATED             STATUS              PORTS                    NAMES
01d777ef8d9a        mydockeruser/application-container:aa2da7a   "/bin/sh -c 'cd /t..."   10 minutes ago      Up 10 minutes       0.0.0.0:3000->3000/tcp   mygituser-29.03.17_01.09.43
if ARGV.empty?
  abort("Please inform action: \n\s- deploy")
end
application = Deployer.new(ARGV[0])

begin
  application.execute!
rescue Interrupt
  puts "\nDeploy aborted."
end

上はCLIから引数を受け取って、アプリのデプロイを実行します。Ctrl-Cでデプロイをキャンセルすると、rescueブロックでわかりやすいメッセージが表示されます。

アプリをデプロイする

この時点でのフォルダ構造は次のようになっているはずです。

.
├── blog
│   ├── app
│   ├── bin
... (application files and folders)
├── deployer.rb
├── Dockerfile

次は、アプリを実行してデプロイしましょう。

$ ruby deployer.rb deploy

コマンドが実行されるたびに出力が表示されます。すべての出力結果は、最初の例の手動実行とほぼ同じです。

Sending build context to Docker daemon 4.846 MB
Step 1/9 : FROM ruby:2.3.1-slim
 ---> e523958caea8
Step 2/9 : COPY Gemfile* /tmp/
 ---> Using cache
 ---> f103f7b71338
Step 3/9 : WORKDIR /tmp
 ---> Using cache
 ---> f268a864efbc
Step 4/9 : RUN gem install bundler &&     apt-get update &&     apt-get install -y build-essential libsqlite3-dev rsync nodejs &&     bundle install --path vendor/bundle
 ---> Using cache
 ---> 7e9c77e52f81
Step 5/9 : RUN mkdir -p /app/vendor/bundle
 ---> Using cache
 ---> 1387419ca6ba
Step 6/9 : WORKDIR /app
 ---> Using cache
 ---> 9741744560e2
Step 7/9 : RUN cp -R /tmp/vendor/bundle vendor
 ---> Using cache
 ---> 5467eeb53bd2
Step 8/9 : COPY application.tar.gz /tmp
 ---> b2d26619a73c
Removing intermediate container 9835c63b601b
Step 9/9 : CMD cd /tmp &&     tar -xzf application.tar.gz &&     rsync -a blog/ /app/ &&     cd /app &&     RAILS_ENV=production bundle exec rake db:migrate &&     RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000
 ---> Running in 8fafe2f238f1
 ---> c0617746e751
Removing intermediate container 8fafe2f238f1
Successfully built c0617746e751
The push refers to a repository [docker.io/mydockeruser/application-container]
e529b1dc4234: Pushed
08ee50f4f8a7: Layer already exists
33e5788c35de: Layer already exists
c3d75a5c9ca1: Layer already exists
0f94183c9ed2: Layer already exists
b58339e538fb: Layer already exists
317a9fa46c5b: Layer already exists
a9bb4f79499d: Layer already exists
9c81988c760c: Layer already exists
c5ad82f84119: Layer already exists
fe4c16cbf7a4: Layer already exists
aa2da7a: digest: sha256:a9a8f9ebefcaa6d0e0c2aae257500eae5d681d7ea1496a556a32fc1a819f5623 size: 2627
aa2da7a: Pulling from mydockeruser/application-container
1fad42e8a0d9: Already exists
5eb735ae5425: Already exists
b37dcb8e3fe1: Already exists
50b76574ab33: Already exists
c87fdbefd3da: Already exists
f1fe764fd274: Already exists
6c419839fcb6: Already exists
4abc761a27e6: Already exists
267a4512fe4a: Already exists
18d5fb7b0056: Already exists
219eee0abfef: Pulling fs layer
219eee0abfef: Verifying Checksum
219eee0abfef: Download complete
219eee0abfef: Pull complete
Digest: sha256:a9a8f9ebefcaa6d0e0c2aae257500eae5d681d7ea1496a556a32fc1a819f5623
Status: Downloaded newer image for mydockeruser/application-container:aa2da7a
01d777ef8d9a
c3ecfc9a06701551f31641e4ece78156d4d90fcdaeb6141bf6367b3428a2c46f

出力結果は、ハッシュやDockerキャッシュの違いによって異なることがあります。最後に、上のように2つのハッシュが出力されます。

01d777ef8d9a
c3ecfc9a06701551f31641e4ece78156d4d90fcdaeb6141bf6367b3428a2c46f

1つ目の短いハッシュは、停止したコンテナのハッシュです。最後の長いハッシュは、新たに実行中のコンテナのハッシュです。

これで、リモートサーバーのIPアドレスにアクセスするとアプリが実行されていることを確認できます。

Semaphoreで継続的デリバリー(CD)する

本チュートリアルのスクリプトを使って、アプリをSemaphoreに自動デプロイできます。やり方を見てみましょう。

最初に、「Project Settings」でDocker support付きのプラットフォームを指定します。

SemaphoreのProjectページで、「Set Up Deployment」をクリックします。

「Generic Deployment」を選択します。

「Automatic」を選択します。

Gitのブランチを選択します(普通はmaster)。

ここではアプリをデプロイしたいだけなので、ローカルコンピュータで実行するときと同じ方法でデプロイスクリプトを実行します。

rbenv global 2.3.1
docker-cache restore
ruby deployer.rb deploy
docker-cache snapshot

2つのdocker-cacheコマンドにご注目ください。これらがビルドしたイメージを取り出しを行うので、ゼロからビルドする必要はありません。ローカルでの実行と同様、最初は少し時間がかかりますが、次回からは速くなります。詳しくはSemaphoreの公式ドキュメントをご覧ください。

また、rbenv global 2.3.1コマンドをメモしておきましょう。これは、スクリプトの実行に必要な現在のRubyのバージョンを設定するためのものです。別の言語を使う場合は、必要な環境を設定する必要があります。

次の手順では、リモートサーバーへのアクセスに使うSSHキーのアップロード(必要な場合)と、新しいサーバーへの名前付けを行っています。完了すると、コードをmasterブランチにpushするたびにこのスクリプトが実行され、定義済みのリモートサーバーにアプリがデプロイされます。

その他の自動化可能なコマンド

この後のセクションでは、便利な自動化コマンドをいくつかご紹介します。

現在のバージョン

現在実行中のアプリのバージョンをトラックするには、コンテナのTagに情報を記述します。

現在実行中のバージョンを取り出すには、以下のコードが必要です。

def current
  remote_revision = `#{ssh_command} docker ps | grep -v CONTAINER | awk '{print $2}' | rev | cut -d: -f1 | rev`.strip

  abort('No running application.') if remote_revision == ''

  current_rev = `git show --ignore-missing --pretty=format:'%C(yellow)%h\
%C(blue)<<%an>> %C(green)%ad%C(yellow)%d%Creset %s %Creset'\
#{running_revision} | head -1`.strip
  if current_rev.empty?
    puts 'Local revision not found, please update your master branch.'
  else
    puts current_rev
  end
  deploy_by = `#{ssh_command} docker ps --format={{.Names}}`
  puts "Deploy by: #{deploy_by}"
end

各行の動作について解説します。

remote_revision = `#{ssh_command} docker ps | grep -v CONTAINER | awk '{print $2}' | rev | cut -d: -f1 | rev`.strip

上のコマンドは以下を行います。

  • docker psでリモートコンテナのステータス出力を取得
  • grep -v CONTAINERで出力からヘッダを除去
  • awk '{print $2}'で2番目のカラム(image name:tag)を取得
  • 残りのコマンドでimage nameと:を削除し、残りの部分とコミットハッシュを返す
  • 返された文字列の最終行の改行を.stripで削除
abort('No running application.') if remote_revision == ''

コンテナが1つも実行されていない場合や、コミットが1つも見つからない場合はコマンド実行をやめます。

current_rev = `git show --ignore-missing --pretty=format:'%C(yellow)%h\
%C(blue)<<%an>> %C(green)%ad%C(yellow)%d%Creset %s %Creset'\
#{running_revision} | head -1`.strip

このコマンドは、git logにマッチするコンテナハッシュを検索して書式を整えます。

if current_rev.empty?
   puts 'Local revision not found, please update your master branch.'
else
  puts current_rev
end

このコミットが現在のgit historyにない場合、ユーザーにリポジトリの更新を促します。これは、新しいコミットがローカルコピーからまだrebaseされていない場合に発生することがあります。コミットがある場合は、ログ情報を出力します。

deploy_by = `#{ssh_command} docker ps --format={{.Names}}`

このコマンドは、現在実行中のコンテナ名を返します。コンテナ名にはユーザー名とタイムスタンプが含まれます。

puts "Deploy by: #{deploy_by}"

上のコマンドは、デプロイを行ったユーザーとタイムスタンプを出力します。

ログ

多くのアプリはログを出力するので、場合によってはログの面倒も見なければなりません。Dockerに組み込まれているログシステムを使うと、シンプルなSSH接続でアプリのログに簡単にアクセスできるようになります。

アプリからログを出力するには、以下を入力します。

def logs
  puts 'Connecting to remote host'
  system("#{ssh_command} 'docker logs -f --tail 100 \$(docker ps -q)'")
end

docker logsコマンドは、アプリで生成されたログをすべて出力します。-fフラグは、接続を保持してすべてのログをストリームとして読み出せるようにします。--tailフラグは、出力する古いログの最大行数を指定します。最後の$(docker ps -q)は、リモートホストで実行中のコンテナごとにIDを返します。今はアプリを実行しているだけなので、コンテナをすべて取り出しても問題ありません。

メモ: 本記事のサンプルアプリはすべてのログをファイルに書き込むので、Dockerにはログを一切出力しません。この振る舞いは、アプリの起動時にRAILS_LOG_TO_STDOUT=true環境変数で変更できます。

Dockerのインストールとログイン

新しいホストでは、必要なインストールや設定をsetupコマンド一発でできるようにすると便利です。

インストールとログインの2つの手順を完了させます。

def docker_setup
  puts 'Installing Docker on remote host'
  system("#{ssh_command} -t 'wget -qO- https://get.docker.com/ | sh'")

  puts 'Adding the remote user to Docker group'
  system("#{ssh_command} 'sudo usermod -aG docker #{HOST_USER}'")

  puts 'Adding the remote user to Docker group'
  system("#{ssh_command} -t 'docker login}'")
end

各コマンドの動作について解説します。

system("#{ssh_command} -t 'wget -qO- https://get.docker.com/ | sh'")

このコマンドはDockerのインストールスクリプトを実行します。リモートユーザーのパスワード入力を促すには-tフラグが必要です。パスワード入力を求められたら入力します。

system("#{ssh_command} 'sudo usermod -aG docker #{HOST_USER}'")

このコマンドは、Dockerグループにリモートユーザーを追加します。これは、sudoせずにdockerコマンドを実行する場合に必要です。

system("#{ssh_command} -t 'docker login'")

更新されたアプリをダウンロードするためにログインが必要なので、このコマンドが必要になります。-tフラグは、ログイン入力できるようにするためのものです。

ロールバック

新しいアプリの実行で何か問題が起きたら、直前のバージョンにいつでもロールバックできることが重要です。Dockerコンテナのアプローチを用いたことで、デプロイされたすべてのバージョンがホスト上に保存されているので、即座にロールバックを開始できます。

次のコードスニペットをご覧ください。

def rollback
  puts 'Fetching last revision from remote server.'
  previous_revision = `#{ssh_command} docker images | grep -v 'none\|latest\|REPOSITORY' | awk '{print $2}' | sed -n 2p`.strip
  abort('No previous revision found.') if previous_revision == ''
  puts "Previous revision found: #{previous_revision}"
  puts "Restarting application!"
  system("#{ssh_command} 'docker stop \$(docker ps -q)'")
  system("#{ssh_command} docker run --name #{deploy_user} #{APPLICATION_CONTAINER}:#{previous_revision}")
end

各手順の動作について見てみましょう。

  puts 'Fetching last revision from remote server.'
  previous_revision = `#{ssh_command} docker images | grep -v 'none\|latest\|REPOSITORY' | awk '{print $2}' | sed -n 2p`.strip
  abort('No previous revision found.') if previous_revision == ''

このコマンドは、リモートホスト上にあるすべてのDockerイメージの中から直前のコンテナtagをgrepします。このタグはgitコミットの短いハッシュになっていて、アプリのロールバックを参照するときに使われます。直前のDockerイメージがない場合は、ロールバックをやめます。

  system("#{ssh_command} 'docker stop \$(docker ps -q)'")

このコマンドは、実行中のコンテナをすべてシャットダウンして、直前のコンテナを起動できるようにします。

  system("#{ssh_command} docker run --name #{deploy_user} #{APPLICATION_CONTAINER}:#{previous_revision}")

このコマンドは、直前の手順で見つかったタグを用いてアプリを起動します。デプロイメソッド(deploy_user)で使われているのと同じ命名ルールを利用できます。

まとめ

本チュートリアルのすべての手順を行うと、ソフトウェアをデプロイする自動ツールが完全に動くようになるはずです。このツールは、アプリを簡単にデプロイできなければならないが、Herokuなどの自動化された環境にホスティングできない場合に便利です。

このツールが有用だとお思いいただけましたら、お気軽に本チュートリアルを共有してください。疑問点などがございましたら、ぜひ元記事にコメントをどうぞ。

皆さまが楽しくリリースできますように。

追伸: Dockerを用いた継続的デリバリー(CD)にご関心がおありでしたら、SemaphoreのDocker platformをぜひチェックしてください。タグ付きのDockerイメージのレイヤキャッシュを完全にサポートしています。

関連記事

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

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


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

$
0
0

概要

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

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

Rails開発者なら誰でも、アセットの読み込みに非常に時間がかかることを痛感しています。アセット読み込みを高速化する便利なコツをご紹介します。

コツ

  • 1つの巨大なファイルに何もかもバンドルしないこと。CDNを用いて、必要なページでだけrequireしましょう(CDNはCKeditorなどのツールでも使われています)。
  • I18n-jsは翻訳を個別のファイルに保持します。必要な言語だけを設定するようにしましょう。さもないと、新しいライブラリを導入するたびに翻訳ファイルがあっという間に肥大化してしまいます。必要な言語だけを設定することでコンパイル済みファイルの肥大化を回避でき、ひいてはコンパイルとページ読み込みを高速化できます。
config.i18n.available_locales = %i(en)
  • therubyracerGemfileから削除しましょう。このgemはメモリを大量に消費するためです。代わりに、最新バージョンのNodeをインストールします。
  • SASSファイルやSCSSファイルでrequirerequire_treerequire_selfを使わないこと。これらはあまりに素朴であり、Sassファイルでうまく動作しません。代わりに、Sassネイティブの@importディレクティブを使いましょう。sass-railsはこのディレクティブをカスタマイズしてRailsプロジェクトの慣習に統合します。
  • SASSファイルやSCSSファイルでの@importディレクティブの使い方には注意が必要です。@import 'compass/css3/flexbox'のように個別にインポートできる状況であれば、@import 'compass';のようにパッケージアセットをまるごとインポートするのは避けましょう。
  • JavaScriptやCoffeeScriptのマニフェストでrequire_tree .を使うのは避けましょう。次のように、アプリの管理(admin)パネルが独自のアセットを持つ場合を考えてみます。
//** assets/javascripts/admin/admin.js

//= require admin/tab.js
//= ...

//** assets/javascripts/application.js

//= require 'something'
//= require_tree . // 悪手: adminのアセットまでrequireされてしまう
  • アセットをコンパイルするときのログをチェックしましょう。デフォルトのロガーをオーバーライドしてDEBUGモードでアクセスしたときの状態を確認するのは簡単です。
# /lib/tasks/assets.rake

require 'sprockets/rails/task'

Sprockets::Rails::Task.new(Rails.application) do |t|
  t.logger = Logger.new(STDOUT)
end

まとめ

上のルールはコンパイル済みアセットを高速化するのに有用です。この概念を実証するcintrzyk/sprockets-tipsをご覧いただければ、Railsのサンプルアプリでより詳細な部分を確認できます。

Rails 5.1以降、アセットの管理方法は多様になりました。私はYarnとWebpackによる管理を強くおすすめします。その有用性については、ぜひ私を信じてください。アセットの依存性管理、効率の高いコンパイルプロセス、ページを更新せずにコードを動的に再読み込みする機能、ES6サポート、PostCSSなど、多くのメリットを得られます。

関連記事

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

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

$
0
0

概要

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

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

ある少年がナイフを一本もらいました。「これはとてもよく切れるナイフだから」という触れ込みでしたが、いざ少年が肉などを切ろうとしてみるとどうもうまく切れないので、少年は「このナイフ、言うほど大したことないな」と思ってしまいました。しかしナイフをくれた人が「ちょい待ち、ナイフの刃をもう少し傾けて。それからナイフの持ち方がよくないからこう持ってごらん」とアドバイスしたところ、今度は見事ナイフで肉を切ることができました。

オープンソースソフトウェアや人生はちょうどこのナイフのように、正しい使い方を学ばないとうまくいかないことがあります。しかも、ナイフそのものに避けがたい問題があることもあります。さらに、ネットで見つけたナイフの使い方の情報がひどいしろもので、始末の悪いことに特定の状況ではその情報が適切だったりすることもあります。人生が面倒なのは今に始まったことではありませんが、ともあれ本記事ではRails.application.routes.url_helpersというナイフについて書いてみたいと思います。

Railsコントローラのコンテキストの外でURLを生成する状況は非常に多いので、シリアライザやジョブなど、その機能が自動的には使えないような場所でこのモジュールの機能が必要になることも非常によくあります。ネットの情報では、このモジュールに直接アクセスすることを何年もの間気軽に勧めていて、しばらくの間これで何の問題も生じませんでした。

しかし運の悪いことに、最近のバージョンのRailsではこの方法で問題が生じるようになりました(問題を指摘しているGithub issue修正のPRを参照)。これによる問題を明らかにするため、簡単なテスト用Railsアプリをセットアップしてみました。関連するコードを以下に示します。

# routes.rb
Rails.application.routes.draw do
  resources :things do
    collection do
      get :faster
    end
  end
end
# app/whatever/url_helper.rb
class UrlHelper
  include Singleton
  include Rails.application.routes.url_helpers
end
# app/controllers/things_controller.rb
class ThingsController < ApplicationController
  def index
    things_json = (1..100).map do |i|
      {
        id: i,
        url: Rails.application.routes.url_helpers.thing_url(i, host: 'localhost')
      }
    end

    render json: things_json
  end

  def faster
    things_json = (1..100).map do |i|
      {
        id: i,
        url: UrlHelper.instance.thing_url(i, host: 'localhost')
      }
    end

    render json: things_json
  end
end

このindexアクションでは、StackOverflowのアドバイスどおりにコードでモジュールを直接呼び出しています。fasterアクションの方では、このモジュールをincludeするヘルパークラスを使っています。Railsではこのモジュールをincludeして使うことが推奨されているようです。

2つのアプローチの実行結果を並べて見てみましょう。abと単一のRailsインスタンスで小さなテストを実行してみました(不要な出力が大量にあったので省略してあります)。

➜ ab -n 1000 http://127.0.0.1:3000/things

Concurrency Level:      1
Time taken for tests:   18.155 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      4815000 bytes
HTML transferred:       4485000 bytes
Requests per second:    55.08 [#/sec] (mean)
Time per request:       18.155 [ms] (mean)
Time per request:       18.155 [ms] (mean, across all concurrent requests)
Transfer rate:          259.00 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       1
Processing:    15   18   2.2     17      42
Waiting:       15   18   2.2     17      42
Total:         15   18   2.2     18      42
 ➜  ab -n 1000 http://127.0.0.1:3000/things/faster

 Concurrency Level:      1
 Time taken for tests:   8.540 seconds
 Complete requests:      1000
 Failed requests:        0
 Total transferred:      4815000 bytes
 HTML transferred:       4485000 bytes
 Requests per second:    117.09 [#/sec] (mean)
 Time per request:       8.540 [ms] (mean)
 Time per request:       8.540 [ms] (mean, across all concurrent requests)
 Transfer rate:          550.58 [Kbytes/sec] received

 Connection Times (ms)
               min  mean[+/-sd] median   max
 Connect:        0    0   0.1      0       1
 Processing:     7    8   1.3      8      23
 Waiting:        7    8   1.3      8      23
 Total:          7    8   1.3      8      23

最初の例(indexメソッド)ではモジュールを直接呼び出していますが、平均して18msを要し、スループットは55リクエスト/秒です。本番で調子の良いときにはリクエストに5秒かかるのであれば「そんなに悪くないんじゃ?」とお思いかもしれません。しかしモジュールを直接呼び出すのではなく、単にincludeする方はどうでしょうか?こちら(fasterメソッドの方)は平均して8msでスループットは117リクエスト/秒と、最初のアプローチのほぼ倍速になっています。私は名シェフではありませんが、このナイフを正しく持てば適切に肉を切れるのです。

結論: Rails.application.routes.url_helpersを直接呼ばず、このモジュールをクラスにincludeすることで、コードは高速化します。

関連記事

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

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

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

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

$
0
0

概要

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

@jnchitoさんの以下の記事も合わせてどうぞ。

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

RSpec 3.7登場前のfeature spec は、実物(またはヘッドレス)のブラウザ環境でJavaScriptの絡むアプリのやり取りをフルスタックでテストする手段でした。最近リリースされたRSpec 3.7では、Railsのsystem testを元にしたsystem specが追加されました。Rails 5.1ではActionDispatch::SystemTestCaseが導入され、実際のブラウザでのテストに使えるようになりました。設定済みのCapybaraラッパーが提供され、Railsフレームワークに組み込まれている多くの機能を利用できるようになりました。設定済みのCapybaraのおかげで、従来は正しく設定するためにトリッキーになりがちだった手動設定の手間が大きく軽減されました。feature specの代わりにsystem specを使うメリットは次のとおりです。

  1. テストが終わるとデータベースの変更が自動でロールバックされるので、database_cleaner gemを用いてロールバックを手動で設定する必要がない。
  2. driven_byを使うと、specごとにブラウザを簡単に切り替えられる。
  3. テストが失敗すると即座にスクリーンショットを自動撮影し、ターミナルにもスクリーンショットをインライン表示する。この機能は事前設定済みなので、capybara-screenshot gemなどが不要になる。

上述のメリットに加えて、RSpecチームがRails 5.1以降ではfeature specよりもsystem specを推奨している点も強調しておきたいと思います。

RSpecのsystem specをヘッドレスChrome向けにセットアップする

まずはRailsプロジェクトを新規作成しましょう。いつものように--skip-testオプションを追加して、RailsデフォルトのminitestではなくRSpecを使うようにします。

rails --version
Rails 5.2.0.beta2
rails new rspec-system-specs --skip-test --skip-active-storage

セットアップでGemfilerspec-railsを追加する作業以外で最も面倒なのは、ヘッドレスChromeブラウザのテストに必要なgemを見極めることでしょう。必要なgemのリストを以下に示します。

group :development, :test do
  # Capybara system testingとselenium driverのサポートを追加
  gem 'capybara', '~> 2.16.1'
  gem 'selenium-webdriver', '~> 3.8.0'
  # Chromeでのシステムテスト実行に使うchromedriverを簡易インストール
  gem 'chromedriver-helper', '~> 1.1.0'
  gem 'rspec-rails', '~> 3.7.2'
end

chromedriverがインストールされていることを確認します(訳注: 以下はMacでhomebrewを使う場合です)。

brew install chromedriver
chromedriver --version
#> ChromeDriver 2.34.522932 (4140ab217e1ca1bec0c4b4d1b148f3361eb3a03e)

spec/test_helper.rbspec/rails_helper.rbを生成します。

rails g rspec:install

system specを書く

ここでは単純なhome#indexアクションがroutes.rbのrootとして設定されていて、app/views/home/index.html.erbで以下をレンダリングするとします。

<h1 id="title">Hello World</h1>

最初のsystem specを次のようにspec/system/home_spec.rbに実装できます。

require 'rails_helper'

describe 'Homepage' do
  before do
    driven_by :selenium_chrome_headless
  end

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

ここではヘッドレスChromeを使いたいので、:selenium_chrome_headlessドライバを設定しています。Capybaraでこの他に提供されている登録済みドライバは、:rack_test:selenium:selenium_chromeです。ブラウザの解像度などの高度な設定オプションについてのドキュメントは、driven_byをご覧ください。

ちゃんと動くかどうか確認します。

$ rspec
Puma starting in single mode...
* Version 3.11.0 (ruby 2.5.0-p0), codename: Love Song
* Min threads: 0, max threads: 4
* Environment: test
* Listening on tcp://127.0.0.1:50713
Use Ctrl-C to stop
.

Finished in 1.41 seconds (files took 2.04 seconds to load)
1 example, 0 failures

ドライバをspecごとに設定したくない場合は、spec_helper.rbでデフォルトのグローバル設定を以下のように行なえます。

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium_chrome_headless
  end
end

後は必要に応じて個別のspecファイルでデフォルトのドライバ設定を上書きします。

テスト失敗時のブラウザスクリーンショット

system specで特筆したい点は、specが失敗したときにブラウザのスクリーンショットを自動で撮影し、ターミナルにインライン出力してくれる便利な機能があることです。

spec失敗時

この機能はRailsのシステムテストに組み込まれているので、feature specのようにcapybara-screenshotなどによるサポートを手動で設定する必要がありません。

JavaScriptのテスト

ここまではサーバー側でのコンテンツレンダリングのテストだけなので、今度はクライアント側のJavaScriptを追加して、JavaScriptが動くブラウザ(ここではヘッドレスChrome)がsystem specで使えることを示してみましょう。

$ ->
  $('#title').text('Hello Universe')

specのアサーションをexpect(page).to have_content 'Hello Universe'に変更すると、クライアント側でのJavaScript変更のspecテストはこれまで同様パスします。

$ rspec
Finished in 1.99 seconds (files took 2.44 seconds to load)
1 example, 0 failures

データベースの自動ロールバック

上述したように、system specでのデータベース変更は自動的にロールバックされます。ページにデータベース出力を少し追加してテストしてみましょう。

<h1 id="title">Hello World</h1>
<p>
  Planet count: <%= Planet.count %>
</p>

specを変更してレコードをseedし、出力されたレコードのcountのアサーションを行うspec exampleを新たに追加します。

describe 'Homepage' do
  before do
    driven_by :selenium_chrome_headless
    puts 'creating a planet'
    Planet.create!(name: 'Mars')
  end

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

  it 'shows planet count' do
    visit root_url
    expect(page).to have_content 'Planet count: 1'
  end
end

rspecの結果は次のようになります。

$ rspec --format d
creating a planet
  shows greeting
# spec example追加後のデータベース自動ロールバック
creating a planet
  shows planet count

Finished in 1.52 seconds (files took 1.03 seconds to load)
2 examples, 0 failures

できました!feature specではspec example実行後にデータベースの状態をクリーンアップするためにdatabase_cleaner gemの設定が必要でしたが、これで不要になりました。

本記事のデモに用いた例のソースコードはGitHubに置いてあります。

関連記事

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

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

Rails: ActiveRecord関連付けのpreload/eager-loadをテストする2つの方法(翻訳)

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

$
0
0

こんにちは、hachi8833です。ついに東京都のインフルエンザ流行が警報を超えました。剣呑剣呑。

1月最後のRailsウォッチ、いってみましょう。

Rails: 今週の改修

今週も主に最近のcommitから見繕いました。

assert_changesが式の変更を認識するよう修正

# activesupport/lib/active_support/testing/assertions.rb#159
-        if to == UNTRACKED
-          error = "#{expression.inspect} didn't change"
-          error = "#{message}.\n#{error}" if message
-          assert before != after, error
-        else
+        error = "#{expression.inspect} didn't change"
+        error = "#{error}. It was already #{to}" if before == to
+        error = "#{message}.\n#{error}" if message
+        assert before != after, error
+
+        unless to == UNTRACKED

つっつきボイス:assert_changesに評価前の値が渡ってたということか」

nil/falseチェックをtrue/falseチェックに修正

activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#86
       def initialize(connection, logger, connection_options, config)
         super(connection, logger, config)

-        @active     = nil
+        @active     = true
         @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
       end
...
       def active?
-        @active != false
+        @active
       end

つっつきボイス: 「これは直したいやつ: SQLite3ってまず使わないけど」「そういえばSQLite3ってもうRailsのデフォルトから外れてる?」「まだデフォルトはSQLite3ですね」

ActionMailer::Base.defaultのprocの引数の個数(アリティ)を1に戻した

# actionmailer/lib/action_mailer/base.rb#901
+      def compute_default(value)
+        return value unless value.is_a?(Proc)
+
+        if value.arity == 1
+          instance_exec(self, &value)
+        else
+          instance_exec(&value)
+        end
+      end

もしかしてarityって、binaryとかunaryとかtrinaryの「-ary」から来てるんでしょうか。とても造語っぽい。


つっつきボイス:Proc#arityって可変長の場合マイナスの値を返すのか!」「でも確かにRubyとしてはここでInteger以外の値を返したくないだろうな」「arityはプログラミング業界の造語っぽいですねー」「私の手元にある英語辞書串刺し検索でも類似のもの出てこないです」

参考: Ruby 2.5.0リファレンスマニュアル Proc#arity
参考: Wikipedia-ja アリティ

has_many :through関連付けでレコードがdestroyされてない状態でカウンタキャッシュを更新しないよう修正

# activerecord/lib/active_record/associations/has_many_through_association.rb#145
           case method
           when :destroy
             if scope.klass.primary_key
-              count = scope.destroy_all.length
+              count = scope.destroy_all.count(&:destroyed?)
             else
               scope.each(&:_run_destroy_callbacks)
               count = scope.delete_all

つっつきボイス: 「ふむう、scope.destroy_all.lengthの返す値がタイミングによって適切でなかったってことなのかな」

参考: ActiveRecord::Relation#destroy_all

JRuby向け: opt = options.dupを追加

# railties/lib/rails/generators/app_base.rb#316
       def convert_database_option_for_jruby
         if defined?(JRUBY_VERSION)
-          case options[:database]
-          when "postgresql" then options[:database].replace "jdbcpostgresql"
-          when "mysql"      then options[:database].replace "jdbcmysql"
-          when "sqlite3"    then options[:database].replace "jdbcsqlite3"
+          opt = options.dup
+          case opt[:database]
+          when "postgresql" then opt[:database] = "jdbcpostgresql"
+          when "mysql"      then opt[:database] = "jdbcmysql"
+          when "sqlite3"    then opt[:database] = "jdbcsqlite3"
           end
+          self.options = opt.freeze

元のPR #31641では、frozen_string_literalを外してどうにかしようとしていたのをy-yagiさんが「外さない方がいい」とdupでやる方法を勧めてこうなったようです。

changes_appliedのパフォーマンス低下を修正

PR #30985 Move Attribute and AttributeSet to ActiveModelで低下したパフォーマンスをkamipoさんが修正しました。

Before:

Warming up --------------------------------------
create_string_columns
                        73.000  i/100ms
Calculating -------------------------------------
create_string_columns
                        722.256  (± 5.8%) i/s -      3.650k in   5.073031s

After:

Warming up --------------------------------------
create_string_columns
                        96.000  i/100ms
Calculating -------------------------------------
create_string_columns
                        950.224  (± 7.7%) i/s -      4.800k in   5.084837s

つっつきボイス: 「変更量多かったので結果を貼ってみました」「変更内容は繰り返し多いですけどね: 無駄な参照を減らして毎回総ナメしなくていいようにするなどして高速化したように見える」

# activerecord/lib/active_record/attribute_methods/dirty.rb#113
       # Alias for +changed+
       def changed_attribute_names_to_save
-        changes_to_save.keys
+        mutations_from_database.changed_attribute_names
       end

HasOneThroughAssociation#build_recordを修正

最新のRails 5.2 (と5.1も)で、HasOneThroughAssociation#build_recordメソッドを使ってHasOneThroughAssociationを手動でビルドできない。
再現用のサンプルアプリでこのメソッドを呼ぶと以下がraiseされる:

NameError: undefined local variable or method `through_association' for #
<ActiveRecord::Associations::HasOneThroughAssociation:0x007fa209d2f7a0>

HasOneThroughAssociationでは、(HasManyThroughAssociationオブジェクトと異なり)#build_recordメソッドでのThroughAssociationミックスインに必要な#through_associationが定義されてないっぽい。
#31762より大意


つっつきボイス: 「ま、HasOneみんなあんまり使ってないけど」「たまに必要ー」

bulk_change_table<< procs#concatに変更

# activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#375
             if respond_to?(method, true)
               sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) }
               sql_fragments << sqls
-              non_combinable_operations << procs if procs.present?
+              non_combinable_operations.concat(procs)
             else
               execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
               non_combinable_operations.each(&:call)

つっつきボイス: 「修正は純粋な高速化」「そういえばこのbulk_change_tableってkamipoさんが最近何か追加したんじゃなかったかな?」「そういえばごく最近のウォッチ↓で扱った#31331ですね」

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

リレーションに渡していたassociation引数を削除

# activerecord/lib/active_record/association_relation.rb#3
module ActiveRecord
   class AssociationRelation < Relation
-    def initialize(klass, table, predicate_builder, association)
-      super(klass, table, predicate_builder)
+    def initialize(klass, association)
+      super(klass)
       @association = association
     end

associationはまず要らないだろうということで削除されました。


つっつきボイス: 「他の引数から辿れるならassociationなくてもいいな」「お、こうやってちゃんとキーワード引数使って修正してる↓: 修正前のは古い書き方」「{}は残ってるけど、superに渡したりするかもだしインターフェイス変えるわけにはいかないか」

# activerecord/lib/active_record/relation.rb#25
-    def initialize(klass, table, predicate_builder, values = {})
+    def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})

Rails

Bootstrap 4がついに正式リリース

先週のウォッチを公開した直後にリリースされました。


getbootstrap.comより


つっつきボイス: 「もうね、どんだけ待たされたのかと」「Bootstrapのトップページが変わってから半年経たずにリリースだから早い方かな」「4のテンプレートもとっくに販売されてますしね」

bootstrap_form gemがBootstrap 4に対応(Awesome Rubyより)

<!-- リポジトリより -->
<%= bootstrap_form_for(@user, layout: :inline) do |f| %>
  <%= f.email_field :email, hide_label: true %>
  <%= f.password_field :password, hide_label: true %>
  <%= f.check_box :remember_me %>
  <%= f.submit %>
<% end %>

つっつきボイス:bootstrap-rubyって人たちがいるのか: 本家Bootstrapのリポジトリとは別なのかな?」「どうやら本家のはtwbsにあるtwbs/bootstrap-rubygemですね: ということはbootstrap-rubyは公式ではないと」「なかなかややこしい」

このgemではDry-rbのDry-auto_injectを使っています。

Ruby: Dry-rb gemシリーズのラインナップと概要

kan: 軽量な認証/承認ライブラリ(Awesome Rubyより)

# リポジトリより
class Post::AdminAbilities
  include Kan::Abilities

  register :read, :edit, :delete { |user, _| user.admin? }
end

class Comments::Abilities
  include Kan::Abilities

  register 'read' { |_, _| true }
  register 'edit' { |user, _| user.admin? }

  register :delete do |user, comment|
    user.id == comment.user_id && comment.created_at < Time.now + TEN_MINUTES
  end
end

つっつきボイス: 「カン、って読むのかな?」「もしかするとCanCanCanのもじりか」「確かにCanCanCanっぽい匂いを感じる気がする」「まあこういうのって一度は作りたくなるんですよ: 既存のものがうまく合わないときとか」「モデルにアタッチするというよりAbilityregisterしていく、どっちかというとコントローラに結合させていく感じですかね」

rack-cache: HTTPキャッシュやバリデーションを行うRackミドルウェア(Awesome Rubyより)

# リポジトリより
# config/application.rb
config.action_dispatch.rack_cache = true
# or
config.middleware.use Rack::Cache,
   verbose:     true,
   metastore:   'file:/var/cache/rack/meta',
   entitystore: 'file:/var/cache/rack/body'

つっつきボイス:nginxの設定ファイルをRubyで書きたい人向けなのかな?」「全部Rubyで書きたい気持ちはわかる: nginxの設定ファイルの記法って結構特殊だし」「そういえば最近はnginxの設定ファイルを自動生成するサービスがありますね↓: 最終的には自分で仕上げないといけないですが」


nginxconfig.ioより

「rack-cacheが欲しい人がいるとすれば、ミドルウェアのないJava Serveletみたいな世界から来た人たちが直接下のレイヤをチューニングしたい、とかかも」

acts_as_votable: 投票機能に特化したActiveRecord向けgem(Awesome Rubyより)

# リポジトリより
class Post < ActiveRecord::Base
  acts_as_votable
end

@post = Post.new(:name => 'my post!')
@post.save

@post.liked_by @user
@post.votes_for.size # => 1

つっつきボイス: 「voteとかvotableって、最初パターンの名前か何かと思ったら違うみたいでした」「文字どおりの機能です: こういうモジュール化は割りと簡単に書けるから皆さんもぜひ一度やってみるといい: 特にメタプロの練習には絶好」「機能がここまで簡素で特化していれば大してバグらないだろうし」「きっと自分のプロジェクトで機能をgemに切り出して、せっかくだから公開したとかそういうノリなんでしょうね」
「そういえばRails 3ぐらいの頃はこういう↓acts asなんちゃらみたいなモジュール名がとても流行ってましたね: 今は流行ってないけど」

【Rails】acts_as_paranoid uniquenessでハマる

Railsでフィールドのキャッシュが無効になる問題をDSLで解決する

# 同記事より
class User < ActiveRecord::Base
  include Touchable
  touch :tasks, in_case_of_modified_fields: [:first_name, :last_name]
...
end
class Task < ActiveRecord::Base
  has_one owner, class_name: :User
end

つっつきボイス: 「キャッシュを明示的にクリアしないと残っちゃう的な問題ですね」「キャッシュを頻繁に更新したいことって多いんですか?」「多いですよー: 生成にすごく時間のかかるものとか、リクエストが来てからレンダリングしてたら到底間に合わないんで、こうやって変更時に明示的にtouchするとかしないと」

Paypal PayoutsをRailsで使う(RubyFlowより)

既存のMass Payは今後非推奨になるそうです。


cookieshq.co.ukより


つっつきボイス: 「PayPalのインターフェースって数が多すぎてマジでわからなくなったりするんだよなー」「Mass Payってちょいダサいネーミングな気が」「一括処理らしさは伝わるかな」

Rails + Reduxアプリを作ってみた


hackernoon.comより


つっつきボイス:SPA的なアプリやるならこういう構成でしょうね」

Railsのバックグランドジョブにsucker_punchを使う(RubyFlowより)

sucker_punchはメモリ上に展開するので非常に速い代わりに、ジョブキューが非常に多い場合には不向きだそうです。


つっつきボイス: 「sucker_punch、すごい名前w」「Redisサーバーとかを立てないでやりたいときにいいのかな」「昨年末のRails勉強会@Tokyoで、sucker_punchは最近メンテされてなさげって聞いた気がします」

sucker punch: いきなりのパンチ[殴打]、不意打ち、急襲

plan.io: Redmineのセキュリティチェックサービス


plan.ioより

ファクトリーで注意すべき点(Ruby Weeklyより)

# 同記事より
it "downcases the location on save" do
  user = create(:user, location: "Boston", first_name: "Joël")

  expect(user.location).to eq "boston"
end

つっつきボイス: 「ファクトリーでどこまでやるかというのはある」「ファクトリーって基本はデフォルト値の置き場かなー」「そう思う」「テストデータを一番簡単に作るやつ」「has manyとか増えてくると条件がどんどんややこしくなって結局自分でビルダー作る方が早かったり」

「Railsのfind_byにSQLインジェクションの脆弱性」に異議が唱えられる

** DISPUTED ** SQL injection vulnerability in the ‘find_by’ method in Ruby on Rails 5.1.4 and earlier allows remote attackers to execute arbitrary SQL commands via the ‘name’ parameter. NOTE: The vendor disputes this issue because the documentation states that this method is not intended for use with untrusted input.
nvd.nist.govより

参考: JVNDB-2017-011712

** 未確定 ** 本件は、脆弱性として確定していません。
jvndb.jvn.jpより


つっつきボイス:find_byにインジェクション…だと?」「disputed: 係争中ですね」「そもそもfind_byって信頼できるものしか渡さないでしょ普通」「そういえばSQLインジェクションをいっぱい集めたサイトありますよね↓」「そうそう、この辺の話」

# rails-sqli.orgより
params[:id] = "admin = 't'"
User.find_by params[:id]

巨大なRailsアプリでRubocopをパスするには

$ rubocop --format offences
11010 Metrics/LineLength
3140 Style/StringLiterals
2201 Style/FrozenStringLiteralComment
1891 Style/Documentation
…and thousands of rare offences
1 Style/ZeroLengthPredicate
 —
26154 Total

つっつきボイス: 「あるある、後からRubocopかけるとこういうエグいのが出てくる↑」「そっ閉じするヤツw」「rubocop -aで最近ヒアドキュメントが壊れたりする」「『inherit_from: .rubocop_todo.ymlするな』ってのは正しい: やったら絶望できる」「少しずつ項目を増やすのが吉」「『これだけは許せない』っていうymlだけ作って共有したいです」「プロジェクトや会社によっていろいろ宗教が違うんで大変だけど」

「line lengthのチェックが邪魔で」「自分はline length 200ぐらいあってもいいかな: あんまり長いのは確かに嫌だけど」
「gemのアルファベット順並びチェックされるのも嫌ですよね」「あれは空行入れればブロックが分かれるはず: 空行なしでべた書きすると言われるしそれはわかる」「Rubocopに言われるままの修正だけだと何のアウトプットにもならないのがなー」(以下延々続く)

「JetBrainsのUpsourceみたいな方向でIDEとRubocopが統合されたら幸せになれるかな: ちなみにUpsourceはとっても優秀で、Web画面からinspect definitionとかのJetBrainsのエディタ機能が使えるし、逆にRubyMineとかからレビューコメント付けたりもできる」「へー!」「Upsource、10人まで無料なのか」「GitLabのWeb IDEも似た路線でとてもいいんだけど一番高いエディションが必要」


jetbrains.comより

「お、モーフィアスが」「マトリックス知ってる人がいて何だかほっとしたー」

#yield_selfは思ったよりいいぞ(RubyFlowより)

# 同記事より
# before
url = construct_url
response = Faraday.get(url)
data = JSON.parse(response.body)
id = data.dig('object', 'id') || '<undefined>'
return "server:#{id}"
# or, short yet less readable form:
return "server:" +
  (JSON.parse(Faraday.get(construct_url)).dig('object', 'id') || '<undefined>')

# after:
construct_url
  .yield_self { |url| Faraday.get(url) }
  .yield_self { |response| JSON.parse(response) }
  .dig('object', 'id').yield_self { |id| id || '<undefined>' }
  .yield_self { |id| "server:#{id}" }

# with method()
construct_url
  .yield_self(&Faraday.method(:get))
  .body
  .yield_self(&JSON.method(:parse))
  .dig('object', 'id').yield_self { |id| id || '<undefined>' }
  .yield_self { |id| "server:#{id}" }

「メソッド名はクソだけど」だそうです。

参考: Ruby リファレンスマニュアル Object#yield_self

Rails小粒記事2本

めちゃ短い記事です。著者はいずれも同じ人です。


つっつきボイス: 「TechRachoの朝記事向けに短い記事も押さえておきたかったので」「(3本目)SELF JOINはSQLやってる人なら普通に使うやつ」

Ruby trunkより

C-APIからの不要なexportをなくしてパフォーマンス向上(反映->close)

Ruby 3でprime_divisionを強化&修正

使うにはrequire 'prime'が必要です。

# 14383より
-1.prime_division
 => [[-1,1]]

0.prime_division
Traceback (most recent call last):
        4: from /home/jzakiya/.rvm/rubies/ruby-2.5.0/bin/irb:11:in `<main>'
        3: from (irb):85
        2: from /home/jzakiya/.rvm/rubies/ruby-2.5.0/lib/ruby/2.5.0/prime.rb:30:in `prime_division'
        1: from /home/jzakiya/.rvm/rubies/ruby-2.5.0/lib/ruby/2.5.0/prime.rb:203:in `prime_division'
ZeroDivisionError (ZeroDivisionError)

つっつきボイス:prime_divisionって素因数分解か」「おーこんなメソッドあるんだ」「ゼロも整数」

参考: Rubyリファレンスマニュアル Integer#prime_division

forが要素をsplatしない

a = [Struct.new(:to_ary).new([1, 2])]
a.each {|i, j| p [i, j]} #=> [1, 2]
for i, j in a; p [i, j]; end #=> [#<struct to_ary=[1, 2]>, nil]


つっつきボイス: 「おー、forで変数が展開されないってことか」「nobuさんが投げたやつですが今のところ動きなさそう」「forだと動きにぶいでしょうね: そもそもRubocopに直されちゃうw」「直されるべき」

Ruby

Java経験者が知っておきたいRubyの特徴(RubyFlowより)

// Java
public static Object create(Class c, String value) throws Exception
{
  Constructor ctor = c.getConstructor( new Class[] { String.class } );
  return ctor.newInstance( new Object[] { "Hello" } );
}

public static void main (String args[]) throws Exception
{
  Greeting g = (Greeting) create(Greeting.class, "Hello");
  g.show();
}
# Ruby
def create(klass, value)
  klass.new(value)
end

g = create(Greeting, "Hello")
g.show

つっつきボイス: 「うん、これはなかなかいい記事」「Java出身のkazzさんがきっと好きだと思って」「ここでいうmessageってメソッド呼び出しのことなんじゃ?」「messageってSmalltalkの用語じゃん!: Javaじゃなかったのかw」「書いた人が結構いい年の予感」

参考: Wikipedia-ja Smalltalk

Ruby 25周年でメッセージ募集


つっつきボイス: 「『私とRubyの初めての出会い』みたいな感じならみんなだいたい書けるんじゃ?」「自分のときは、学部1〜2年の頃使ったCD-ROMライターPCのFedora Linuxのインストールスクリプトか何かがRubyだったなー: たぶんバージョン0.xぐらいの頃」
「そういえば未踏の一番最初がRubyだったんじゃないかな」「そうなんだ!」

Rubyのメモリ使用量を削減する

Aaron Pattersonさんの記事です。


つっつきボイス: 「ちょうど以下のGC改善gemを見つけたんですが、1年前から動きがないのと、最近のRuby JIT周りの動きが早くて時の流れを感じてしまいました」

Ruby 3 JITの最新情報: 現状と今後(翻訳)

Rubyオブジェクトのアロケーションを追ってみた(Ruby Weeklyより)

Rubyのobjspaceライブラリを使っています。

# 同記事より
require 'objspace'

ObjectSpace.trace_object_allocations do
  obj = Object.new

  puts "File: #{ObjectSpace.allocation_sourcefile(obj)}"
  puts "Line: #{ObjectSpace.allocation_sourceline(obj)}"
end

# File: trace.rb
# Line: 4

つっつきボイス: 「こう書くとアロケーションがこう変わるみたいなのがわかりますね: Stringをfreezeするとこうなるとか」「レイヤは違うけど、ちょうど今日社内勉強会でやった「データマッパー」の話にも通じるものがある: lazyに書かないとこうなるよ、みたいな」

RubyRogueポッドキャスト「Rubyデバッガー」


devchat.tv/ruby-roguesより

ゲストのDaniel Azuma氏はGoogleでRubyとElixirのチームリーダーを務めているそうです。

awesome-awesomeness: まとめ情報のまとめ


つっつきボイス: 「まとめのまとめ」「草不可避」「awesomeなんちゃらという名前のまとめ情報って、GitHubで手っ取り早く★集めたい人が作りまくってますね」「とりあえずRubyの項はなかなかよさげでした」

fir: fishシェル風の対話的Ruby REPL


同記事より

SQL

MySQLをダウンタイム最小でPostgreSQLに移行する(Postgres Weeklyより)

⭐PostgreSQLの設定ファイルをGUIで生成するサービス(Postgres Weeklyより)⭐


pgconfigurator.cybertec.atより


つっつきボイス: 「よくあるやつかなと思ったけど、How would you describe your workload?↓があるのがいい: 結局どういうデータアクセスがあるかが重要なんで」


pgconfigurator.cybertec.atより

今週の⭐を進呈いたします。おめでとうございます。

pg_trgm: ワイルドカード検索を高速化する標準ツール(Postgres Weeklyより)

pg_trgm ignores non-word characters (non-alphanumerics) when extracting trigrams from a string. Each word is considered to have two spaces prefixed and one space suffixed when determining the set of trigrams contained in the string. For example, the set of trigrams in the string “cat” is “ c”, “ ca”, “cat”, and “at ”. The set of trigrams in the string “foo|bar” is “ f”, “ fo”, “foo”, “oo ”, “ b”, “ ba”, “bar”, and “ar ”.


つっつきボイス: 「全文検索とかで使うやつですね」「trigram: 三重文字」「PostgreSQLのこういうExtensionとかプラグインってもう無数にありますね: データベースの研究者がオープンソースであるPostgreSQLにたくさん集まって実装と検証に使ったのが大きいんじゃないかな」
VLDBっていう世界最大のデータベースカンファレンスがあるんですが、プロポーザルがすっごく分厚い」「2017年で第43回!」「Very Large!」「最近追えてないけど、当時はXMLデータベースが熱かった」

Herokuが「Postgres PGX」プランをリリース(Postgres Weeklyより)


blog.heroku.comより

JavaScript

ちょっと便利なスニペット

とても短い記事です。

// 同記事より
const originalObject = { a:1, b: 2, c: 3 };
const shallowObjectClone = {...originalObject};

つっつきボイス: 「記事は短いんですが、コメントやツイートで俺も俺もと盛り上がってます」「大喜利化しとるなー」

await (await fetch('https://api.github.com/users/wesbos')).json();

信頼できないJavaScriptをSaaSとして動かすには


つっつきボイス: 「レイヤで仕切ったりDockerでサンドボックス化したり、この通りにすれば大きな被害はないだろうけど、いったいどれだけ信頼されてないんだそのコードw」「書いた人(´・ω・)カワイソス」

Reduxを使っていいときいけないとき


blog.logrocket.comより


つっつきボイス: 「BPS アプリチームもRedux使ってた気がする」

FirebaseとAngularでAuth0認証する方法

Auth0はシングルサインオンのサービスのようです。

CSS/HTML/フロントエンド

平方マイルをみたいにmileの肩に2を付けて表すには


dev.to/ice_lenorより

formatDistanceでやれるそうです。


つっつきボイス: 「一応Unicodeを探してみましたが、平方マイルの文字はないですね」

WebSubがW3C仕様でRecommendationに

WebSub provides a common mechanism for communication between publishers of any kind of Web content and their subscribers, based on HTTP web hooks. Subscription requests are relayed through hubs, which validate and verify the request. Hubs then distribute new and updated content to subscribers when it becomes available. WebSub was previously known as PubSubHubbub.
w3.orgより

前はPubSubHubbubという名前だったんですね。


つっつきボイス: 「早口言葉すぐる」「しかも最後のbubが小文字とか」「インド人の話す英語みたいな」「名前変えてよかったね~」

Chrome Devtoolsで変更をリロード後も維持できる機能が追加


developers.google.comより


つっつきボイス:babaさんが『いたずらに使えそうw』って」「CSSはともかくJSでどうやってんのかな」

CSSのコメントの書き方ベストプラクティス(Frontend Weeklyより)


webdesign.tutsplus.comより


つっつきボイス: 「うん、CSSスタイルガイドとかこういうベストプラクティスがあるのはいい: 良記事」「お、これはBootstrapのファイル↓」


webdesign.tutsplus.comより

「ところで、BootstrapのCSSを最近読む機会があったんだけど、思ったよりずっとシンプルで読みやすいんですよ: もっと死ぬほどでかいかと思ってた」「へー」「しかもブラウザハック的なコードにはちゃんと解説が書いてあるのがエライ」

CrookedStyleSheets: CSSだけでトラッキングするテクニック集

// 同リポジトリより
@supports (-webkit-appearance:none) and (not (-ms-ime-align:auto)){
    #chrome_detect::after {
        content: url("track.php?action=browser_chrome");
    }
}

つっつきボイス: 「Googleに怒られないかなとふと思って」「まあ昔の携帯Webサイトなんか、こうやるしかなかったですからね」「はーん、vender-prefixで切り替えたりとか」「JS禁止の案件なんかで使えるかもしれないけど、まあお遊びというか芸ですね」

その他

Bashシェルスクリプトをデバッグする

# 同記事より
#!/bin/bash
_DEBUG="on"
function DEBUG()
{
 [ "$_DEBUG" == "on" ] &&  $@
}

DEBUG echo 'Reading files'
for i in *
do
  grep 'something' $i > /dev/null
  [ $? -eq 0 ] && echo "Found in $i file"
done
DEBUG set -x
a=2
b=3
c=$(( $a + $b ))
DEBUG set +x
echo "$a + $b = $c"

brook: Goで書かれたマルチプラットフォームのプロキシ

CLI版とGUI版があり、設定がほとんど不要だそうです。

Kubernetesを2500ノードにスケールアップ(WebOps Weeklyより)


blog.openai.comより

minikube: ローカルで動かせるKubernetes(GitHub Trendingより)


kubernetes/minikubeより


つっつきボイス: 「これは前からあるやつ: kubernetesは複数クラスタが前提なんで、これは設定確認用ですね」

村井純先生の講演

2018年2月19日を過ぎると消されるそうですので、それまでにどうぞ。


つっつきボイス: 「休みの日に聞いてたんですがめちゃめちゃ面白かった: いろいろキワドイことやってたんだなーと」「村井先生はネットの先駆者としていろいろやってきた人なので」(以下延々省略)「あと村井先生がとても美声でした」「大学の先生は話すのも仕事のうちなので声大事ですね」

番外

movfuscator: MOV命令だけを吐き出すCコンパイラ(GitHub Trendingより)


xoreaxeaxeax/movfuscatorより


つっつきボイス: 「よーやる」「MOVはCPUにもろ依存するからプラットフォーム限定だろうけど」

この表記知らなかった


つっつきボイス: 「お、KiとかMiとか普通に使いますよ」

手作り暗号ライブラリは…

インフルエンザワクチンの効果が30%しかなくてもワクチンを接種すべき理由

参考: なぜ30%しか効果がなくてもインフルエンザワクチンは打つべきなのか?

壁に隠れている部分を撮影できる次世代カメラ


つっつきボイス: 「昔ブレードランナーで、写真に写っていない物陰にVR的に回り込んで再生するシーンにびっくりしたのを思い出しました↓」「今やスマホのカメラで指紋取れますからね」

参考: 高画質を追求してきたカメラは今後「見えない物を撮る」方向へと進化していく

ドッグイヤーどころじゃなさそう


つっつきボイス: 「今どきのAI/機械学習は質のいいデータセットもないと進めようがないっすね」

はいです


つっつきボイス: 「ズキズキッ」「はいです頑張ります」「やーホントこのとおり: 早く寝れば早く起きられますよ」


今週は以上です。

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

週刊Railsウォッチ(20180119)derailed_benchmarks gem、PostgreSQLをGraphQL API化するPostGraphile、機械学習でモック画像をHTML化ほか

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

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

Rails公式ニュース

Ruby Weekly

Awesome Ruby

Random Ruby

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Github Trending

160928_1701_Q9dJIU

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

$
0
0

概要

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

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

「マイグレーションを実行しないでSQLを取る方法はありますか?」という質問を何度か目にしたことがあります。芸のない回答としては、質問の「マイグレーションを実行しないで」を無視してマイグレーションを実行し、ログファイルをgrepしてSQL出力を取り出し、db:rollbackを実行せよというのが考えられます。しかしこれはズルですし手間もかかります。もっとマシな方法はないものでしょうか。

私の最初のアプローチは、ActiveRecordスタックの相当深いところでメソッド呼び出しをインターセプトし、欲しいマイグレーションの場合は実行せずにSQLを出力するというものでした。私はPostgreSQLを使っているので、ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#executeメソッドをインターセプトしてみたいと思います。以下はプロキシです。

module MyTweak
  def execute(sql, name=nil)
    if caller.detect {|x| x =~ /20171010151334/ } && sql !~ /SHOW TIME ZONE/
      puts sql
    else
      super
    end
  end
end

しかし実際にこれを使ってみるとお世辞にも美しいとは言えませんでした。コンソールでこのコード変更を適用し、マイグレーションを明示的に呼び出さないといけません。

class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
  prepend MyTweak
end
require "#{Rails.root}/db/migrate/20171010151334_add_wing_count_to_jets"
AddWingCountToJets.new.change

サンプルの出力結果をいくつかGistに置きました。しかし見てのとおり、この方法は相当イケてないうえに何というか苦痛です。

StackOverflowのこのスレでもっとよいアプローチをいくつか見つけました。1つ目の回答はalias_methodActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#executeを再定義していますが、私が上のMyTweakでやったことと大差はなく、特定のメソッドをショートさせてSQLを出力しています。しかし本当の改善は、fake_db_migrateというRakeタスクを定義してモンキーパッチを当ててからdb:migrateを実行するという方法でした。これならハードコードも不要ですし、どんなマイグレーションでも動きます。

そのスレの別の回答はもっとうまく動くのですが、少々不安定な感じでもありました。その方法ではSQLの取得にトランザクションとロールバックを使っていました。この方法もコンソールで特定のマイグレーションをrequireしなければなりませんが、コードにパッチを当てるのではなく、マイグレーションをトランザクションで実行して明示的にロールバック例外をraiseしています。

ActiveRecord::Base.connection.transaction do
  AddWingCountToJets.new.migrate :up
  raise ActiveRecord::Rollback
end

このアプローチも、質問の「マイグレーションを実行しないで」を無視していますが、その点はおそらく大丈夫でしょう。やりたいのは、データベースを元の状態のままSQLを取り出すことだからです。SQLを実行してからロールバックすればこの目的を達成できます。

Railsコアプロジェクトで、この機能のためのプルリク#31630がオープンされました。このプルリクのアプローチでは、「dry run」フラグを取り入れたロールバック戦略を用いています。この実装の今後の移り変わりや、ActiveRecordコアに取り入れられるかどうかについては興味を惹かれます。

マイグレーションのArel AST(抽象構文木)を取り出してto_sqlを呼べばいいのになぜそうしないのかとお思いの方もいるかもしれませんが、これはマイグレーションの実際の動作とは異なっています。マイグレーションは、ActiveRecordクエリのように途中でツリーを生成したりプロセスをフルスキャンしたりするのではなく、必要に応じてSQLからビルドされます。たとえば以下は、マイグレーションの非常に便利なメソッドであるchange_column_defaultのPostgreSQLアダプタ版から抜粋したものです。ここから文字列が結合される様子がわかります。

  alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s"
  if default.nil?
    # DEFAULT NULLの振る舞いはDROP DEFAULTと同じ結果になる。
    # ただしPostgreSQLはデフォルトをカラム型にキャストし、
    # "default NULL::character varying"のようにデフォルトにする
    execute alter_column_query % "DROP DEFAULT"
  else
    execute alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}"
  end

ArelはDDL ASTをサポートしませんが、DDL文法が存在しているので、これらがマイグレーションの中間層にまとまっていることは想像できます。しかし、現在のアプローチでこの作業を長年に渡って完了できているので、私にはこの部分で頑張るのがよいとは思えません。

結論としては、ロールバック戦略が明確さにおいてベストではないかと思います。データベースアダプタにモンキーパッチを当てたりしないからです。しかし一回こっきりの雑なハックで構わないのであれば、モンキーパッチで切り抜けるのも悪くないでしょう。これがRailsのコアに取り入れられるかどうか、今後もRailsのchangelogに注目しましょう。

訳注: #31630はマージされずにクローズしました。

関連記事

[Rails 5] マイグレーション時にデータベースのカラムにコメントを追加する

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

Viewing all 1381 articles
Browse latest View live