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

Ruby: ループには一時変数ではなくEnumerableを使おう(翻訳)

$
0
0

概要

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

Ruby: ループには一時変数ではなくEnumerableを使おう(翻訳)

Rubyで私の大好きな機能のいくつかはEnumerableモジュールにあります。このモジュールについて詳しくはRubyのドキュメントをどうぞ。

ArraySetHashなどのような「コレクション」を表現するクラスでは、Enumerableのメソッドや機能が使えます。これらのメソッドを用いることで、そのグループをループで回して、コレクションの個別のメンバーを入力とする操作を行えます。

次のようには書かないこと

C言語のように一時変数を用いてループする。多くの言語ではこれでよしとされている。

total = 0
[1, 3, 5, 7].each do |num|
  total += num
end
total

次のように書くこと

RubyのEnumerableメソッド群への愛を存分に発揮して、#injectで実装する。

[1, 3, 5, 7].inject(0) do |total, number|
  total += number
end

次のように書けばさらに良い

Enumerableにある便利な#sumメソッドを用いる。

[1, 3, 5, 7].sum

そうすべき理由

私はRubyを使えば使うほど、Enumerableのメソッド群を実にエレガントな方法で利用できることに至高の喜びを感じます。このようにコードを構成することでRubyらしい書き方(idiomatic Ruby)になります。Rubyらしく書くことで、Rubyの作法に適った健全なRubyコードを書いていると言えるようになります。

巨大なループを扱う場合、Ruby組み込みのEnumerableメソッド群を用いることで、メモリ使用量の削減や速度の向上に著しい効果がもたらされることがよくあります。

ループの冒頭でこの手の「一時変数のセットアップ」を見かけたら、そのループをEnumerableのメソッドで適切に表現する絶好のチャンスと言えるでしょう。

そうすべきではない理由があるとすれば

Ruby経験の浅い人は、#injectなどのメソッドで混乱することもあるようです。こうしたメソッドはRubyを使って得られる基本的なメリットなので、ぜひモノにしましょう。

関連記事

Ruby: 自分のクラスに`#to_a`を実装すべきじゃない(翻訳)


Railsのフロントエンドのノウハウ#1: システムテスト編(翻訳)

$
0
0
  • 次記事: Railsのフロントエンドのノウハウ#2: JavaScript編(翻訳)

概要

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

記事を2本に分割しました。日本語タイトルは内容に即したものにしています。

Railsのフロントエンドのノウハウ#1: システムテスト編(翻訳)

記事の長さ: 8分

Rails 5以降の新しい機能のひとつに「システムテスト」があります。システムテストは、Capybaraを用いたフル装備のフロントエンドテスト機能を提供し、通常のユーザーが体験する完璧なWebエクスペリエンスを提供する本物のブラウザウィンドウを用いて各テストを実行します。この機能がRailsに組み込まれたことでテストがエレガントになり、テストに全力で集中できるようになります。

さらにうれしいことに、標準のRailsジェネレータを用いてモデル名や属性フィールドを生成すると、標準のCRUDシステムテストを自動生成してくれるので、フロントエンドフレームワークの差し替え作業が大幅にシームレスになります。

今回の記事では、前回の記事で用いたRails+VueJSアプリでコード変更が生じたときにシステムテストをすべてパスさせてご覧にいれます。その他に、テストで有用なJavaScriptの知識についてもいくつかご紹介いたします。

フロントエンドアプリを更新する

ここでは、前回の「VueJS Components with CoffeeScript for Rails」記事で使ったアプリを変更します。前回までのソースコードはdanielpclark/vue_exampleでご覧いただけます。

システムテストを実行するには、プロジェクトディレクトリで以下のコマンドを実行します。

rails test:system

訳注: 初めて実行する人はyarnをインストールしたうえでyarn add rails-erb-loaderを実行しておきましょう。場合によってはyarn install --ignore-enginesの実行も必要かもしれません。

リソースの作成と更新の部分でエラーが2件表示されるはずです。「Unable to find visible field “Body” that is not disabled」というエラーメッセージが表示されます。

上述のエラーはこのフィールドでのみ表示されます。これがこのテストで最初のエラーであるためです。テストの順序を変更すると、作成や更新にあるすべてのフィールドについて同様にテストが失敗します。これはテストするフィールドをCapybaraから探索する方法に関連しているはずです。

訳注: 手元のRuby 2.5.1/Rails 5.2.0環境では、最初だけ上述のエラーが1件表示されましたが、その後エラーは表示されなくなりました。springを止めてやり直しても、やはりエラーは表示されませんでした。

Railsのフォーム系メソッドでラベルや入力フィールドを生成すると、HTMLのlabel要素にforフィールドができます。ここには、対象となるあらゆる入力のidオブジェクトの名前が含まれます。私たちが独自のVueJSコンポーネントテンプレートを用いて新しいフォームを手作りしたときに、これらのフィールドについてはテストを書いていませんでした。そのため、app/javascript/form-document.vue.erbファイル内のフォーム用フィールドを変更する必要があります。

<label>Subject</label>
<input type="text" v-model="document.subject" />

<label>State</label>
<select v-model="document.state">
  <%= options_for_select(Document.states.keys, "concept") %>
</select>

<label>Body</label>
<textarea v-model="document.body"></textarea>

上を以下のように変更します。

<label for="document_subject">Subject</label>
<input id="document_subject" type="text" v-model="document.subject" />

<label for="document_state">State</label>
<select id="document_state" v-model="document.state">
  <%= options_for_select(Document.states.keys, "concept") %>
</select>

<label for="document_body">Body</label>
<textarea id="document_body" v-model="document.body"></textarea>

CapybaraにSubjectfill_inするよう指示を出すと、Capybaraはそのラベルのコンテンツとforの値を元に対象を取得し、そのforの値で使われているidを対象とします。この方法のよい点は、それに続く入力フィールドを対象として指定しなくても、このラベルを用いて同じように対象を指定できることです。

(修正後に)rails test:systemを再実行すると、同じエラーメッセージが表示されます。これはRails 5の設定でprotect_from_forgeryがテスト環境で使われていないことに関連するはずで、そのためCSRFトークンが生成されなくなっています。私たちのVueJSコードが失敗したのは、メタ属性フィールドが利用できることを明示的に要求しているためです。

これは以下のいずれかの方法で修正できます。

  • 自分のVueJSコードを編集して、CSRFトークンが存在しなくても動作するようにする(上述のアドバイス)。
  • config/environments/test.rbファイルを以下のように変更する。
# Forgery protection
config.action_controller.allow_forgery_protection = true

フロントエンド側のフォーム送信の実装によっては、ApplicationControllerに以下のコードがないと動かない可能性もあります。私たちの場合はこれは不要になります。

protect_from_forgery prepend: true

原注: 問題発生の原因を突き止めるには、RAILS_ENV=testを指定してrails serverを実行する必要があります。これにより、Documentリソースのneweditリソースを表示したときにブラウザのコンソールでJavaScriptのエラーが発生してる様子を確認できるようになります。

rails test:systemを実行すると、今度は「Update DocumentボタンやCreate Documentボタンが見つからない」というエラーメッセージに変わります。これはRailsが送信ボタンとして生成したと命名スキーマがフォームヘルパーで使われている可能性があるため、テストを更新して現在のボタン名を反映する必要があります。

test/system/documents_test.rbを開き、click_onの対象を"Create Document""Update Document"から"Submit"に変更します。これでテストを実行すると、今度は正しいflash通知が結果の中に見当たらないというメッセージが表示されますので、このflashメッセージを追加する必要があります。以下のapp/views/layouts/application.html.erbアプリケーションテンプレート内の<body>タグの内側に以下を追加します。

<% flash.each do |name, msg| -%>
  <%= content_tag :div, msg, class: name %>
<% end -%>

続いてコントローラのupdateアクションやnewアクションを更新して、適切なflash通知が表示されるようにします。app/controllers/documents_controller.rbを以下のように変更します。

def create
  @document = Document.new(document_params)

  respond_to do |format|
    if @document.save
      flash[:notice] = 'Document was successfully created.'
      format.html { redirect_to @document, notice: 'Document was successfully created.' }
      format.json { render :show, status: :created, location: @document }
    else
      format.html { render :new }
      format.json { render json: @document.errors, status: :unprocessable_entity }
    end
  end
end

def update
  respond_to do |format|
    if @document.update(document_params)
      flash[:notice] = 'Document was successfully updated.'
      format.html { redirect_to @document, notice: 'Document was successfully updated.' }
      format.json { render :show, status: :ok, location: @document }
    else
      format.html { render :edit }
      format.json { render json: @document.errors, status: :unprocessable_entity }
    end
  end
end

これでrails test:systemを実行すると、システムテストがすべてパスするようになります。


  • 次記事: Railsのフロントエンドのノウハウ#2: JavaScript編(翻訳)

関連記事

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

Rails 5: WebSocketのマルチセッションをMiniTestとシステムテストでテストする(翻訳)

Rails: ActiveRecord::Relationで生SQLは避けよう(翻訳)

$
0
0

概要

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

訳注: 原文のActiveRelationは訳文でActiveRecord::Relationに変更してあります。

Rails: ActiveRecord::Relationで生SQLは避けよう(翻訳)

ActiveRecord::Relationは、ActiveRecordの検索やクエリエンジンを強化する、柔軟で強力なツールです。

次のようには書かないこと

ActiveRecord::Relation#whereメソッド内で生SQL文字列を式展開(interpolation)で直接書く。

Person.where("name = #{ params[:name] } AND hidden_at IS NULL")

これもやらないこと

?による安全性の高い式展開を用いてユーザー入力を「arrayスタイル」で生SQL文字列で書く。

Person.where('name = ? AND hidden_at IS NULL', params[:name])

次のように書くこと

「hashスタイル」の構文で書く。

Person.where(name: params[:name], hidden_at: nil)

そうすべき理由

最初の2つで使われている生SQL手書きは、ActiveRecord::RelationがRailsに(バージョン3.0で)マージされたときまでは、データベースクエリを指定する唯一の方法でした。しかし、上述の「hashスタイル」の方が柔軟性においても安全性においても上です。

1つ目の例は非常に危険です。ユーザーから渡されたパラメータを文字列の式展開で直接使っていますが、こういう書き方は絶対にしないでください。さもないとSQLインジェクション攻撃にさらされ、インターネットに潜む悪意のあるユーザーがあなたのデータベースに対して破壊的またはデータをさらけ出すステートメントの実行を試みることができてしまいます。

2つ目の例で用いた「arrayスタイル」では、渡されるデータがサニタイズされるため、1つ目の「stringスタイル」による#whereよりはましな方法です。しかし、SQLを生書きしないといけない点は変わりません。せっかくRubyで楽しくコードを書いているというのに、ActiveRecord::Relationが生成してくれる完璧なSQLでハッピーになれる道を選ばない理由があるでしょうか。

3つ目の例で用いた「hashスタイル」は、2つ目の「arrayスタイル」よりも短く、クリーンで、エディタのシンタックスハイライトもよりきれいに表示されます。コードは、書くときよりも読むときの方が重要なのです。

#whereに文字列を渡すと、渡した文字列がそのまま生成されたSQLで使われます。テーブル名も不要ですし、タイポとも無縁です。

# hashスタイルの場合
> Person.where(name: 'Andy', hidden_at: nil).to_sql
=> "SELECT \"people\".* FROM \"people\" WHERE \"people\".\"name\" = 'Andy' AND \"people\".\"hidden_at\" IS NULL"

# stringスタイルの場合
> Person.where('name = ? and hidden_at is null', 'Andy').to_sql
=> "SELECT \"people\".* FROM \"people\" WHERE (name = 'Andy' and hidden_at is null)"

この「hashスタイル」構文が評価されると、データベースのテーブル名がクエリに含まれ、SQLの精度も向上します。これにより、複数のモデルでJOINしたりクエリを生成したりするときのエラーを削減できます。

hashスタイルのさらに便利な点は、データベースアダプタが変更されたときにも生成されるSQLの互換性が保たれることです。

そうすべきではない理由があるとすれば

文字列による条件を使うと、使っているデータベースの特定のSQL方言(フレーバー)への依存が生じる可能性があります。ただし、これが問題になるとすればよほど難解なデータベース固有SQL(検索や地理情報クエリなど)を使う場合ぐらいです。上述の例のように素直なSELECT構文なら問題になりません。

さらに、いったんproductionにデプロイされた後でデータベースをPostgreSQLからMySQLに(あるいはその逆)に移行することは、実際にはまずありません。よほどマゾヒスティックな性格の方なら別ですが。

また、本記事でご紹介した例は非常にシンプルなものです。現実には、使っているデータベース固有の文字列引数を#whereに与える必要が生じることは多々あり、それ自体はまったく問題ありません。しかし選択の余地があるならば、柔軟かつスコープの明快な方式を選ばない理由があるでしょうか。

関連記事

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

Rails: Shopify流「非同期マイグレーション」でデプロイを安定させよう(翻訳)

$
0
0

概要

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

内容に即した日本語タイトルにしています。

Rails: Shopify流「非同期マイグレーション」でデプロイを安定させよう(翻訳)

スキーマ変更を伴うコードをデプロイするたびに、bin/rails db:migrateを実行して新しいActive Recordマイグレーションを適用しなければなりません。これは共通で使われるデプロイスクリプトです(Capistranoを参照)。

デプロイの一環としてマイグレーションを走らせることは、多くの企業で用いられているデフォルトのアプローチですが、Railsコミュニティはどうしたわけかこれ以外の方法を検討したことがありません。マイグレーションを行うと、その分リリース手順が複雑になりませんか?

  • マイグレーションが失敗した場合、デプロイを取り消すべきか?
    • 取り消ししたいのであれば、マイグレーションが失敗するまでに新しいコードが短時間production環境で実行されていた可能性があるが、ロールバックしたときにその新しいコードがさらなる問題を引き起こすかもしれない。
  • 利用しているデータベースが複数ある場合(シャーディングを使っている可能性もある)、それぞれのデータベースにマイグレーションを適用しなければならなくなる。
  • マイグレーションに時間がかかると(数時間)、その間デプロイを完了できなくなる。
    • マイグレーションを実行するアクターでssh接続が失われた場合など
  • クラウド環境(HerokuやKubernetes)の場合、マイグレーションを実行する「after deploy」的フックが使えるとは限らない。

本記事では、デプロイ手順からマイグレーションを削ぎ落とす方法について解説します。Shopifyが辿り着いた非同期マイグレーションなる手法は、「デプロイが終わってから最終的に適用する」「制御は人間が行う」がポイントです。

非同期マイグレーションのしくみ

最初に、db:migrateで実際に何が行われているかを理解しておく必要があります。

Active RecordのRakeタスクを見てみると、ActiveRecord::Base.connection.migration_context.migrateに対してcallを行っていることがわかります。ここがマイグレーション実行のエントリポイントでなければなりません。(ENV['VERSION']のような)引数を付けずに呼び出すと、MigrationContext#migrateがマイグレーション用のクラスごとにMigrationProxy作成し、続いてMigrator.new.migrate呼び出します

マイグレーションが呼び出されるしくみを理解できましたので、これを非同期的な手順として再設計し、デプロイ中にマイグレーションが走らないようにする準備が整いました。マイグレーションをバックグラウンドジョブから実行するという方法はどうでしょうか?

ペンディング中のマイグレーションがあると、そのたびにバックグラウンドジョブに送り込んで実際のマイグレーションをそこで実行し、結果を出力するというアイデアです。どんな実装が可能か見てみましょう。

最初に、(sidekiq-cronなどのツールを用いて)ペンディング中のマイグレーションがあるかどうかを数分おきに繰り返しチェックするジョブをスケジューリングする必要があります。

class MigrationAutoCannonJob < ApplicationJob
  def perform
    return unless migration_context.needs_migration?

    pending_migrations = (migration_context.migrations.collect(&:version) - migration_context.get_all_versions)
    # ここで実行する!
  end

  private

  def migration_context
    ActiveRecord::Base.connection.migration_context
  end
end

マイグレーションは複数同時に実行できない「ブロッキングプロセス」であることを忘れてはなりません。直前のマイグレーションが完了するまでは次のマイグレーションを実行してはならないのです。また、マイグレーションの実行状況を監視できるようにしておきたいので、実行状況を追いかけるためのActiveRecordモデルを1つ作成してみましょう。

$ rails generate model async_migration version:integer state:text
# app/models/async_migration.rb
class AsyncMigration < ApplicationRecord
end
# 必ずuniqueインデックスを追加すること!

それではこの自動機関砲的なジョブ(auto cannon job)を更新して実行状況をトラッキングし、一度に1つのマイグレーションだけが実行されるようにしてみましょう。

class MigrationAutoCannonJob < ApplicationJob
  def perform
    return unless migration_context.needs_migration?

    if AsyncMigration.where(state: "processing").none?
      AsyncMigration.create!(version: pending_migrations.first, state: "processing")
    end
  end

  def pending_migrations
    (migration_context.migrations.collect(&:version) - migration_context.get_all_versions)
  end

  # ジョブが続く

このジョブによってasync_migrationsテーブル内にエントリが1つ作成されますが、これは他のエントリが”processing”でない場合に限って行われます。これによって複数のマイグレーションが同時に走ることを防止します。ジョブそのものが競合に対して保護されていない点には注意を払う必要がありますが、スケジューリングされたインスタンスは1つしかないので、その点は大丈夫です。

それでは、マイグレーションを実際に行うモデル用のコールバックを1つ作成してみましょう。

class AsyncMigration < ApplicationRecord
  after_commit :enqueue_processing_job, on: :create

  private

  def enqueue_processing_job
    MigrationProcessingJob.perform_later(async_migration_id: id)
  end
end

AsyncMigrationが作成されるたびに、実際のマイグレーションを実行するMigrationProcessingJobがキューに送り込まれます。このジョブがどのようなものか見てみましょう。

class MigrationProcessingJob < ApplicationJob
  def perform(params)
    async_migration = AsyncMigration.find(params.fetch(:async_migration_id))

    all_migrations = migration_context.migrations
    migration = all_migrations.find { |m| m.version == async_migration.version }

    # 実際のマイグレーション
    ActiveRecord::Migrator.new(:up, [migration]).migrate

    async_migration.update!(state: "finished")
  end

  def migration_context
    ActiveRecord::Base.connection.migration_context
  end
end

まだまだ足りないものがありますが、アイデアについてはこれで把握できることでしょう。2つのジョブと1件のデータベースレコードを組み合わせることで、バックグラウンドで1件ずつ実行されるマイグレーションをスケジューリングできます。

本記事のコード例はいずれも「作り中そのもの」である点にご注意ください。もっときちんとやりたいのであれば、以下の点にも配慮が必要になるでしょう。

  • エラーハンドリングがまったく行われていない。マイグレーションがエラー終了した場合にAsyncMigrationのステータスを更新したいところ。
  • 最大リトライ数がジョブで定義されていない。マイグレーションのリトライまでやりたいかどうか検討したいところ。
  • マイグレーションの所要時間の測定と永続化もやっておきたいところ。

可能性はいくらでも考えられます。管理UIを構築してマイグレーションを監視できるようにすることもできますし、マイグレーションの完了または失敗をSlackチャンネルに通知することもできます。

Shopifyでは数百にのぼるデータベースシャーディングを扱っており、スキーマ変更のたびにシャーディングひとつひとつでマイグレーションを実行しなければなりません。マイグレーション手順がデプロイスクリプトの一部に組み込まれていると、リリースプロセスがぐらついてしまうでしょう。Shopifyでは代わりに、リリースの完了後に最終的に提供される「非同期マイグレーション」を用いています。非同期マイグレーションは、Shopifyが誇る1日50回を超えるリリースを支える重要な機能のひとつです。

マイグレーションのステータスを次のようにSlackチャンネルに流すことも可能です。

こうした機能が心にズドッと突き刺さりましたら、私のチームで一緒に働きましょう!ぜひShopifyの採用ページをご覧ください。

関連資料

最新記事を追いかけたい方はTwitterで@kirshatrovのフォローをお願いします。

関連記事

Rails: データベーススキーマをダウンタイムなしで変更する(翻訳)

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

Rails: PostgreSQLのマイグレーション速度を改善する(翻訳)

Railsのフロントエンドのノウハウ#2: JavaScript編(翻訳)

$
0
0

概要

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

記事を2本に分割しました。日本語タイトルは内容に即したものにしています。

Railsのフロントエンドのノウハウ#2: JavaScript編(翻訳)

RailsにおけるJavaScriptについての洞察

Rails開発者の多くは、JavaScriptがうまく動作しない場合の振る舞いについてなかなか把握できないことがあります。しかもRailsの場合、JavaScriptのいくつかの振る舞いについても特定の方法で変更されます。

TurbolinksやSpringを嫌う人が多いという話を聞いたことがあるかと思います。私はたまたまどちらの技術も好きですが、これらの動作を理解しておかないと、百戦錬磨のJavaScript開発者ですら頭を抱えるような事態になるかもしれません。

これら2つのライブラリについて簡単にご説明いたします。

Turbolinksは、ページの読み込みを高速化したり、フォームや一部のCRUD操作で使えるUJS機能を素早く読み込めるようにしたりします。Turbolinksを導入するとJavaScriptの「page ready hook」が変更されます。このフックは"turbolinks:load"を行うのに使われます。Turbolinksを使っていると、標準のJavaScript「page ready hook」でいくつかの問題が生じます。

Springはアプリのコードを対象とするプリローダであり、Rubyコードとフロントエンドのアセットの両方について読み込んだものを自身のプロセスにキャッシュし、次回のリクエストで使えるようにします。JavaScriptのエラーハンドリングについての知識がないと、解決に多大な日数を要するバグを追いかけるはめになるかもしれません。Springは、最初のページ読み込みから最初に操作を行った「後で」これらを機能させるためです。したがって、サイトが読み込まれるときに一部が動かないことがあっても、リンクをクリックすると動き出します。

Springの役割を詳しく調べる前に、JavaScriptがひどいコードをどのように扱うかを最初に見てみることにしましょう。

Javascriptが何らかのひどいコードに遭遇したり、評価すべきでないものを評価したりすると、JavaScriptは現在の作業を停止して、何か問題が生じていることをブラウザのコンソールに出力します。すなわち、アプリにJavaScriptの何か新しいライブラリを導入し、そこにひどいコードがあると、それ以降のコードはライブラリであろうと自分のコードであろうと動かなくなります。これはJavaScriptの通常の振る舞いであり、ひどいコードを実行する時点でコードの事項を停止するためのものです。

しかしSpringを追加した場合、アセットがキャッシュに(Turbolinksもろとも)読み込まれてひどいJavaScriptコードが評価された後、SpringはJavaScriptコードをの実行を回避しません。つまり最初のページを読み込むと、SpringやTurbolinksがアセットやライブラリをキャッシュしてより高速読み込みに備えるのです。

最初のリンクを操作してたどるまではまったく問題ないように見えることがあります。というのも、SpringはひどいJavaScriptコードの後でよいJavaScriptコードを生成するためです。最初のページ読み込みでJavaScriptの機能が不要であれば、何か問題が起きているかどうかをコンソールで確認しない限り、何も問題が起きていないように見えます。そして(productionで)コードを公開すると、「ローカルではちゃんと動く」にもかかわらず、サイトの一部がまったく突然にダウンして期待どおりに動かなくなります。

この手の「ローカルと同じ結果をproduction環境で得られない」問題のせいで、多くの人が髪の毛をかきむしりまくっています。

ここでJavaScript開発者にひとつ耳寄りな小技をご紹介します。アプリに新しいライブラリなり新しいJavaScriptコードなりを追加する場合は、常にその末尾にconsole.log("Seems okay ¯\_(ツ)_/¯");を追加しておくというものです。こうしておけばブラウザコンソールでコードがちゃんと動いていることを確認できるようになります。コンソールログにこれが出力されていれば、自分のJavaScriptコードが読み込み中に「ひどいコード」として評価されていないことがわかります。

私はBootstrap 4ライブラリをいろんなバージョンで試しましたが、その中に「ひどい」と評価されるJavaScriptコードも含まれていたことがありました。しかも私はそれを、アプリで必要とされる他のJavaScriptライブラリより前に配置していたのです。おかげで最初のページ読み込みでJavaScript関連のコードが軒並み動かず、ページをリフレッシュしたり何らかのリンク操作を行ったりした後でどうやら動くという事態になりました。私がBootstrap 4ライブラリをTurbolinksより前に置いていたために、コードの振る舞いがそのようになってしまったのです。Springがあっても最初のページ読み込みで問題なく動くように、CoffeeScriptでささやかなハックを仕込みました。

if !Turbolinks?
  location.reload

Turbolinks.dispatch("turbolinks:load")

これはローカル環境では問題なく動作したのですが、productionでは動きませんでした。Rails開発者は、"turbolinks:load"トリガがWebサイトの最初のページ読み込みで当初呼び出されないという問題をちょくちょく踏んでいます。

Turbolinks.dispatch("turbolinks:load")は、JavaScriptのその部分のコードが評価された後できるだけ早い段階でこのWebフックを即座にトリガするための方法です。これは、最初のページ読み込みの時点でJavaScriptやCoffeeScriptのスクリプトをトリガしたい方にとって役に立ちます。

ページ読み込み時にCoffeeScriptをトリガする場合、ちょっと一手間かける必要があります。というのも、CoffeeScriptコードはすべて1つの関数内にラップされていて、スコープが保護されているためです。Turbolinks.dispatch("turoblinks:load")メソッドを使うことで、このあたりのややこしさが少しマシになります。

Springを使っている場合に有用なハックはif !Turbolinks?; location.reloadしかありません。これが必要になるのは、JavaScriptの何らかの「ひどいコード」が評価されるdevelopment環境でJavaScriptコードを読み込む場合だけです。これを使うのではなく、先ほどのconsole.logテクニックを用いてJavaScriptコードの実行に成功したかどうかを確認しましょう。その後で、「ひどいコード」として評価される状況を改善しましょう。

最後に

Capybaraは、フロントエンドのエクスペリエンス全体のテストをシンプルに書ける素晴らしいライブラリです。RailsはCapybaraテストの大半を代わりに生成してくれるので、それを読んで学べば同じようなテストを今後も書き続けられます。面倒な設定はRailsが肩代わりしてくれるので、フロントエンドのテストを始めるのがとても簡単になります。

Rails 5のもうひとつうれしい点は、システムテストで例外が発生すると、ブラウザウィンドウの内容のスクリーンショットをtmp/screenshots/に自動保存してくれるようになったことです。画面とスクリーンショットを比較してテストの問題を特定しやすくなりました。

Railsのライブラリと併用した場合のJavaScriptの振る舞いを理解しておけば、地雷を避けてデバッグ時間を大幅に節約できるようになります。JavaScriptの振る舞いを正しく理解すれば、これまでうんざりさせられていた強力なツールたちをさらに使いこなせるようになります。TurbolinksSpringは、効用をと利用法を理解しておけば素晴らしいツールになるのです。

おかげで近頃はRails開発者でよかったと思えます。フロントエンドテストで大いに楽しみましょう!

Twitter: x“Rails Frontend Testing with JavaScript Insights” via @6ftdan

関連記事

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

Rails 5: WebSocketのマルチセッションをMiniTestとシステムテストでテストする(翻訳)

PostgreSQL 10: テーブル継承と宣言的パーティショニングでスケーリングする(翻訳)

$
0
0

概要

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


timescale.comより

PostgreSQL 10: テーブル継承と宣言的パーティショニングでスケーリングする(翻訳)

PostgreSQLのパーティショニングの図

PostgreSQL 10がついにお披露目されました(原注: 本記事執筆後間もなく10.1がリリースされました)。PostgreSQL 10のリリースは、論理レプリケーション、JSONやJSONBでの全文検索サポート、クロスカラム統計、並列クエリのサポート強化といった根本的な新機能を含んでおり、いろいろな意味でエキサイティングです。全般に、PostgreSQL 10は賞賛に値するオープンソースデータベースとして着実な進歩を重ねており、その大きなマイルストーンと言えます。

PostgreSQL 10の素晴らしい新機能のひとつに、宣言的パーティショニング(declarative partitioning)があり、これによってPostgreSQLをビッグデータ級のボリュームにスケールさせることが本質的に簡単になります。

PostgreSQLをスケーラブルにすることは、将来の成功のために重要です。PostgreSQLでのスケーリングの難しさに、多くの開発者たちが長年不満の声を上げていました。その結果、スケーラビリティを約束する「NoSQL」ソリューション人気を博してそちらに流れる傾向が目立ち、みな熱心にNoSQLソリューションを採用したのです。しかしそうしたNoSQLのオプションは多くの場合、信頼性、パフォーマンス、使いやすさを犠牲にしていました(近年SQLに回帰する開発者が増えた理由のひとつがこれです)。

宣言的パーティショニングによって、PostgreSQL 10の基礎部分はこうした他の技術と渡り合えるようになりました。宣言的パーティショニングは、PostgreSQLが提供すべき堅牢さ、信頼性、強力な複合クエリ、おびただしいツールやコネクタのエコシステムを、さらに巨大なビッグデータアプリで最終的に利用できるようにするための第一歩であると考えられます。私はPostgreSQL上で動作する新しい時系列データベースの開発者として、PostgreSQLが追い求める方向性に深い感動を覚えます。

ここではっきりさせておきますが、一般的なパーティショニングは何もPostgreSQL 10が最初ではありません。多くのユーザーはテーブル継承を長年PostgreSQLテーブルのスケーリングに用いています。宣言的パーティショニングとは、そこに追加された興味深いオプションにすぎません。

本記事では、PostgreSQLのパーティショニングで用いられるこれら2つのオプションを吟味することにします。

本記事では以下に重点を置きます。

  • PostgreSQLが自然にスケーリングしない理由(データのパーティショニングから始める必要がある理由)
  • テーブル継承によるパーティショニングのしくみ(PostgreSQL 10の主要なオプション)
  • PostgreSQL 10の宣言的パーティショニングのしくみ
  • 新しい宣言的パーティショニングの制限事項

私たちは、さまざまな分野の専門知識を兼ね備えた開発者がPostgreSQLに集結するという経験を得られたので、本記事では基本的な事項についていくつか解説したうえで、そうした詳細部分についても追うことにします。もちろん、最初のセクションから先の部分については皆さまの専門性のレベルに応じて適宜読み飛ばしていただいて結構です。

メモ

宣言的パーティショニングは、PostgreSQLパーティショニングの新しい進路の第一歩に過ぎません。これを基礎として、この先さらに改良が重ねられる予定です。しかし本記事では、そうした輝かしい将来計画の方ではなく、あくまでPostgreSQL 10の現状についてのみ考察いたします。

PostgreSQLだけでは自然なスケールがうまくいかない理由

これは、RAMとディスクの間のパフォーマンスの違いに由来します。

多くのリレーショナルデータベースでは、データのサイズ固定ページの(順序のない)コレクションに1つのテーブルを保存し(例: PostgreSQLのページサイズは8 KB)、システムはそれらの上にデータをインデックス化するためのデータ構造(ヒープなど)を構築します。インデックスを使うことで、指定されたID(例: 銀行の口座番号)を持つ行をクエリで素早く検索できます。これにより、そのためにテーブル全体を何らかのソート順に基いて片っ端からスキャンする必要がなくなります。

しかし、インデックスに使うすべての(ほぼ固定サイズの)ページがメモリ上に収まりきれなくなるほどの大規模データになると、ツリーのさまざまな部分をランダムに更新するときに(ページをディスクからメモリに読み込み、メモリ上で変更し、他のページの場所を空けるためにディスクに書き戻す)ディスクI/Oが著しく増加します。さらに、PostgreSQLのようなリレーショナルデータベースは、インデックスの値を効率よく検索するために、各デーブルで用いるインデックス(やB-treeなど)を保持し続けます。すなわち、インデックスを付けるカラム数が増えるに連れて、問題が複合的に発生します。

PostgreSQLにおけるメモリのスワップイン/スワップアウトのコストについては、以下のパフォーマンスグラフでご覧いただけます。テーブルサイズの増加に伴ってINSERTのスループットが分散しつつ急落しています。これは、リクエストがメモリでヒットするか、ディスクからのフェッチ(複数の可能性あり)を要するかによって変わります。

PostgreSQL 9.6.2(Azure標準DS4 v2(8コア)マシン+DDSベースのプレミアムLRSストレージ環境)におけるテーブルサイズの関数としてのINSERTのスループットです。クライアントは個別の行をデータベースにINSERTします(それぞれにタイムスタンプ、ランダムに選択されるインデックス付きID、追加の数値測定10件: 計12個のカラムを含む)。PostgreSQLの測定結果は当初15000件(INSERT/秒)ですが、5千万行を超えると著しく低下し、分散の度合いも極めて大きくなります(中にはINSERT/秒が100件台のものまであります)。

(あるベンチマークでは素のPostgreSQLでのINSERTのパフォーマンスは10億行にも達します。1万行のバッチINSERTの結果についてはこちらでご覧いただけます)

パーティショニングというソリューション

テーブルのパーティショニングは、ディスクへのスワップの高コスト問題を回避するうえで有用です。巨大なテーブルをパーティションに分割することで得られるサブテーブルのインデックスはINSERT中もメモリに保持されます(正しくインデックス化されていればの話です)。

これにより、以下を含む多くのメリットを得られます。

  1. すべてのインデックスがメモリに乗るようになり、INSERTやクエリが改善される
  2. 制約による除外が利用できるようになり、クエリが改善される
  3. より小さなデータセットを(パーティションごとに)配置することで、シーケンシャルスキャンを効率よくデータベースで利用できることがある
  4. インデックスのサイズが小さくなるため、VACUUMREINDEXなどのメンテナンス操作が高速化される
  5. 全パーティションの削除が著しく高速化される(DROP TABLEだけで行え、高コストのVACUUM操作を回避できるため)

巨大なテーブルの代わりにパーティショニングを検討すべき正確なポイントは、負荷やマシンリソースによって異なります。たとえば上述のグラフでは、表記の負荷およびマシンスペックにおけるPostgreSQLのパフォーマンスが5千万行を超えたところで低下しています。メモリを追加すれば、この問題の発生ポイントを押し上げられます。一般に、PostgreSQLテーブルに巨大なデータを書き込むのであれば、いずれどこかでパーティショニングは必要になります。

テーブル継承によるパーティショニング

PostgreSQL 10登場前のテーブルパーティショニングは、テーブル継承という形でのみ実行可能でした。テーブル継承はPostgreSQL 10以後も、宣言的パーティショニングでは力不足な状況において有用なパーティショニングオプションのひとつです。

テーブル継承を用いてパーティショニングするには、まず親テーブルを1つ作成してから子テーブルをパーティションごとに作成します。これはCREATE TABLEステートメントでINHERITS句を用いて行います(継承リンクが作成されます)。親テーブルにクエリをかけると、親テーブルと子テーブル群の両方からデータが返されます。

具体的には、テーブル継承を実装するために以下の操作が必要です。

  1. 親テーブルを作成する(すべてのパーティションはこれを継承する)。
  2. 子テーブルを複数作成する(各子テーブルはデータのパーティション1つを表し、親テーブルを継承する)。
  3. パーティションテーブルに制約を追加することで、パーティションごとの行の値を定義する。
  4. 親テーブルや子テーブルのインデックスを個別に作成する(インデックスは親テーブルから子テーブルに展開されない)。
  5. マスターテーブルに最適なトリガ関数を1つ作成して、親テーブルへのINSERTを適切なパーティションテーブルに振り分ける。
  6. このトリガ関数を呼び出すトリガを1つ作成する。
  7. 子テーブルのセットが変更される場合は、必ずトリガ関数を再定義すること。

テーブル継承は長年の間、開発者がPostgreSQLの扱いにくいテーブルをパーティショニングするための有用な手段でありつづけました。テーブル継承が提供する柔軟性も便利です。たとえば、親テーブルに現在ないカラムを子テーブルが余分に持つことを許可して、ユーザーの指定に沿った形でデータを分割できます。

しかし、上述の手順からわかるように、テーブル継承では手動の操作の手間がかなり増えてしまいます。一部の手順についてはスクリプトで自動化できますが、それでもエンジニアがつきっきりで作業しなければならず、オーバーヘッドが増大します。

テーブル継承にはいくつかの制限事項もあります

  1. データの一貫性は、子テーブルごとに指定するCHECK制約に依存します。
  2. INSERTコマンドやCOPYコマンドを行っても、データは継承階層内の他の小テーブルに自動では反映されませんが、かといってトリガに頼ると今度はINSERTが遅くなります。
  3. 子テーブルの作成と維持には、本質的に手動の作業が欠かせません。
  4. 3に関連しますが、パーティショニングの再編成作業はかなりの手間です(ディスクの追加やスキーマ変更など)。
  5. パーティション化されたテーブル全体に渡る一意性の確保はサポートされていません。
  6. 子テーブルには、インデックスや制約といったさまざまなメンテナンス用コマンドを明示的に適用する必要があり、管理タスクが著しく複雑になります。

宣言的パーティショニングは、こうした制限事項や手動操作を克服するためにPostgreSQL 10に導入されました。

PostgreSQL 10の宣言的パーティショニングは、上述の問題1と2を解決します。3の問題についても単純化されますが、それでも結構な量の手動操作や制限事項がつきまといます。

PostgreSQL 10の宣言的パーティショニング

宣言的なテーブルパーティショニングは、PostgreSQLのデータをパーティショニングときに必要な作業量を削減します。これは、パーティション化テーブルや子テーブルパーティションの作成構文を改良することで達成されました。

現時点の宣言的パーティショニングではRANGEパーティションとLISTパーティションがサポートされています。

  • RANGE: テーブルを、キーカラムやカラムセットで定義される「範囲」にパーティショニングします。異なるパーティションに振り分けられた値(device_idなど)は重複しません。
  • LIST: テーブルを、各パーティションにキーのどの値があるかというリストを明示することでパーティショニングします。適用対象は単一のカラム(device_typeなど)のみとなります。

宣言的パーティショニングのしくみ

最初にパーティションテーブルを1つ作成します。

  • PARTITION BY句を用いてパーティション化テーブルを1つ作成します。これに含まれるパーティショニング手法(この例ではRANGE)やカラムのリストはパーティションのキーとして使われます(例はPostgreSQL 10ドキュメントをそのまま引用しました)。
CREATE TABLE measurement (
 city_id int not null,
 logdate date not null,
 peaktemp int,
 unitsales int
) PARTITION BY RANGE (logdate);
  • パーティション化テーブルを作成すると、次のようにパーティションを手動で作成できます。
CREATE TABLE measurement_y2006m02 PARTITION OF measurement
 FOR VALUES FROM ('2006--02--01') TO ('2006--03--01')

CREATE TABLE measurement_y2006m03 PARTITION OF measurement
 FOR VALUES FROM ('2006--03--01') TO ('2006--04--01')

CREATE TABLE measurement_y2006m04 PARTITION OF measurement
 FOR VALUES FROM ('2006--04--01') TO ('2006--05--01')
...
CREATE TABLE measurement_y2007m11 PARTITION OF measurement
 FOR VALUES FROM ('2007--11--01') TO ('2007--12--01')
  • 続いてインデックスを作成できます。
CREATE INDEX ON measurement_y2006m02 (logdate);
CREATE INDEX ON measurement_y2006m03 (logdate);
CREATE INDEX ON measurement_y2006m04 (logdate);
...
CREATE INDEX ON measurement_y2007m11 (logdate);

パーティション化テーブルの動作は、テーブル継承の親(マスター)テーブルと似ていますが、大きく改良されていて、背後にある子テーブルのパーティションに対してはるかに強力なクエリインターフェイスを提供します。たとえば、TRUNCATECOPYといったコマンドをパーティション化テーブル経由で子テーブルに対して実行できます。さらに、パーティション化テーブルを経由してデータを背後の子テーブルにINSERTできます。これが可能なのはINSERT時にタプル(行など)が正しいパーティションにルーティングされるためで、テーブル継承のときのようにトリガに頼る必要がありません。

以上のように、テーブル継承の8つの手順は宣言的パーティショニングで劇的にシンプルになります。3つの手順で手動操作が削減され、ユーザーにとってわかりやすいものになりました。INSERTの自動化や、パーティション化テーブル経由でのテーブルコマンドの追加反映も大きな成果です。

宣言的パーティショニングは、PostgreSQLの新しいパーティショニングの第一歩に過ぎません。私たちが理解した限りにおいてですが、テーブル継承と対照的に、パーティショニングへの追加制約(親テーブル自身はデータを保持してはならないなど)が宣言的パーティショニングに導入されたことで、さらに強力なクエリ最適化などの機能を今後PostgreSQLで構築する準備が整いました。言い換えれば、PostgreSQL 10の宣言的パーティショニングのほとんどの改良点はパーティション定義のインターフェイスや構文に集中していますが、今後はこの構造を用いてさらに高度な機能や最適化を実装できるようになるはずです。

宣言的パーティショニングの制限事項

宣言的パーティショニングは、PostgreSQLのパーティショニングの自動化を進めるうえで重要な一歩を踏み出していることは間違いありません。しかしながら、まだ制限事項がいくつもあります。

手動によるパーティション管理

パーティション管理作業は宣言的パーティショニングによって楽になりましたが、それでも必要な手動操作がかなり残っています。

たとえば、データの子テーブルは、データがINSERTされる前に存在している必要があります。データが成長・進化していると、これが管理上の困難になるかもしれません。たとえば時期を基準にパーティショニングする場合、新しいデータがやってきたときにシステムが自動で新しいパーティションを新規作成してくれると便利です。現時点ではデータベース管理者が手動で操作しなければなりません。さらに、使いもしないパーティションを事前に多数作成するとパフォーマンスに悪影響が生じる可能性もあります。

手動操作が必須となる制限事項はドキュメントの「5.10.2.3 制限事項」にも掲載されており、以下のような記述があります(訳注: 訳文はリンク先から引用し、原文に合わせて調整しました)。

  • すべてのパーティションに適合するインデックスを自動的に作成する機能はありません。 インデックスは個々のパーティションについて別々のコマンドで追加しなければなりません。
  • 行をあるパーティションから別のパーティションに移動させることになるUPDATEは失敗します。
  • 行トリガーが必要であれば、個々のパーティションに定義されなければなりません。

こうした制限事項から、テーブル作成時には現在もテーブル空間(tablespace)の割り当てやインデックス・制約の作成は適宜手動で行う必要があることが見て取れます。同様に、親テーブルを変更する場合は、個別のパーティションにも手動でインデックスや制約の変更をかける必要があります。さらに、CLUSTERREINDEXといった多くのコマンドを個別の子テーブルに適用する必要もあります。

まとめると、PostgreSQL 10における個別のパーティション管理は、パーティションテーブルのインターフェイス経由ですべて賄えるわけではありません。追加操作が必要とされているため、特にパーティションを後から繰り返し追加したときやデータモデルに変更が生じたときに、データ管理上のボトルネックが形成されてしまう可能性があります。

多次元パーティショニングはつらいよ

スケーラビリティやクエリのさらなる改良のために重要となる「多次元パーティショニング」では、既存パーティション上に最初のパーティションが作成された後に手動でサブパーティションを作成しなければなりません。例として、LIST(デバイスなど)というディメンジョンがひとつ、RANGE(期間など)というディメンジョンがひとつあるとすると、これらをパーティショニングするには、最初にすべてのLISTパーティションを作成してから、それぞれの子テーブルでそれに対応するRANGEサブパーティションを作成する必要があります(逆も同様)。これによってテーブルのツリー階層が形成され、端点(leaf)のサブパーティションが効率よくタプルを保持します。

-- 「デバイス」パーティションの作成
CREATE TABLE conditions_p1 PARTITION OF conditions
      FOR VALUES FROM (MINVALUE) TO ('g')
      PARTITION BY RANGE (time);
CREATE TABLE conditions_p2 PARTITION OF conditions
      FOR VALUES FROM ('g') TO ('n')
      PARTITION BY RANGE (time);
CREATE TABLE conditions_p3 PARTITION OF conditions
      FOR VALUES FROM ('n') TO ('t')
      PARTITION BY RANGE (time);
CREATE TABLE conditions_p4 PARTITION OF conditions
      FOR VALUES FROM ('t') TO (MAXVALUE)
      PARTITION BY RANGE (time);

-- 「期間」パーティションの作成(デバイスパーティションごとに1週間を割り当てる)
Create time partitions for the first week in each device partition
CREATE TABLE conditions_p1_y2017m10w01 PARTITION OF conditions_p1
      FOR VALUES FROM ('2017-10-01') TO ('2017-10-07');
CREATE TABLE conditions_p2_y2017m10w01 PARTITION OF conditions_p2
      FOR VALUES FROM ('2017-10-01') TO ('2017-10-07');
CREATE TABLE conditions_p3_y2017m10w01 PARTITION OF conditions_p3
      FOR VALUES FROM ('2017-10-01') TO ('2017-10-07');
CREATE TABLE conditions_p4_y2017m10w01 PARTITION OF conditions_p4
      FOR VALUES FROM ('2017-10-01') TO ('2017-10-07');

-- 各端点のパーティションに期間〜デバイスのインデックスを作成
CREATE INDEX ON conditions_p1_y2017m10w01 (time);
CREATE INDEX ON conditions_p2_y2017m10w01 (time);
CREATE INDEX ON conditions_p3_y2017m10w01 (time);
CREATE INDEX ON conditions_p4_y2017m10w01 (time);

INSERT INTO conditions VALUES ('2017-10-03 10:23:54+01', 73.4, 40.7, 'sensor3');

複数に渡る手順による処理と、追加のディメンジョンやパーティションのサブレベルがあることでさらに複雑化が進むため、使いもしない巨大な継承ツリーを作ると、INSERTやクエリの両方で処理する必要のあるテーブル数が増加し、パフォーマンスやスケーラビリティに悪影響が生じる可能性があります。

データ統合でサポートされていない機能

ドキュメントの「5.10.2.3 制限事項」に記載されているように、PostgreSQLで使われているデータ統合(data integrity)機能の中には、PostgreSQL 10でサポートされていないものがあります。

  • パーティション上に主キーを作成できません。つまり、パーティション化テーブルを参照する外部キーも、パーティション化テーブルから別のテーブルを参照する外部キーも、サポート対象外です。
  • 1つのパーティション化テーブル全体に渡る一意制約(排他制約)はサポートされません。一意制約や排他制約は、個別のパーティション上にのみ作成できます。このため、ON CONFLICT句もサポート対象外となります(ただしPostgreSQL 11の機能候補にはあるようです)。

IoTアプリや時系列アプリの多くがON CONFLICT句によるupsert(訳注: update+insert)を利用しているのを目にしますので、そうした分野のアプリではかなりハードルが高くなるかもしれません。主キーが使えない点も、主キーの存在を前提とするORMユーザーにとって問題になるかもしれません。

最後に

宣言的パーティショニングは、PostgreSQLの巨大データセットでのスケーリングを推進するうえで重要な一歩です。PostgreSQL 10では、よりシンプルなパーティション定義構文が導入され、タプルもルーティングできるようになりました。どちらもパーティショニング設定を簡単にしてくれるものであり、さまざまな場面で恩恵を得られます。しかし私たちからすれば、現在進行中の作業の中で最もエキサイティングなのは、まだ登場していない「クエリ最適化」です(PostgreSQL 11以降の機能候補に挙がっています)。管理方法や使い勝手の改善の余地も、まだまだたっぷり残されています。

しかし、PostgreSQLのコア開発とは別立てで、(PostgreSQLの拡張性の高さのおかげで)TimescaleDBなどの拡張を用いてパーティションの管理や使い勝手を大きく改良することもできます。

PostgreSQLはこれらの拡張によって、指数関数的に増加する現実世界の時系列データのような、従来のOLTP(オンライントランザクション処理)の範疇を超える多様な負荷を支えることができます。

時系列データをPostgreSQL 10の宣言的パーティショニングで動かすしくみ(および私たちのパーティショニング手法が他とどう異なっているか)について知りたい方は、ぜひ最近の私たちの記事『Problems with PostgreSQL 10 for time-series data』をご覧ください。


本記事を気に入っていただけましたら、ぜひ元記事へコメントお願いします。

TimescaleDBについて詳しくお知りになりたい方は、GitHubのtimescale/timescaledbをご覧いただき(★追加はいつでも大歓迎です😃)、メールにてお問い合わせください。

関連記事

Rails+PostgreSQLのパーティショニングを制覇する(翻訳)

PostgreSQL 10の使って嬉しい5つの機能(翻訳)

Rails: データベーススキーマをダウンタイムなしで変更する(翻訳)

Rails: モデルの外では名前付きスコープだけを使おう(翻訳)

$
0
0

概要

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

Rails: モデルの外では名前付きスコープだけを使おう(翻訳)

前回の記事では#whereメソッドで「hashスタイル」を使おうというお話をいたしましたが、あまり自分が現実に使うことのなさそうなコード例でした💦

次のようには書かないこと

コントローラやビューで#whereスコープを使う。

class PostsController < ApplicationController
  def index
    @posts = Post.where(status: 'published')
  end
end

次のように書くこと

名前付きスコープはモデルでのみ定義する。

class Post < ApplicationRecord
  scope :published, -> { where(status: 'published') }
end

コントローラなどでは次のように書く。

class PostsController < ApplicationController
  def index
    @posts = Post.published
  end
end

そうすべき理由

この手法を用いることで、コード編成が改善されます。次の2つの点において、長期的な生産性向上のために実に有用です。

  • その1: 自分が作り出す概念が名前として具現化されます。それ自体が明快に語ってくれる適切な名前を付けることで、あなたの同僚はもちろん、「未来の自分」にとってもしばしばありがたいものになります。
  • その2: 探す場所が1箇所に絞られます。スコープをモデルの外で定義していると、思いつくままのスコープやら条件やらの定義がコードベースのあちこちにばらまかれることになります。すべての条件が定義される場所がわかっていれば、リファクタリングやデータベースパフォーマンスの最適化でどこをチェックすればよいかで迷うことがなくなります。

そうすべきでない理由があるとすれば

#limitや単純な#orderやページネーションに関連するスコープぐらいであれば、その場限りの特定のスコープを作ってもほとんど困ることはありません。ActiveRecord::Relationのメソッド群の構文はかなり明快だからです。

スコープに名前付けするメリットは、明快さをさらに高める場合にのみ有用です。#whereを使わないクエリの場合、シンプルなActiveRecord::Relationのメソッド群をスコープ内にラップしてもそれ以上理解しやすくなるとは限りません。

scope :by_title, -> { sort(:title) }                    # メリットないかも
scope :by_updated_at, -> { sort(:updated_at) }          # 名前ひどすぎ
scope :recently_updated, -> { sort(updated_at: :desc) } # たぶんこれはやる価値あり

順序が複雑な場合や、ソート順が#whereクエリで指定される条件と強く関連している場合であれば、名前付きスコープは依然としてよい選択肢である点は覚えておくとよいでしょう

#order#limitを使う名前付きスコープを作成するかどうかの判断に使える、うまいヒューリスティック(発見的方法)があります。「その概念には簡単に名前を付けられるかどうか」「既存のActiveRecord::Relationメソッド群よりもよい結果が得られるかどうか」です。

関連記事

Ruby/Railsのプロ開発者としての5年間を振り返る(翻訳)

Rails: ActiveRecord::Relationで生SQLは避けよう(翻訳)

Rails: 提案「コントローラから`@`ハックを消し去ろう」(翻訳)

$
0
0

概要

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

週刊Railsウォッチで絶賛された記事です。太字は訳で追加いたしました。

Rails: コントローラから@ハックを消し去る(翻訳)

少し前、とある記事を書いたとき、Railsコントローラをメンテしやすくするために私が用いていた、伝統的でない戦略をいくつかご紹介しました。今でも考え方そのものにはまったく問題はないと思っていますが、その中でもとりわけ気に入っているものについては、Railsで標準になるべきだとも思っています。

そのために本記事は、Railsのコントローラでデータの読み込みやアクション間での共有、ビューとのやりとりの手法を変更すべきであるという提案を皆さんに納得していただくための事例を作成するために執筆しています。

「Rails Way」のおさらい

私の事例をご紹介する前に、「Rails Way」のどの点が素晴らしく、どの点が不十分であるかをしっかり理解しておくことが重要と考えます。それによって、私からの提案がさらに明瞭になればと願っています。

データの読み込みやビューとのデータのやり取りを、冗長な(Rails的でない)方法で行うと次のようになります。

def show
  user = User.find params[:id]
  render :show, locals: { user: user }
end

運のよいことに、Railsフレームワークの開発者はどのアクションも最終行が似たり寄ったりであることに気付いたので、テンプレートに渡す必要のあるデータを何度も何度も書くことにきっと嫌気がさしたのでしょう。そしてこうした定型コード(boilerplate)を減らすために賢い方法を2つ編み出しました。

  • @変数のハック
  • 暗黙のレンダリング

では、これら2つのRails的手法を適用するとどうなるかを見てみましょう。

def show
  @user = User.find params[:id]
end

暗黙のレンダリングでは、レンダリングするテンプレートをアクション名で決定します。私はこちらのRails Wayを愛していますので、これは変えたくありません。問題にしたいのは、もうひとつの@変数のハックの方です。

@変数のハック

そもそも「@変数のハック」とは何でしょうか。Railsフレームワークの開発者は、何らかのハッシュ的なものを用いてビューに変数を渡すのではなく、その変数にマーキングするだけの方がよいだろうという決定を下したのです。

マーキングはどのようにして行われるかご存知でしょうか?Ruby自身には、変数にマーキングする方法が2とおりあります。変数の先頭に$記号または@記号を付けることでマーキングできます。

Rubyで$記号を付けると、値がグローバルになるのでよくありません。では@記号はどうかというと、通常であれば、その変数は同一のインスタンス上でのあらゆるメソッド呼び出しで共有されるべきであるという指定になります。これはRubyや多くのオブジェクト指向言語でネイティブでサポートされている「インスタンス変数」と呼ばれるものです。

Railsフレームワークの開発者は、コントローラではインスタンス変数の使いみちがあまりないことに気付きました。コントローラは、概念上(開発者目線では、ですが)以下を行います。

controller = UsersController.new request
response = controller.show

もちろん内部では他にもいろいろやっているのは承知のうえで、概念上は以下にまとめられます。

  • インスタンスを1つ作成する
  • メソッドを1つ呼び出す
  • 作成されたオブジェクトを渡す

これは本質的にオブジェクト指向的というより関数型的です。

コントローラオブジェクトはさほど長生きしませんし、呼び出すメソッドはたった1つなので、コントローラではインスタンス変数の出番があまりありません。Railsフレームワークの開発者はここに目をつけて、@というマーカーを別の目的に転用する決断を下したのです。

技術的にはインスタンス変数であることは変わりませんが、Railsはこれらの変数を監視してビューにコピーします。これによって、ビューに渡す変数のハッシュを指定する必要がなくなりました。

ある機能を(言語設計者の)意図しない目的に転用するという意味で、私はこれをハックと呼んでいます。「ハックだからよくない」ということではありませんのでお間違いなきよう。定形コードを削減するために未使用の機能を転用するのは賢いやり方です。私が問題にしたいのは、このハックそのものではなく、実際にはこのハックでも満たせないニーズがあるという点に尽きます。

何が問題なのか

@変数のハックのどのあたりに問題があるのでしょうか?@マーカーを転用したことが問題なのではなく、読み込みのパターンが複数のアクション間で共有されてしまっていることが問題なのです。次のように、いくつものアクションが同じようなデータを欲しがるというのはよくあることです。

def show
  @user = User.find params[:id]
end

def edit
  @user = User.find params[:id]
end

他のupdatedestroyなどのメンバーアクションも同様です。Railsには、甚だしい繰り返しをコールバックで解決する手法があります。上のように書く代わりに、以下のように書くことができます。

before_action(only: %i[show edit]) { @user = User.find params[:id] }

def show
end

def edit
end

しかしコールバックによる手法にはいくつもの問題があります。

  • only:except:を用いて特定のアクションだけを対象にしようとするとエラーが発生しやすくなります。これらのリストがちゃんとメンテされていないばかりに誤ったデータを読み込んでいたアプリを山ほど目撃してきました。
  • アクションの実際の動作が見えづらくなります。アクションだけを眺めても、何もしてないように見えてしまいます。
  • 現実世界の巨大なコントローラでコールバックを把握するのはつらい作業になる可能性があります。コールバックが親クラスで定義されていたりモジュールとしてincludeされていればなおさらです。
  • ソースコードの順序がシビアになります(あるbefore_actionフックで別のbefore_actionで読み込んだデータが必要になるなど)

「メソッドでやろうよ」

私の提案するソリューションはいたってシンプルです。一言で言えば「メソッドでやろうよ」です。

私の提案では私たちのニーズがすべて勘案されているので、まともな定形コードを素早く構築できます。定形コードはまさに@ハックが殺そうとしていたものなのですから、ぱっと見歴史を繰り返しているように思われるかもしれません。私のアイデアがお気に召すかどうかを皆さんにご検討いただくためにも、どうかもうしばらくお付き合いください。最終的には、ささやかなメタプログラミングを用いてあらゆる定形コードをラップし、(@ハックの)メリットを失うことなく簡潔なコードに作り変えます。

メソッドを定義する

典型的なオブジェクト指向システムにおいて、あるコンポーネントが他のコンポーネントからデータを取得する最もシンプルなソリューションと言えば何だかおわかりでしょうか?最初のコンポーネントが、その情報を担当する次のコンポーネントにメッセージを送信することです。この「メソッドを介するメッセージ送信」は、オブジェクト指向プログラミングの核となるアイデアです。

さて、データをビューに送信しようとするのではなく、両者の立場を逆転させて、ビューが自分の欲しいデータをコントローラに問い合わせる形にしてはどうでしょうか?つまり、データを要求するメッセージはビューからコントローラに送信し、コントローラはレスポンスでデータを返すことになります。

ビューでは以下のような感じになります。

<%= controller.user.name %>

そしてコントローラは次のような感じになります。

def user
  User.find params[:id]
end

これは単なるpublicメソッドであり、オブジェクト指向としてはごく普通の考え方です。このメソッドは別のテンプレートからも使えます。editテンプレートとshowテンプレートのどちらもuserを欲しがっているのであれば、どちらも同じメソッドを呼べばよいのです。indexテンプレートでは欲しくないのであれば、indexテンプレートで呼ばなければよいのです。必要もないのにデータを読み込むことはしません。コールバックの定義でonly:except:のリストを今後いつまでもいじくりまわす必要もありません。

メモ化

userの属性を大量に出力したいが、コントローラで新しいインスタンスを毎回読み込むのは嫌だ。そんなときは次のように読み込みをメモ化(memoize)しましょう。

def user
  @user ||= User.find params[:id]
end

上のコードではあえてインスタンス変数を用いていることにご注意ください。しかしこれはインスタンス変数の本来の用い方なのです(同一インスタンス内にある異なるメソッド呼び出し同士でデータを共有する)。上のコードではインスタンス変数がnilの場合が考慮されていませんので、もう少しちゃんと書くと次のようになります。

def user
  return @user if defined? @user
  @user = User.find params[:id]
end

ヘルパー

もはや概念上は「ハック」ではなくなりましたが、その分テンプレートはわずかに冗長になりました(コントローラも冗長になりましたが、これについては後述します)。いちいちcontroler.なんちゃらみたいな変数にアクセスしたくはありません。コントローラへのプロキシを次のようなヘルパーメソッドにしてみてはどうでしょうか。

module UsersHelper
  def user
    controller.user
  end
end

これでテンプレートは以下のように書くだけで済みます。

<%= user.name %>

これはこれでありがたいのですが、データ読み込み系メソッドごとにヘルパーメソッドをいちいち書くのはだるくて仕方ありません。幸いなことに、Railsにはこんなときに使える手があります。コントローラのどのメソッドでも、helper_methodを呼んでおきさえすればRailsがヘルパーメソッドを代わりにこしらえてくれます。これでコントローラのデータ読み込み部分は次のように書けます。

def user
  return @user if defined? @user
  @user = User.find params[:id]
end
helper_method :user

メソッドを「代入可能」にする

これらのメソッドを代入可能にする(訳注: =で終わるいわゆるセッターメソッドを定義する)と、読み込みの振る舞いをアクション間でもっとうまく共有できることにも気が付きました。たとえば何らかのアクセス制御を行うとしましょう。定義はおそらく次のようになります。

def users
  return @users if defined? @users
  @users = policy_scope User.all
end
helper_method :users

def user
  return @user if defined? @user
  @user = users.find params[:id]
end
helper_method :user

ここでは、データ読み込みメソッド(users)のひとつを用いて、他のメソッド(user)の実装を支援しています。before_actionコールバックによる方法とは異なり、ソースコードの順序はまったく影響しません。なにしろ単にメソッドを呼んでいるだけなので、userを先に定義しても構わないのです。

ここまでは何もかもうまくいってる感ありますね。今度はindexアクションで検索もできるようにしたいとしたらどうでしょうか?indexアクションの定義は次のようになるでしょう。

def index
  @users = users.search params[:q]
end

ここでようやっといつもの@ハックに立ち戻りました。もし(インスタンス変数でない)usersデータ読み込みメソッドに検索結果を代入すると、showアクションでも現状のuserの実装に合わせて自前で検索を行うはめになります。データ読み込みの振る舞いの一部(policy_scopeなど)はアクション間で共有したいが、その他の側面(検索)は共有したくない、というのは一般によくある問題です。

この問題も、代入によって解決できます。次のように、あるアクションでデータを絞り込めるよう、別のメソッドを定義してみてはどうでしょう。

private

def users= assigned_users
  @users = assigned_users
end

これで次のようにindexアクションを定義できます。

def index
  self.users = users.search params[:q]
end

先ほどメモ化を実装しておいたことにより、同じインスタンス変数が使われていれば(繰り返しますが、このインスタンス変数は同一インスタンス内のメソッド呼び出し間でデータを共有するのに使われます)、indexビューでusersを呼び出すとpolicy_scopeや検索が適用されたリストを取得できます。showアクションでuserを呼び出せば、policy_scopeのみが適用され、検索は除外されます。

この代入メソッド(users=)はprivateにしてあります。理由は、このメソッドはそのアクション内(またはせいぜいbefore_actionフック)でしか使われないからです。このメソッドをコンポーネントの外(ビューなど)で使う理由はとんと思い当たりません。

テストを書く

この提案におけるもうひとつの絶大なメリットは、テストの書きやすさです。これらのメソッドはいずれもpublicなので、テストでまったく普通に呼び出せます。たとえばindexアクションで検索がちゃんと行われているかどうかをテストしたい場合、従来の方法では、おそらく以下のような感じのテストを書くでしょう(RSpec構文とFactoryBotを使用)。

it 'searches for the given query' do
  create :user, last_name: 'Doe'
  create :user, last_name: 'Jackson'

  get '/users', params: { q: 'Doe' }

  expect( response.body ).to have_content 'Doe'
  expect( response.body ).not_to have_content 'Jackson'
end

上のテストコードには、出力されたテンプレートにキーワードが含まれているかどうかをチェックすることでコントローラが正しく振る舞っていると見なすという、暗黙の前提があります。しかし、そのページに何かのはずみでJacksonという単語が紛れ込んでしまえば、テストは「正しく機能していない」という理由で失敗するでしょう。しかしこの失敗は本当の失敗ではなく、false positive(偽陽性)です。

それでは、先ほどのpublicメソッドを用いて同じテストを書いてみましょう。

it 'searches for the given query' do
  expected = create :user, last_name: 'Doe'
  create :user, last_name: 'Jackson'

  get '/users', params: { q: 'Doe' }

  expect( controller.users ).to eq [expected]
end

こちらのテストは実に頑丈にできています。欲しいレコードが見つかること、そしてそれ以外のレコードが検索されないことを確認しています。false positiveをうっかり引き起こすような副作用の起きる余地はありません。

定形コードを減らす

皆さまがここまで投げ出さずに読んでくださり、そして私の推す提案を気に入っていただければ幸いです。しかし、まだ「定形コードを何度も書くのがだるい」という問題が残されています。何やかやで、現在のデータ読み込みメソッドは以下のようになっています。

def user
  return @user if defined? @user
  @user = User.find params[:id]
end
helper_method :user

private

def user= assign_user
  @user = assign_user
end

えっへん!Rubyには、この手の共通パターンをシンプルに書くのにうってつけのメタプログラミングという強い味方があるのです。上のコードで提供したいものは、結局次の2つに集約されます。

  • データ変数の名前
  • そのデータ変数の読み込み方法

開発者の皆さまが普段から慣れ親しんでいるRSpecと対になるよう、この新しいメソッドにletと命名しました。データ読み込みにメタプログラミングを用いると、次のような感じで書けます。

let(:user) { User.find params[:id] }

簡潔でありながら、先ほどの定形コードによる方法のメリットを何ひとつ失っていません。Lettableモジュールでどんなメタプログラミングが使われているのか、とくとご覧ください(Gist)。

module Lettable
  def let name, &blk
    iv = "@#{name}"

    define_method name do
      return instance_variable_get iv if instance_variable_defined? iv
      instance_variable_set iv, instance_eval(&blk)
    end
    helper_method name

    define_method :"#{name}=" do |value|
      instance_variable_set iv, value
    end
    private :"#{name}="
  end
end

上のコードをapp/controllers/concernディレクトリに放り込んで、ApplicationControllerでこのコードをextendすれば準備完了です。実にこぢんまりと書けたコードです。ばかでかいライブラリを持ち出す必要もありません。letという名前がお気に召さないのであれば、好きに変えていただいて構いません。

コード例

上のコードを用いるとコントローラをどんなふうに書けるか見てみましょう(Gist)。

class WidgetsController < ApplicationController
  let(:widgets) { Widget.all }
  let(:widget) { widgets.find_or_initialize_by id: params[:id] }

  def new
    render :form
  end

  def edit
    render :form
  end

  def create
    save
  end

  def update
    save
  end

  def destroy
    widget.destroy
    redirect_to widgets_url, notice: 'ウィジェットは削除されました'
  end

  private

  def save
    if widget.update secure_params
      redirect_to widget, notice: 'ウィジェットは保存されました'
    else
      render :form
    end
  end

  def secure_params
    params.require(:widget).permit :name
  end
end

私はcreateupdateのどうでもいいような差分を消し去るのが大好きなので、テンプレート名をformとし、パーシャルレンダリング用のダミーテンプレートは一切用いませんでした。もちろん、このあたりの書き方は好みに応じて変えていただいても一向に構いません。

このletによる手法を既存のコントローラに導入したとしても、全コントローラをがっつり書き直す必要はありません。皆さまのお好きなようにコーディングしていただければ結構です(訳注: 少しずつコントローラを書き直すなど)。letは単にアクション間やコントローラ-ビュー間でデータ読み込みを共有し、テストを楽にするためのものでしかないのです。

影響を受けたgem

私の提案は、decent_exposure gemのライブラリから影響を受けていることをここに認めるものであります。このライブラリのおかげで最初の着想に触れることができました(ダジャレにあらず)。decent_exposureで好きになれなかったのは「暗黙の読み込み」の部分でした。これをやると隠蔽が甚だしくなり、カスタマイズが難しくなるからです。

訳注: 「exposed me to the idea」とdecent_exposureのシャレと思われます。なおdecent exposureは「個人情報の適度な露出」という流行語です。

decent_exposureを用いることも検討しましたが、暗黙の読み込みはどうしても使いたくありませんでした。そして、巨大なライブラリを導入しなくても、ひとかけらのメタプログラミングさえあれば十分やれることに気付き、それをconcernに置くのがよいと決意するに至ったのです。

関連記事

Railsコードを改善する7つの素敵なGem(翻訳)

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


Ruby: シングルトンオブジェクトをデフォルト引数として使う(翻訳)

$
0
0

概要

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

Ruby: シングルトンオブジェクトをデフォルト引数として使う(翻訳)

オプション引数を1つ取るメソッドを定義したくなることがときどきあるかと思いますが、プログラマーがこのメソッドにnilを渡す可能性がないわけではありません。かつコードの側では、値が提供されない(つまりデフォルト値にする)場合とnilの場合を区別する必要があるとします。どうやったらこれをうまくさばけるでしょうか?

こういう場合によく使われるのは、nilやその他の空値やゼロ値をデフォルト値としてメソッドを定義するというものです。0や文字列、空の配列などの場合であれば、この方法は理にかなっています。

class Foo
  def bar(one, two: nil)
    # ...
  end
end

しかし、nilと、値が指定されていない場合を区別する必要がある場合はどうしたらよいのでしょうか。具体的には、次の2つを区別する方法です。

foo.bar(:something, two: nil)
foo.bar(:something)

こんな場合に使えるのは、単一かつ一意のオブジェクトを定義して、それをデフォルトに用いる方法です。そして、渡された引数がnilかどうかをチェックするのではなく、シングルトンオブジェクトであるかどうかをチェックします。

class Foo
  NOT_PROVIDED = Object.new

  def bar(one, two: NOT_PROVIDED)
    puts one.inspect
    if two == NOT_PROVIDED
      puts "not provided"
    else
      puts two.inspect
    end
  end

  private_constant :NOT_PROVIDED
end

private_constantは無理して使う必要はありませんが、私はこのメソッドを引数に使えることと、privateクラスでも同じようなことができることをRuby開発者に思い出していただきたいのです。

Foo.new.bar(1)
1
not provided

Foo.new.bar(1, two: 2)
1
2

:not_providedのようにシンボルを使ってもよいですし、数値などRuby内部で一意であるものなら何でも構いません。しかし後述のassert_changesのような一般的なメソッドでは、引数に有効なオブジェクトが渡される可能性もあります。最善の方法は、引数として受け渡せない一意のオブジェクトを使うことです。

以下は、Railsでこれを用いてassert_changesを実装する方法です。

assert_changes :@object, from: nil, to: :foo do
  @object = :foo
end

assert_changes -> { object.counter }, from: 0, to: 1 do
  object.increment
end
UNTRACKED = Object.new
def assert_changes(expression, message = nil, from: UNTRACKED, to: UNTRACKED, &block)
  exp = if expression.respond_to?(:call)
    expression
  else
   -> { eval(expression.to_s, block.binding) }
  end

  before = exp.call
  retval = yield

  unless from == UNTRACKED
    error = "#{expression.inspect} isn't #{from.inspect}"
    error = "#{message}.\n#{error}" if message
    assert from === before, error
  end

  after = exp.call

  if to == UNTRACKED
    error = "#{expression.inspect} didn't changed"
    error = "#{message}.\n#{error}" if message
    assert_not_equal before, after, error
  else
    error = "#{expression.inspect} didn't change to #{to}"
    error = "#{message}.\n#{error}" if message
    assert to === after, error
  end

  retval
end

私はどうやら、RSpecのこのアプローチが好みです。

expect do
  object.increment
end.to change{ object.counter }.from(0).to(1)

しかしその一方で、UNTRACKEDオブジェクトを用いるassert_changes実装もありだと思います。

しかしこれは、論理値(boolean)引数にどこか似ています。これは、2つの異なるオブジェクトで定義されるべきフラグとしてよく用いられます。そこで、foo(1, true)foo(1, false)よりも、単にfoo(1)bar(1)とする方がよいとされることが多く、私も普通はこのガイドラインに従います。ただしassert_changesの場合であれば、名前付き引数とシングルトンオブジェクトの組み合わせはよいのではないかと思います。

お知らせ: もっと知りたい方へ

本記事を気に入っていただけた方は、Arkencyのニュースレターの購読をお願いします。開発者を悪い意味で驚かせないRailsアプリを構築するための弊社のノウハウを毎日お送りします。

よろしければ以下もどうぞ(訳注: いずれも英語記事です)。

弊社の最新刊『Domain-Driven Rails』もぜひどうぞ。特に、巨大で複雑なRailsアプリを扱ってる方に有用です。

関連記事

Rails: モデルの外では名前付きスコープだけを使おう(翻訳)

週刊Railsウォッチ(20180615)TTY gemとHTTPClient gemは優秀、Rubyの謎フリップフロップ、ちょいゆるRubyスタイルガイドほか

$
0
0

こんにちは、hachi8833です。仙台疲れが今になって来たような気がしないでもありません。雨の降らない国に行きたいです。

晴耕雨読のウォッチ、いってみましょう。

各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ

⚓Rails: 今週の改修(Rails公式ニュースより)

今回も主に公式のコミット情報からです。

⚓TableDefinition#columnでカラム定義が重複すると例外を出すように修正

# activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L354
       def column(name, type, options = {})
         name = name.to_s
         type = type.to_sym if type
         options = options.dup

-        if @columns_hash[name] && @columns_hash[name].primary_key?
-          raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
+        if @columns_hash[name]
+          if @columns_hash[name].primary_key?
+            raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
+          else
+            raise ArgumentError, "you can't define an already defined column '#{name}'."
+          end
         end

         index_options = options.delete(:index)
         index(name, index_options.is_a?(Hash) ? index_options : {}) if index_options
         @columns_hash[name] = new_column_definition(name, type, options)
         self
       end

つっつきボイス:TableDefinitionなんてのがあるのね」「マイグレーションがらみのようです」「create_tablet.columnが呼ばれるときの重複チェックを今回修正したと: テストコードがわかりやすい↓」「普通やらかさないエラーだろうけどraiseはしとかないとね😋

# activerecord/test/cases/migration/change_schema_test.rb#L199
+      def test_create_table_raises_when_defining_existing_column
+        error = assert_raise(ArgumentError) do
+          connection.create_table :testings do |t|
+            t.column :testing_column, :string
+            t.column :testing_column, :integer
+          end
+        end
+
+        assert_equal "you can't define an already defined column 'testing_column'.", error.message
+      end

参考: Ruby on Rails 5.2 / ActiveRecord::ConnectionAdapters::TableDefinition — DevDocs

columnがconnection_adapters/abstract/schema_definitions.rbにあるということは、abstractすなわち複数のコネクタで共通で使われるメソッドなんでしょうね: ぱっと見ですが」「abstractクラスなのでそれで合ってると思います🧐: 細かくはちゃんと追わないといけないですが、上のエラーももしかするとDBMSの種類によっては通っちゃうことがあったのかもしれないですね: SQLite3とか割と作りが雑なんでカラム名かぶっても通っちゃうとかありそう、知らんけど😆」「😆

「そういえば先週のウォッチでもSQLite3ラブラブ❤な意見が出てました」「ちなみにSQLite3、かなり速いっすよ🕶: Win32アプリみたいなPCローカルアプリで使うDBMSとしては読み込み速度とかめっちゃ優秀」「あー、そういえば以前いた職場の社内ツールでも中でSQLite3が走ってました」「ただSQLite3って型とかが割と雑で、確か内部では数値なんかも文字列で保存してたような覚えがあるんですよ」「『保存してSQLっぽく検索できればそれでいいのっ』って感じなんでしょうね😓」「だからこそ速いんでしょうけどね🤓

参考: SQLite Home Page


sqlite.orgより

⚓ActiveSupport::Time.zone.atTime::atの引数の不揃いを解消

# 同PRより
# Before:
Time.at(946684800, 123456.789).nsec       #=> 123456789
Time.zone.at(946684800, 123456.789).nsec  #=> ArgumentError (wrong number of arguments (given 2, expected 1))
# After:
Time.at(946684800, 123456.789).nsec       #=> 123456789
Time.zone.at(946684800, 123456.789).nsec  #=> 123456789
# activesupport/lib/active_support/values/time_zone.rb#L357
-    def at(secs)
-      Time.at(secs).utc.in_time_zone(self)
+    #
+    # A second argument can be supplied to specify sub-second precision.
+    #
+    #   Time.zone = 'Hawaii'                # => "Hawaii"
+    #   Time.at(946684800, 123456.789).nsec # => 123456789
+    def at(*args)
+      Time.at(*args).utc.in_time_zone(self)
     end

つっつきボイス:Time.atTime.zone.atを揃えたようです」「おや?これで見るとTime.zone.atの方を変えてる?: Time.zone.atの方が使用頻度多いし(つかRailsではそもそもこっちを使うべき👮‍♂️)、Time.atをじかに使うことってほとんどないと思うんだけどなー」「それもそうですね…🤔」「breaking changeになりそうに見える🕶」「そのときはRailsアップグレードガイドに載るんでしょうね」

参考: Ruby on Rails 5.2 / Time::at — DevDocs
参考: Ruby on Rails 5.2 / Time::zone — DevDocs

⚓.dup.freeze-ショートハンドに置き換え

# activemodel/lib/active_model/attributes.rb#L103
       def self.set_name_cache(name, value)
         const_name = "ATTR_#{name}"
         unless const_defined? const_name
-          const_set const_name, value.dup.freeze
+          const_set const_name, -value
         end
       end

つっつきボイス: 「出たな-🤣」「あ、これ以前のウォッチでもみんなで『キモチワルイー』って言ってた文字列dup&freezeのショートハンドですね😆」「確かRuby 2.5の機能じゃなかったっけ?」

毎度のことですが、記号は検索しづらいです😭。マイナス記号-です。

参考: instance method String#-@ (Ruby 2.5.0)

selfがfreeze されている文字列の場合、selfを返します。 freezeされていない場合は元の文字列のfreezeされた (できる限り既存の) 複製を返します。
ruby-lang.orgより

参考: NEWS for Ruby 2.5.0 (Ruby 2.5.0)
参考: Feature #13077: [PATCH] introduce String#fstring method - Ruby trunk - Ruby Issue Tracking System

⚓記号の雑学

ついでながら、マイナス記号とハイフン記号とダッシュ記号は本来は別の記号だったのですが、記号を増やしようがないタイプライターの時代に1つのキーで代用されてしまい(長いemダッシュは--としたり)、それがそのままASCIIなどにも持ち込まれてハイフンマイナスという苦し紛れな名前が付けられ、Unicodeで別記号が用意された今も地味に混乱を呼び続けてます。ハイフン記号とマイナス記号とダッシュ記号(emダッシュとenダッシュ)の使い分けには闇が横たわっています。ヨーロッパの一部には、ハイフンとダッシュを取り違えて使うとマジギレする国がありますのでご用心。見た目1ドットぐらいしか違わないんですけどね…😫



Dash vs. Hyphen ~ IngliszTiczer.plより

参考: ハイフンマイナス - Wikipedia
参考: ハイフン - Wikipedia
参考: ダッシュ (記号) - Wikipedia

⚓重複した子レコードを親がsaveしないよう修正

# 同issueより再現コード
# モデル

class Parent < ApplicationRecord
  has_many :children
end

class Child < ApplicationRecord
  belongs_to :parent
  validates :name, uniqueness: true
end

# コード
parent = Parent.new(children: [Child.new(name: 'Wiske'), Child.new(name: 'Wiske')])
parent.save
parent.errors.any?  # エラーになるはずが、ならない

つっつきボイス:validates uniqueness: trueだから本来エラーにならないといけないやつ: へー、この書き方↓するとすり抜けちゃってたのか😇」「😇」「これは下手すると既存のアプリでも知らないうちにエラーを作り込んじゃってたりすることがあるかも?自分はこの書き方はしないけどねっ🕶

Parent.new(children: [Child.new(name: 'Wiske'), Child.new(name: 'Wiske')])

「ついでに聞いちゃいますけど、今見ているコードにあるreflection↓っていう言葉は、ここではどういう意味が込められているんでしょうか?(英語的にいっぱい意味がありすぎるので🤯)」「reflectionというと、メソッドオブジェクトを取ってきてそれをゴニョゴニョしたいなんてときに使いますね」「あー、メタプログラミング的な?」「たぶんプログラミング関連ではそれ以外の意味でreflectionという言葉が出てくることはあまりないと思うので💦

# activerecord/lib/active_record/autosave_association.rb#L381
       def save_collection_association(reflection)
         if association = association_instance_get(reflection.name)
           autosave = reflection.options[:autosave]
...
               if autosave != false && (@new_record_before_save || record.new_record?)
                 if autosave
                   saved = association.insert_record(record, false)
                 elsif !reflection.nested?
-                  association_saved = association.insert_record(record)
                   if reflection.validate?
-                    saved = association_saved
+                    valid = association_valid?(reflection, record, index)
+                    saved = valid ? association.insert_record(record, false) : false
+                  else
+                    association.insert_record(record)
                   end
                 end
               elsif autosave

「たとえばですが、reflectionという名前の変数があるとすると、その中には状況によってさまざまなオブジェクトが動的に入る可能性があるよ、という意図が強調される感じですかね」「おー、『あのクラスとあのクラスだけが入ると思うなよ』っていうイメージですか」

「あくまでイメージというか概念寄りの話ですが、reflectionという言葉が使われていると、そこにどんなオブジェクトが入るかわからないし、どんなオブジェクトが来てもいいという感じ: C言語の(void *)キャストとか、Javaの(object)キャストなんかが概念的に似ているかな」「なるほどー!😀

参考: C言語: 汎用ポインタ
参考: 強く型付けされているJavaの理解に必修の“型変換” (2/3):【改訂版】Eclipseではじめるプログラミング(18) - @IT

「このコードならreflection.validate?とあるから、普通ならreflectionにはActiveRecordオブジェクトやActiveModelのインスタンスが入るんだろうけど、それ以外のオブジェクトが入る可能性だってあるんだぜ、という主張を込めて自分なら使うかな😎、ここでは知らんけどw」「😃😃

reflectionには「反射」「反省(self reflection)」「反映」などなどいろんな意味がありますが(「反」の文字が共通してますね🧐)、ポイントは意味が受動的なところかなと思いました。反映もapplyで表すと人間が能動的にやるイメージですが、reflectだと受動的というか性質に従って自動的に行われる、というニュアンスが強いと思います。reflectionがメタプロの文脈でよく使われるのもそれなのかなと。

人間の態度や状況が何かにreflectするとは、そうした態度や状況(またはそう思われるもの)がそこに存在することが示されていることを表す。
Cobuldより大意

⚓初期化ブロックがbuild_recordにあるのをbefore_addに移動

以下はコミットログから見繕いました。

# https://github.com/rails/rails/commit/c256d02f010228d260b408e8b7fda0a4bcb33f39#diff-20f545c453ee24942b6f7ae565e9e369R104
       def build(attributes = {}, &block)
         if attributes.is_a?(Array)
           attributes.collect { |attr| build(attr, &block) }
         else
-          add_to_target(build_record(attributes)) do |record|
-            yield(record) if block_given?
-          end
+          add_to_target(build_record(attributes, &block))
         end
       end

つっつきボイス: 「改修範囲が広いので取りあえずこれだけ引用してみました」「ここだけ見て言うと、add_to_targetというのはおそらくフックを追加するメソッドかな: で、そういうコールバック登録の枠組みが既にあるならそれに揃えようよってことなんだと思う」「😀

# rails/activerecord/lib/active_record/associations/collection_association.rb#L282
      def add_to_target(record, skip_callbacks = false, &block)
        if association_scope.distinct_value
          index = @target.index(record)
        end
        replace_on_target(record, index, skip_callbacks, &block)
      end

⚓force_equality?をpublic APIに

43ef00eの続きだそうです。

# https://github.com/rails/rails/pull/33067/files#diff-e66146ba2ab968e251944a764e4ed967R54
+      def force_equality?(value)
+        coder.respond_to?(:object_class) && value.is_a?(coder.object_class)
+      end

つっつきボイス:@kamipoさんが何度かに分けて修正したようです」「force_equality?ってメソッドがあるのか: どうやらrespond_to?(:object_class)でオブジェクトのクラスまで含めて等しいかどうかをチェックするやつみたいですね」「おー」「たとえばシリアライズしたときのパラメータのリストは同じでも、オブジェクトの種類が違うものがやってくることがあったりするけど、そういうのも検出するということでしょうね」「😃

「このメソッドなら、たとえばSTI(Single Table Inheritance)なんかでそれが親クラスのオブジェクトなのか子クラスのオブジェクトなのか、なんてのも見分けられるはず」「なるほど!」「オブジェクトが参照しているデータベースが同じだとシリアライズした後では区別できなくなっちゃうけど、シリアライズより前にそういうところをチェックするときに使えるんじゃないかな?ってね🤓」「😋」「これが欲しい気持ち、よくわかる」

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

参考: Rails ActiveRecordのSTI(Single Table Inheritance)の使い方 | EasyRamble

⚓Rails

⚓Bundler 2リリース間近のチェックリスト

Bundler関連ということで関連してこちらも。


つっつきボイス: 「リリースはまだみたいなので、こういうことをやりますリストですね」「そういえば今はもうRubyをバージョンアップすると最新のBundlerが入るようになったんだっけ?どっちだったっけ?」

そういえば2.5でのBundler標準化は延期されたのでした↓。

「チェックリストをざざーっと見た感じでは互換性の心配ほとんどなさそうというか、こっちでやることあまりないかも?: つか互換性なかったらRubyバージョンを単純に上げたときに即死だし😇」「Bundlerといえば、HerokuのBundlerが最新でないのでwarning出たりたまにつっかえたりするんですよね(今はどうだったかな💦)」「まーHerokuの場合Herokuの環境に身を任せないといけないので、面倒を見てくれる範囲は大きいけど自分はコンテナでやりたいかなー😂」「😀」「Linuxサーバーの面倒なところ触りたくないっという人にはもちろんHerokuはおすすめですね: Railsチュートリアルは今もHeroku使ってるんだったかな?」「今は違ってたような…?」

後で調べたら、Railsチュートリアルは今もHeroku使ってました

⚓graphql-ruby: Railsとも連携


graphql-ruby.orgより

今回のウォッチは何となくShopify成分が多めでした。


つっつきボイス: 「これ扱ったことなかったっけ?」「なかったっぽかったので(と思ったらありました💦)」「このロゴは素晴らしいですね😍」「グラフ理論のグラフをうまくあしらってるし😎

参考: apollo + rails(graphqlサーバー)でファイルをアップロードするmutationを作る方法 - Qiita

⚓Active Record 5.2のメモリ肥大化を探る

このSam Saffaronさんのブログは良記事多いですね。翻訳許可をいただいたので今後出したいと思います。

# 同記事より
a = []
Topic.limit(1000).each do |u|
   a << u.id
end

つっつきボイス: 「あー、↑こうやって書けばそりゃ確かに肥大化するナ」「eachを使ってるから?」「eachだけじゃなくてu.idを参照してるから: このu.idを評価しないといけないのでu.idが呼ばれるたびにTopicというモデルのオブジェクトが生成される」「そっちかー」「しかもそれをa <<で代入しちゃってるからこのループの間このモデルオブジェクトがまったく解放されない」「😲」「pluckすると速いって記事に書いてあるのもたぶんそれで、pluckならモデルオブジェクトを保持する必要ないはずだし」「pluckは配列を返しさえすればいいですしね」

「確かこの間のRails Developers Meetup 2018でもまさにこの辺の話をしていた人がいたと思う」「自分見てなかったかも😓

後でmorimorihogeさんからセッション名を教えていただきました↓。

参考: railsdm2018で「ActiveRecordデータ処理アンチパターン」を発表しました - Hack Your Design!

「まーでもActiveRecordの挙動とかlazy loadingのあたりなんかを忖度できるようにならないと、上のような書き方がヤバイ理由は見た瞬間にはわからないでしょうねー🧐」「そこが経験が必要なところかー😃」「あかんのはu.idという参照の仕方です😎: 終わってからGCすれば消えますけど(たぶんね)」「覚えます!」

参考: Ruby on Rails 5.2 / Enumerable#pluck — DevDocs

最近知ったRailsの便利なメソッド

⚓Rails 5で「関連付け先のないレコード」を使う(Ruby Weeklyより)


つっつきボイス: 「記事のスタイリングとかシンタックスハイライトがちと読みづらい…」「関連付けのない場合ってことなのかな?ざざっと見た感じ、assosiation先のレコードがなくても取得したい場合にLEFT JOINでの外部結合をActiveRecordでどう書くかって話のように見える」「時間ないので次行きましょうー」

  • Rails 4までは生SQLでJOIN
# 同記事より
User.join('left outer join posts on posts.user_id = users.id')
  .where(posts: {user_id: nil})
  .first
  • Rails 5はちょっとだけクリーンで読みやすい気がする
# 同記事より
User.left_joins(:posts)
  .where(posts: {user_id: nil})
  .first

⚓Rails 5.2のcredentialはセキュアではない?(RubyFlowより)


つっつきボイス: 「GitHub Wikiにざざっと書いた、1ページで収まる内容ですね」「これはわかりみあるヤツ: Rails 5.2のcrenentialは、言ってみれば例のTwelve-Factor Apps↓の原則3『設定を環境変数に格納する』に反してるんですよ」「あー」

参考: The Twelve-Factor App (日本語訳)


12factor.netより

「原理主義者としては許せないでしょうけど、そこまで行かなくても自分もこの辺は微妙: マスターキーの扱いっていろいろ難しいんですよ」「というと?」「チーム開発をしていると、開発メンバーが入れ替わるたびにマスターキーを変えないといけなくなるから: そして現実にメンバーの出入りはよくあることだし」「あー!🤫」「しかもマスターキーの変更履歴がリポジトリに残るのも、どうもねー🤔」「記事の人はnot secureとか書いてるけど、単にcredentialが好きじゃないんでしょうねー😆自分も使うつもりたぶんないし」「😆

⚓deep_pluck gemがRails 5.2に対応


つっつきボイス:pluckの入れ子版みたいなこのdeep_pluckウォッチで扱ったことありましたね: たぶんバッチとか書くときなんかに優秀なヤツ👍」「😀」「これもActiveRecordオブジェクトを生成しないから速いんだとしたら、自分なら生SQL書いちゃう、かなー?😆」「😆

⚓その他Rails

週刊Railsウォッチ(20170407)N+1問題解決のトレードオフ、Capybaraのテスト効率を上げる5つのコツほか


そろそろJSの組み合わせが爆発しそう。


つっつきボイス: 「ちょうど今日のチームミーティングでもWebpackについて発表ありましたけど、やっぱりWebpackerを知るにはWebpackそのものを知っておかないと結局後で困ることになりますね😆」「😆


webpack.js.orgより



つっつきボイス: 「テストって最初のうちはどの程度までみっちり書けばいいのか真剣に悩んじゃいました」「それはもう誰かに決めてもらうのが早い😎」「🤣」「ただ思うのは、よほどシビアなアプリでもない限り、カバレッジの数字だけを当てにしてもあまり意味がないかなってことですね」「カバレッジの値が増えたからといって本当に必要な部分がカバーできてるかどうかはまた別なのか🙂



つっつきボイス: 「2位のMagentoって知りませんでした」「自分も😁」「それにしてもShopify、いつの間にかこんないい位置についてるなー: ユーザー数はともかく、捌いているトラフィック数は凄い👁」「Shopify、Railsですしね🤠」「EC-CUBEと比較してみたいところだけど、日本製でほぼ日本市場だけだろうから比較は難しいかなー」



つっつきボイス:mizchiさんいいこと言うなー: フロントエンドの強い人で今は確かフリーランスだったかな?」「さっきのreflectionの話もそうでしたけど、コードを書いた人の意図を読み取る能力と、読み取れるようにコードを書く能力ってやっぱり重要ですね🧔🏽

そのうちIPA試験あたりにコード読解問題とか出たりするんでしょうか。


⚓Ruby trunkより

⚓Unicode 11のグルジア語の大文字小文字の取り扱いをどうしよう?


http://www.unicode.org/versions/Unicode11.0.0/ch07.pdf (Section 7.7, Georgian, pp. 320-321) より

Geogianが「グルジア語の」とも「ジョージアの」とも読めてしまうので、いちいち断りが必要です。グルジアというとスターリンの出身国というイメージ。なおグルジア語とロシア語には共通部分がほぼありません。


https://ja.wikipedia.org/wiki/%E3%82%B0%E3%83%AB%E3%82%B8%E3%82%A2%E8%AA%9E#/media/File:Kartvelian_languages.svg より

参考: グルジア語 - Wikipedia


つっつきボイス: 「これはもう多言語マニアがいないとどうしようもない😇」「issueでもそう言ってました😆

⚓GRND_NONBLOCKを設定せずにgetrandomが複数呼ばれるとロックする

if which ruby >/dev/null && which gem >/dev/null; then
    PATH="$(ruby -e 'puts Gem.user_dir')/bin:$PATH"
fi

つっつきボイス:GRND_NONBLOCKって、またLinuxに新しいフラグが出たのか」「(Linuxでしたか😓)」「よく見つけたなこれ感」

参考: getrandom(2) - Linux manual page

⚓Procの行番号/カラム番号、できればfirstやlastも取りたい

# rspec-parameterized gemのサンプル

  describe "lambda parameter" do
    where(:a, :b, :answer) do
      [
        [1 , 2 , -> {should == 3}],
        [5 , 8 , -> {should == 13}],
        [0 , 0 , -> {should == 0}]
      ]
    end

    with_them do
      subject {a + b}
      it "should do additions" do
        self.instance_exec(&answer)
      end
    end
  end

# 出力例

  lambda parameter
    a: 1, b: 2, answer: -> {should == 3}
      should do additions
    a: 5, b: 8, answer: -> {should == 13}
      should do additions
    a: 0, b: 0, answer: -> {should == 0}
      should do additions

tagomorisさんとjoker1007さんからのリクエストです。


つっつきボイス: 「最初気づかなかったけど、joker1007さんがそういう感じのgem作ってたそうです↓」「RSpecでwhere(:a, :b, :answer)みたいにwhereで指定したいってことのようだ: Procが複数になるとつらいとか、これは確かにやるなら言語でサポートする方がいいでしょうね😋

⚓Ruby

⚓Relaxed Ruby Style: ちょいゆるRubyスタイルガイド

Version 2.3とあるのはRubyのことなのかこのスタイルガイドのことなのか、どちらなんでしょう?


つっつきボイス: 「rubocop.ymlに軽くパッチを当てて使うようです」「パッチじゃなくて、元々rubocopにはinherit_fromというデフォルト設定を読み込む機能があるので、それを使って読み込んだ上で自分達のルールとの差分を上書きする、みたいな使い方ですね」「あー、そうでした😓」「うん、こういうのいいよね: BPS社内でも標準Rubocop.yml定めたいと思ってる🤓

⚓ramda-ruby:

# 同リポジトリより: トランスデューサー
  appender = R.flip(R.append)

  xform = R.map(R.add(10))
  R.transduce(xform, appender, [], [1, 2, 3, 4]) # [11, 12, 13, 14]

  xform = R.filter(:odd?.to_proc)
  R.transduce(xform, appender, [], [1, 2, 3, 4]) # [1, 3]

  xform = R.compose(R.map(R.add(10)), R.take(2))
  R.transduce(xform, appender, [], [1, 2, 3, 4]) # [11, 12]

  xform = R.compose(R.filter(:odd?.to_proc), R.take(2))
  R.transduce(xform, R.add, 100, [1, 2, 3, 4, 5]) # 104)
  R.transduce(xform, appender, [], [1, 2, 3, 4, 5]) # [1, 3])
  R.into([], xform, [1, 2, 3, 4, 5]) # [1, 3])

JavaScriptのRamdajsをRubyに移植したものだそうです。名前からlambdaのもじりという感じで、ramdajs.comには「育ちすぎたram(子羊)じゃないんだなー、たぶん」とありました。ramだ。


つっつきボイス: 「ramda: いいのかその読み方で😆」「たぶん子羊じゃないといいつつ、シンボルマークは羊ですね😄」「関数型の追求というよりは、フィルタ的な便利ツール集という印象」


ramdajs.comより

参考: ラムダ計算 - Wikipedia

トランスデューサーそのものはめちゃめちゃ意味が広いですが、ここでは関数型言語よりの何かなんでしょうね。何も見ずに「translator + reducer」かなと推測。

参考: トランスデューサー - Wikipedia

⚓Rubyとmallocの問題

⚓TTY: Ruby錬金術師の秘薬

RubyKaigi 2018で自分が見てなかったやつです(´・ω・`)。


つっつきボイス: 「お、これ自分見ましたよ: いい内容👍」「😃」「TTYってマルチスレッド対応のプログレスバーとかいろいろ機能揃ってますよ」

「このTTYってrails new的にファイルをどさっと作るんで、腰を据えて本格的なCLIアプリを作るためのテンプレート的なものでしょうね: 気楽にさっと書くにはかなり重装備な感じ」「この間のウォッチで取り上げたoptparseとはまた違うんでしょうか?」「optparseは引数処理を楽に書けるヤツなのでまた違いますね🕶
「TTYは、railsコマンドとかsystemdコマンドみたいなものすごく複雑なCLIを見通しよく作れますね: その気になればaptyarnだって作れちゃう😆」「🤣」「TTY上でmarkdownすら書ける」「何だかすげー」

「ただまあ、今こんなごついCLI書くぐらいならWebアプリにするよなという気もすると言えばする」「確かにー」「でも最初CLIで完成させてからそれをWebアプリ化するという流れはありかなとも思うし: CLIならバッチとかとっても扱いやすいし、融通が利きやすいし」「たぶんデバッグもしやすいし😃

「『こういうCLIはRubyで作ればいいじゃん』っていう文化になったらいいなーと思うし」「こうやってTTYなんかで作ったRubyのCLIを、mrubyとかでコンパイルして配布できればいいのにとも思うし: 結局RubyのCLIって配布がだいたい問題になるんですよね」「確かにー」「CLIでのGo言語の強みはrun anywhereですからね🕶

⚓go-mruby: mrubyのGoバインディング

// 同リポジトリより
package main

import (
    "fmt"
    "github.com/mitchellh/go-mruby"
)

func main() {
    mrb := mruby.NewMrb()
    defer mrb.Close()

    // Our custom function we'll expose to Ruby. The first return
    // value is what to return from the func and the second is an
    // exception to raise (if any).
    addFunc := func(m *mruby.Mrb, self *mruby.MrbValue) (mruby.Value, mruby.Value) {
        args := m.GetArgs()
        return mruby.Int(args[0].Fixnum() + args[1].Fixnum()), nil
    }

    // Lets define a custom class and a class method we can call.
    class := mrb.DefineClass("Example", nil)
    class.DefineClassMethod("add", addFunc, mruby.ArgsReq(2))

    // Let's call it and inspect the result
    result, err := mrb.LoadString(`Example.add(12, 30)`)
    if err != nil {
        panic(err.Error())
    }

    // This will output "Result: 42"
    fmt.Printf("Result: %s\n", result.String())
}

ついでにquartzという、RubyからGoプログラムを呼び出すgemも見つけたのですが、RubygemにGoのnative extensionを含める感じではなさそう…なぜかこの方面はさっぱり発展しません(´・ω・`)。

⚓httprbはいいぞ(Ruby Weeklyより)


同リポジトリより

# 同リポジトリより
HTTP.post("http://example.com/upload", form: { file: HTTP::FormData::File.new(io) })

つっつきボイス: 「このhttprb/httpってどうでしょう?」「少なくとも表の青いところにある標準のNet::HTTPはまず使うことはないと思う、というかつらすぎて使いたくない😆」「😆」「😆


同記事より

参考: class Net::HTTP (Ruby 2.5.0)

「この表の中で一番メジャーなHTTPクライアントって、赤いところにあるHTTPClient↓なんじゃないかなー: 少なくとも自分的にはベスト」

「ただ、HTTPClientはREADMEがそっけないんですが、実はそこから地味にリンク貼られているRDocのAPIドキュメントがすごくしっかり書かれてるんですよ↓」「ほー!!」「ここ読むとわかりますけど、プロキシもちゃんと対応してるし、BASIC認証の先にあるBASIC認証の対応とか、環境変数からプロキシ渡すなんてのもできます」「いいこと聞いた!😍」「😍

「表の緑色のところにあるFaraday↓もひと頃流行ったけど、Faradayの抽象化は人によって好みが分かれるかもしれない: クローラーを書くとかならFaradayが向いてますね」「逆にステータスコードが取りたいとかヘッダを確認したいとかならHTTPClientがいい」

「で、本題のhttprbは表の分類からもHTTPClientと同じところを狙ってるというのが取りあえずは見て取れますね」「😃」「おっ?httprbのインストールはgem install httpだって: この名前よく取れたなー😲」「Rubygems.orgで決めてるんでしたっけ?」「早いもの勝ちです😎

⚓Rubyで書かれたワールドカップ試合情報CLI(RubyFlowより)

# 同リポジトリより
$ footty              # Defaults to today's world cup 2018 matches

今日は以下でした。

#1 Thu Jun/14       Russia (RUS) vs Saudi Arabia (KSA) Group A  @ Luzhniki Stadium, Moscow

つっつきボイス: 「試合結果というより、『今日ある試合は何だったっけ?』用っぽいです」「こういう文化っていいですよねー」

⚓Rubyの謎機能: フリップフロップ

# 同記事より
irb(main):021:0> (1..10).each {|i| puts i if i==3..i==5 }
3
4
5
=> 1..10

つっつきボイス:Less Feature-Rich, More Funという記事を翻訳していて、一番最後にRubyのフリップフロップという謎の機能について言及されていたので」「何だこれ…確かに謎🤔

「あー、if i==3..i==5..の前と後の条件がフリップフロップになってるのか!😲」「😲」「😲」「こんなのがコードレビューに出てきたらつらすぎ😭」「Ruby Gold試験になら出そうですね…😓

⚓その他Ruby




RubyKaigiの応募要項です。


ほのっとしちゃいました。その後英語版↓も出ました。

⚓クラウド/コンテナ/Linux

以下時間切れのため、つっつきはここまでです🙇。後追いで何か追記するかもしれません。

⚓Linuxのloadavgの問題を追求

はてブで見つけました。


つっつきボイス: 「これはとてもいい記事」

⚓Googleのgvisorすごいかも

この間のウォッチで軽く取り上げたgvisorですが、TCFM #22の前半がこの話題でもちきりでした。

⚓ワンライナー特集

⚓GitLabが立て続けに機能を拡大

最新の記事ではありませんが一応。


参考: GitLabがGoogleのKubernetes Engineを統合、コンテナアプリケーションのデプロイが超簡単に | TechCrunch Japan

⚓その他クラウド/コンテナ/Linux

⚓SQL

⚓PostgreSQLのメモリ設定(Postgres Weeklyより)

⚓ロシア発: PostgreSQL 10の論理レプリケーションの復旧(Postgres Weeklyより)


同記事より

かなり長いです。

⚓はじめてのマテリアライズド・ビュー(Postgres Weeklyより)


同記事より

参考: マテリアライズドビュー - Wikipedia

⚓JavaScript

⚓Vue Native: VuejsでネイティブWebアプリを作るフレームワーク


vue-native.ioより

⚓PhantomJSの開発が正式に終了し、アーカイブ化(JSer.infoより)

昨年のウォッチでもお伝えしたPhantomJSが正式に終了しました。お疲れさまです。

週刊Railsウォッチ(20171026)factory_girlが突然factory_botに改名、Ruby Prize最終候補者決定、PhantomJS廃止、FireFoxのFireBug終了ほか

参考: PhantomJSの開発が終了しリポジトリがアーカイブ化された - JSer.info

⚓mobx: JSのステート管理ライブラリ(JSer.infoより)


mobx.js.orgより

JavaScriptのステート管理はVuejsにもあったりReduxもあったりと賑やかですね。js.orgに集結している?


redux.js.orgより

JavaScript: Reduxが必要なとき/不要なとき(翻訳)

⚓ExcelでJavaScriptがサポート

TypeScriptを直接サポートするつもりはないそうです。

参考: Microsoft、Excelカスタム関数としてJavaScriptのサポートを発表

⚓その他JavaScript

⚓CSS/HTML/フロントエンド/テスト

⚓直感に反する「なぜか覚えられないUI」(Frontend Weeklyより)

↑著名な認知心理学者のDonald NormanことDon Normanの名前から「Norman door」と呼ばれているそうです。てっきりこのドアの発明者かと思ってしまいました。
引くかと思ったら押す、押すかと思ったら引くような、「押す/引く」表示がないとガンガンに間違えるドアだそうです。

参考: Urban Dictionary: Norman Door
参考: ドナルド・ノーマン - Wikipedia

⚓テスティングを楽しく学びたい人向けのサイトなど

  • 元記事: テストラジオ — テスティングの話題が豊富なポッドキャスト

既に60回を超えているんですね。凄い。


testerchan.hatenadiary.comより

⚓Apollo Clientとは


apollographql.comより

⚓言語よろずの間

⚓Mage: Goのタスクランナー


同リポジトリより

英語の古語にある「メイジ(魔法使い)」だよなと思いつつ、日本人なのでつい「マゲ」かと思ってしまいました。

⚓Fo: Goで関数型やってみる言語


play.folang.orgより

なお、Goでジェネリクスを欲しいと思ったことが今のところありませんでした。

参考: ジェネリックプログラミング - Wikipedia

⚓Stack Overflowのデベロッパーアンケート結果2018年版

毎度お騒がせ。

参考: 2018年 人気&嫌われプログラミング言語トップ25- Stack Overflow | マイナビニュース

⚓その他言語

参考: 依存型 - Wikipedia





⚓その他

⚓Gitコマンドを異世界転生モノで解説するよ

⚓らめぇそれ

⚓「不正指令電磁的記録に関する罪」とは

はてブで知りました。

⚓Windowsに新しいアプリインストール形式「MSIX」が登場予定

⚓その他のその他





社内勉強会のお題が「トランザクションとロック」だったので、そういうときについ思い出す動画です。

⚓番外

⚓今どきの法科学

⚓あれそんなにヤバイ物質だったのか

⚓学校でがっつり教えられた人の立場は

⚓香害

⚓リアルポケモン認定したい

⚓火星で有機物発見か

地球に巨大隕石がぶつかったか何かで火星に飛来した小型隕石によるコンタミの可能性が気になります。南極は一面真っ白で月の石や火星の石がちょくちょく見つかるので、逆もありそうな気がしてしまいます。

参考: 南極サイエンス基地 > 南極隕石


今週は以上です。

おたより発掘

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

週刊Railsウォッチ(20180608)特集「RubyKaigi 2018後の祭り」、`Enumerable#index_with`は優秀、コントローラから`@`を消し去るほか

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Ruby: ありそうでなかったRubyリファレンスの決定版を作った(翻訳)

$
0
0

概要

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

日本語タイトルは内容に即したものにしました。

https://rubyreferences.github.io/rubyref/
rubyreferences.github.ioより

Ruby: ありそうでなかったRubyリファレンスの決定版を作った(翻訳)

プログラミングを学習している知人とか、新しい言語に乗り換えようとしている友人からこんなことを聞かれたと想像してみてください。

そうね、Rubyやってみるわ。チェックしたいから取りあえず言語のリファレンス投げてもらえる?なお「はじめての何とか」みたいな教科書とか、Amazonに5年も前からあるような書籍のリンクとかはなしで。とにかく真っ当なリファレンスよろ。

さあ、あなたならこんなとき何と返答しますか?

どうしたらよいか考えてみましょう。

公式サイトには何と書いてあるでしょうか?Documentationセクションには有用なリソースがぎっしり掲載されています(とはいうものの「Getting Started」セクションに載っている一押しマニュアルの中に『Why’s (Poignant) Guide to Ruby』があるのはちょっと何だかなぁという感じです: 本書にはみんなの大好きな小技がたっぷり詰まっていますが、言語の正当な紹介とはまるで違います)。

しかし、同リンク集には小さいながら問題が1つあります。最新かつ網羅的かつ無償かつ公式の言語リファレンスがこのリンク集のどこにも見当たらないのです。

ここから選択可能な道は次のとおりです。

  • The first version of Programming Rubyは、Ruby 1.6だけが対象です。
  • Ruby User’s Guideは初期にMatz自らがしたためたもので、短いにもかかわらず有用です。私の推測する限りでは、少なくともRuby 2.1の頃までは更新されています。
  • Collaborative Wikibookは有用な一面もあるのですが、定数の項が「工事中」というのがWikiらしい点です。
  • Ruby Core Referenceは、まさしくリファレンスですが、その、何というか、ナビゲーション周りでもう一息頑張って欲しい感じです。特にRubyが初めての人のためにも。

ではここで、他の言語のリファレンスがどんな感じか見てみましょう。

このぐらいリストアップすればよいでしょうか。

さてRubyはどんな感じでしょう?思い起こせば2000年、私がRubyに乗り換えた頃は、Pragmatic Programmersシリーズの『Programming Ruby』が主なリファレンスでした。これをおすすめする手もないわけではありませんが、もう新しくありません(残念ながら最新版はRuby 1.9と2.0のみが対象です)し、さらに、同書は有料のPDFまたは紙の書籍として入手する方法しかありません。少なくとも、メインの言語リファレンスとして使うには最適とは言えません。

これ以上はくどくど申しません。そこで、「言語リファレンスのリンク、欲しいー」と尋ねられたときに私からすっとお渡しできるリファレンスを今ここにご紹介いたします

同リファレンスの理念とは、リファレンスは次の要件を満たすべきだというものです。

  • すべてを網羅していること: 言語/コアクラス/標準ライブラリのあらゆる側面を盛り込む
  • ギャップがないこと: シンプルな概念から高度な開発テクニックまで、上から順に迷いなく読み進められる
  • 実用的であること: 最新バージョンのRubyに対応する
  • アクセスしやすいこと: PC/スマホを問わず、検索/ナビゲーションを楽に行える

これらの目標を達成する際に、かのフランケンシュタイン博士の「ツギハギ工法」をかたじけなくも使わせていただきました。同リファレンスは、以下を「縫い合わせる」形で制作されました。

  • ソースリポジトリ内のRDocドキュメント
  • www.ruby-lang.orgサイトのページを少々
  • ギャップを埋めるためにコンテンツの一部を独自に追加(Rubyのコメントの書き方について公式のリファレンスが存在しないことをご存知ですか?)
  • 有用な情報のみを切り取って縫い合わせ、上から下まで楽々読み進められる端正な書籍としてまとめるための設定を山ほど行った

このようにして、Rubyのバージョンが新しくなるたびに同リファレンスも簡単に更新できます(コンテンツのほとんどはRuby自身やRubyのサイトから抽出したものであり、残りの追加部分はわずかです)。そんなわけで、同リファレンスはほぼほぼ公式に近いものになりました。

同リファレンスは最初の(ドラフト)リリースであり、皆さまからのご意見やお手伝いを心よりお待ち申し上げております

  • ウクライナの某氏が書いてくださった「カスタムコンテンツ」部分は、おそらく手を加える余地がかなりあるかもしれません。
  • コンテンツをインポートするスクリプトも改良の余地がありそうです(RDoc=>Markdownフォーマットが正しく変換されていない部分もあります)。
  • 同リファレンスの構成についてもレビューと安定化が必要です(今後Ruby言語のバージョンが新しくなった場合でもURLが変わらないのが望ましい)。
  • そしてたぶん、同リファレンスのアイデア全般について、もう少し議論や検証が必要でしょう。

以上です。

関連記事

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

Ruby 2.5の`yield_self`が想像以上に何だかスゴい件について(翻訳)

Ruby: これはなくてもいいかも?と思う10の機能(翻訳)

$
0
0

概要

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


idiosyncratic-ruby.comより

Ruby: これはなくてもいいかも?と思う10の機能(翻訳)

1993年にRubyが誕生して以来、長い月日が流れています。好ましいコーディングスタイルも大きく変わり、定まったベストプラクティスも生まれました(とはいうものの帯に短し襷に長しなのは世の常です)。それと同時に、Rubyのツールサポートはずいぶん向上したものの、言語はまだまだ複雑すぎます。おそらく、Rubyの機能を少しばかり間引く時が来たのでしょう。

機能の間引きは、既存コードが動かなくなる1というつらみを伴わないわけにはいきませんが、それでもやる価値はあります。

  • Ruby初心者が最初から「正しい書き方」を学べる: 「forループは避けるべし」といったベストプラクティスを後から学ばなくてよくなる
  • 言語がよりシンプルになる: すなわちツールのサポートも改善される!

コードに甚だしい非互換性をもたらさずに間引けそうな(そう願っています)Rubyの機能を、一部に独断を含みつつ以下にリストアップしました。

(多少の差はあれど)あまりがたつかずに削除できそうなRubyの10の機能

1. forキーワードとinキーワード

forループはめったに使われることがなく、Array#eachInteger#timesといった意味論上ほぼ同等の機能の方が遥かに多用されています。しかも、foreachを直接使うよりもわずかに遅いのです。『The Evils of the For Loopforループは邪悪)』もご覧ください。

2. ?で始まる1文字リテラル

?を使うと1文字だけの文字列を引用符なしで書けます。Ruby 1.8とRuby 1.9のどちらでも動くコードを書くときにとても有用でした。

"Idiosyncratic"[0] == ?I

Ruby 1.9以降では右辺と左辺はどちらも"I"が返り、Ruby 1.8では右辺と左辺はどちらも73になるので、結果はどちらのバージョンでも一貫してtrueになります。Ruby 1.8のサポートは2013年で終了したので、?構文をサポートするメリットはもはやありません。

3. @@によるクラス階層の変数

クラス変数は使うもんじゃありません。ほぼすべての初心者が混乱するので、言語から削除すべきです。代替手段をいくつかリストアップします。

公平のため申し添えますと、クラス変数の削除は本記事の他の項目よりも広範囲に渡ってコードが動かなくなるかもしれません。単に削除するには大きすぎるやつです。

4. thenキーワード

改行や;によって条件を他の文と区切る場合は次のようになります。

if true
  p 42
else
  p 43
end

ただし、thenキーワードを使うこともできます。

if true then
  p 42
else
  p 43
end

thenを使う意味は2とおり考えられます。1つはワンライナーです。

if true then p 42 else p 43 end

しかし三項演算子(?:)の方が見栄えは上です。

p true ? 42 : 43

もう1つはwhenを1行で書く場合です。

case
when true  then 42
when false then 43
end

しかしthenを使わなくとも、;で同じように1行で書けます。

case
when true;  42
when false; 43
end

つまるところ、thenは余分です。

5. TRUE/FALSE/NIL定数

truefalsenilはいずれもキーワードですが、これらに対応する定義済みの定数があり、しかもその気になればTRUE, FALSE, NIL = nil, true, falseのような再定義もできてしまいます。これらの定数を今後も使う理由がありません。

6. 正規表現マッチングによる暗黙のローカル変数生成

最後にマッチした正規表現の結果にアクセスするうえで、わざわざ=~でローカル変数を作成する必要はありませんし、=~の右辺と左辺を入れ替えることもできません。やはり明示的に$~[:group_name]でアクセスする方がずっと簡潔です。

7. and/or/notキーワード

優先順位の低いこれらの論理演算子は、たまに有用なこともあります。たとえば、代入される値がnilの場合に例外を発生させたい場合に次のようにorを使えます。

a = dangerous_operation or raise "dangerous operation failed"

しかし、これらのキーワードをRubyに今後も残す理由として十分でしょうか?既に多くのRuby初学者がこれらのキーワードで混乱しています。

8. コマンドラインの謎オプション: -s-x

皆さんが最後に$ ruby -s$ ruby -xを実行したのはいつだか覚えていますか?

9. シンボル

意味論上、シンボルとfrozenの文字列は極めて近い間柄です。シンボルは文字列のショートハンドにすべきです

10. フリップフロップ

本ブログではRubyの知られざる機能をたくさん紹介してきましたが、Rubyのフリップフロップについての記事はさすがにありません。

訳注: 上のリンクをクリックするとわかるように、英語圏では一般にflip-flopというと真っ先にビーチサンダルを連想します。

訳注

10.のフリップフロップは今後なくなりそうな塩梅です。

参考: Rubyのフリップフロップ - monamonamonad.github.io

関連記事

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

[Ruby] Kernelの特殊変数をできるだけ$記号なしで書いてみる


  1. もしかするとですが、マジックコメントを用いたstrictモードやquarksモードを追加できるのかも知れません。余計ややこしくなるでしょうが。 

Rails tips: Railsアプリにシンタックスハイライト機能を追加する(翻訳)

$
0
0

概要

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

Rails tips: Railsアプリにシンタックスハイライト機能を追加する(翻訳)

コードのシンタックスハイライト機能は、開発者向けブログやWebサイトでコードスニペットを表示するのになくてはならないものです。JekyllやWordPressなどのブログエンジンを使っているなら簡単にシンタックスハイライト機能を追加できますが、Railsアプリにも簡単に追加する方法があります。

今回のねらい

美しいシンタックスハイライト機能を追加すると同時に、コードをデータベースに安全に保管して簡単に編集できるようにもしてみたいと思います。必要な要件をすべてリストアップしてみました。

  • コードのハイライト機能
  • ハイライトのテーマを簡単に変更できること
  • アプリに送信したコードを安全に保管できること
  • 簡単に編集できること
  • さまざまなプログラミング言語のハイライトをサポートすること

コードを安全にデータベースに保存する最も楽な方法は、markdown構文を用いることです。こうした構文であれば、ロジックの編集や改修も楽になります。

markdown

markdown構文をサポートするには、redcarpet gemのインストールが必要になります。このgemの利用法はいたってシンプルです。次の例で使い方をご覧いただけます。

Redcarpet::Markdown.new(Redcarpet::Render::HTML).render("This is *bongos*, indeed.")

上のコードから以下の出力を得られます。

<p>This is <em>bongos</em>, indeed.</p>

Gemfileには以下を追加する必要があります。

gem 'redcarpet'

続いてbundle installを実行すれば、markdown構文を用いて記事を書き、以下のコードでビューに表示できるようになります。

<%= Redcarpet::Markdown.new(Redcarpet::Render::HTML, fenced_code_blocks: true).render(@article.content).html_safe %>

シンタックスハイライト

markdown構文のサポートが片付いたので、シンタックスハイライトに集中できるようになりました。ここではrouge gemを用いることにします。このgemは、markdown構文のサポートに用いているredcarpetとの相性も完璧です。それではGemfileに以下を追加してbundle installを実行しましょう。

gem 'rouge'

お気づきかと思いますが、markdown構文のパースにはRedcarpet::Render::HTMLクラスを用いています。rougeとredcarpetを接続するには、独自のレンダリング用クラスを作成しなければなりません。/lib/blog_render.rbというファイルを作成し、そこに以下のコードを追加します。

require 'redcarpet'
require 'rouge'
require 'rouge/plugins/redcarpet'
# markdown構文向けの独自のレンダリングクラス
class BlogRender < Redcarpet::Render::HTML
  include Rouge::Plugins::Redcarpet
end

続いて、新しいレンダリング用クラスをRedcarpet::Markdownの新しいインスタンスに渡す必要があります。

<%= Redcarpet::Markdown.new(BlogRender, fenced_code_blocks: true).render(@article.content).html_safe %>

fenced_code_blocksオプションによって、GitHubにありそうなコードブロックを使えるようになります。後はCSSスタイルを更新すれば完了です。私のGistにあるCSSをコピペすれば、私のブログと同じテーマで表示されるようになります。

関連記事

Slackのシンタックスハイライト付き「スニペット機能」は使わないと損

Rails tips: DeviseとOmniAuth認証でLinkedIn機能にサインインする(翻訳)

$
0
0

概要

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

Rails tips: DeviseとOmniAuth認証でLinkedIn機能にサインインする(翻訳)

本記事ではRuby 2.5 + Rails 5.1.4を用います。執筆時点ではいずれも最新です。まずはGemfileにDevise gemを追加しましょう。

gem 'devise'

続いてbundle installを実行してPCにgemをインストールしなければなりません。しかしインストールはこれだけでは終わりません。以下のコマンドを実行して、Deviseのイニシャライザファイルと翻訳ファイルを生成しなければなりません。

rails generate devise:install

モデルの生成

これでモデルを生成できるようになりました。認証データを持つモデル名はUserとするのが普通なので、この名前にします。

rails generate devise user

上のコマンドでDeviseがモデルのクラスとマイグレーションとカラのテストを生成してくれます。Userモデルの内容は次のようになっています。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
end

アプリではLinkedInの認証のみを許可したいので、不要なコードを削除します。

class User < ApplicationRecord
  devise :trackable, :omniauthable
end

マイグレーションの内容は次のようになっているはずです。

# frozen_string_literal: true

class DeviseCreateRecruiters < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      t.string :email,              null: false, default: ""

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.inet     :current_sign_in_ip
      t.inet     :last_sign_in_ip

      # LinkedIn
      t.string :provider
      t.string :uid, unique: true

      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
  end
end

ここではメールとパスワードによる認証は使いたくありませんが、後で使えるようにアプリにはユーザーのメールを保存しておきたいと思います。データベースにテーブルを作成するために以下のマイグレーションを実行しなければなりません。

bundle exec rake db:migrate

不要なカラムが残っていることに気づいた場合は、マイグレーションをロールバックしてファイルを修正し、再度マイグレーションを実行できます。

bundle exec rake db:rollback STEP=1

OmniAuthストラテジーによるLinkedIn認証では、providerカラムとuidカラムを使います。

LinkedInの設定

認証にはomniauth-linkedin gemを使いますので、最初にインストールしておきましょう。Gemfileに以下の行を追加してbundle installを実行します。

gem 'omniauth-linkedin'

今度はアプリでconsumer_keyconsumer_secretキーが必要になります。アプリにこれらがない場合はこちらの記事を参考に作成しておいてください。

config/initializers/devise.rbにあるDevise gemイニシャライザを開いて、credenntialを追加します。

config.omniauth :linkedin, "consumer_key", "consumer_secret"

これで、Userモデルを更新して認証プロバイダとしてLinkedInを使うよう指定できるようになります。

class User < ApplicationRecord
  devise :trackable, :omniauthable, omniauth_providers: %i[linkedin]
end

ルーティング

config/routes.rbにdevise_for :usersエントリがあれば、Deviseによって自動的に以下の2つのパスが追加されます。

  • user_omniauth_authorize_path(provider)
  • user_omniauth_callback_path(provider)

これで、LinkedIn認証を開始するためのリンクを追加できます。

<%= link_to "Sign in with LinkedIn", user_linkedin_omniauth_authorize_path %>

このリンクをクリックするとLinkedIn認証ページにリダイレクトされます。今度は、コントローラでリクエストを受けて、指定のユーザーをアプリ側で認証する準備をしなければなりません。

認証コントローラ

認証アクションを扱うために、別のコントローラを作成することにします。このコントローラの名前はAuthorizationsControllerにしました。LinkedInリクエストを処理するために、linkedinメソッドとfailureメソッドの追加が必要です。1番目のメソッドはレスポンスが成功した場合を扱い、2番目は失敗の場合を扱います。

class AuthorizationsController < Devise::OmniauthCallbacksController
  def linkedin
  end

  def failure
    redirect_to root_path
  end
end

上のコードが機能するには、ルーティングファイルを更新して、LinkedIn認証に使うコントローラがどれであるかをDeviseに伝えなければなりません。

devise_for :users, controllers: { omniauth_callbacks: 'authorizations' }

それでは、ユーザーの作成と検索を担当するコードを実装しましょう。

def self.from_omniauth(auth)
  where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
    user.email = auth.info.email
  end
end

コントローラを更新して、認証が完全に動作するようにしましょう。

class AuthorizationsController < Devise::OmniauthCallbacksController
  def linkedin
    @user = User.from_omniauth(request.env["omniauth.auth"])

    sign_in_and_redirect @user, event: :authentication
  end

  def failure
    redirect_to root_path
  end
end

これでおしまいです。LinkedInネットワーク経由でユーザーを認証できる非常にシンプルな基本アプリを作りました。保存されるのはメールだけです。名前や画像といった他のデータを集めることもできます。可能性は無限なので、何をするかはあなた次第です。

ユーザーが認証されれば、コントローラやビューでcurrent_userを介してアクセスできるようになります。

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

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

関連記事

[Rails] Devise Wiki日本語もくじ1「ワークフローのカスタマイズ」(概要・用途付き)

Ruby: `super`キーワードの4つの側面(翻訳)

$
0
0

概要

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


同サイトより

日本語タイトルは内容に即しました

Ruby: superキーワードの4つの側面(翻訳)

私の発行するRuby 💌ニュースレター💌こちらです!ご自由にサブスクライブいただけます!🚀

本記事では以下のトピックについて扱います。

  • 暗黙の引数
  • super vs super()
  • ブロック付きsuper
  • ancestorsチェイン付きsuper

暗黙の引数

引数を使うメソッドが、子クラスのどれかにあるメソッドによってオーバーライドされている状態で、その子クラス内でsuperを引数なしで呼び出すと、子クラスのメソッドの引数が自動的に親クラスのメソッドに渡されます。

次の例をちょっとご覧ください。

class Parent
  def say(message)
    puts message
  end
end

class Child < Parent
  def say(message)
    super
  end
end

irb> Child.new.say('Hi!')
#=> Hi!

ChildクラスはParentクラスを継承し、このChildクラスはParent#sayメソッドをオーバーライドしています。

Child#sayメソッド内では、superに引数を渡さずに呼び出しています。

すなわち、Rubyは#sayメソッドの探索をChildクラスの「ancestorsチェイン」内で行い、見つかったメソッドにmessage引数を渡します。

メモ: Rubyのancestorsチェインのメカニズムについて知りたい方は私の別記事もどうぞ。

しかし、ここでParent#sayメソッドが引数をまったく取らないとしたらどうでしょうか。

super vs super()

先ほどのParent#sayメソッドを再定義して、message引数を取っ払ってみましょう。

class Parent
  def say
    puts "親です"
  end
end

class Child < Parent
  def say(message)
    super
  end
end

irb> Child.new.say('Hi!')
#=> ArgumentError (wrong number of arguments (given 1, expected 0))

Parent#sayメソッドが引数を受け取らないにもかかわらず、Child#saysuperを呼んだためにChild#sayメソッドのmessage引数が暗黙でParent#sayメソッドに渡されてしまいました。

この問題を回避するには、Child#sayメソッドに渡される引数を受け取らないようsuperに明示的に指示を出す必要があります。

そのためには、superキーワードに丸かっこを追加して、super()とします。

class Parent
  def say
    puts "親です"
  end
end

class Child < Parent
  def say(message)
    super()
  end
end

irb> Child.new.say('Hi!')
#=> 親です

お次は、Parent#sayメソッドにブロックを1つ渡してみましょう。

ブロック付きsuper

Parent#sayメソッドを再定義して、yieldキーワードを追加しましょう。

class Parent
  def say
    yield
  end
end

class Child < Parent
  def say
    super
  end
end

irb> Child.new.say { puts 'よかった!ママかパパだ' }
#=> よかった!ママかパパだ

Child.new.sayメソッド呼び出しに渡されたブロックは、superキーワードを経由して、Parent#sayメソッドに暗黙で渡されます。

お次はyieldキーワードでこのブロックをキャッチし、Parent#sayメソッド内で実行してみましょう。

メモ: yieldについて知りたい方は私の別記事もどうぞ。

ancestorsチェイン付きsuper

#sayメソッドが定義されているGrandParentクラスをParentクラスに継承しましょう。

class GrandParent
  def say(message)
    puts "おじいちゃんかおばあちゃん: #{message}"
  end
end

class Parent < GrandParent
end

class Child < Parent
  def say(message)
    super
  end
end

irb> Child.new.say('Hi!')
#=> おじいちゃんかおばあちゃん: Hi!

ここでのsuperキーワードは、#sayメソッドの探索をParentクラス内で試みます。

Parentクラスにはこのメソッドが定義されていないため、superは続いてParentクラスのスーパークラス内(つまりGrandParentクラス)でメソッド探索を試みます。

GrandParentクラスには#sayメソッドが定義されています。

これで、Child.new.sayメソッド呼び出しに渡された'Hi!'引数は、superキーワードを経由してGrandParent#sayメソッドに暗黙で渡されます。

いかがでしたか?


本記事をお読みいただきありがとうございました😊

本記事がお役に立ちましたら、ぜひMedium.comの元記事で存分に👏ボタンを押してください。

前回の私の記事『Method Arguments in Ruby: Part II – Mehdi Farsi – Medium』もどうぞ。

関連記事

Ruby: ループには一時変数ではなくEnumerableを使おう(翻訳)


インタビュー: 超高速リアルタイム検索APIサービス「Algolia」の作者が語る高速化の秘訣(翻訳)

$
0
0

概要

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

日本語タイトルは内容に即したものにしました。

インタビューにもあるように、AlgoliaのフロントにはRailsも使われています。


stackshare.ioより

インタビュー: 超高速リアルタイム検索APIサービス「Algolia」の作者が語る高速化の秘訣(翻訳)

原文編集メモ: Nicolas Dessaigne氏はAlgoliaの共同創立者にしてCEOです。Julien LemoineAlgoliaの共同創立者にしてCTOです。お二人は共にフランスのパリ出身で、現在はカリフォルニア州サンフランシスコ在住です

Algoliaは、オフライン検索の改善を図るアプリ向けのモバイルSDKとして出発しました。これをネット上の検索サービスとして使いたいという引き合いが複数の顧客から来るようになると、二人は直ちにこの製品は単なるモバイルアプリを凌駕する可能性に気が付きました。今や同社は、Pebble、WeFunder、CodeCombat、HackerNewsといったWebアプリのホストの検索機能を強化しています。私たちはお二人と座ってくつろぎつつ、同社の検索製品と、それを支えるテクノロジーについて学びました。

本インタビューは以下の2つで構成されています。

  • Algoliaの立ち上げと、他社技術との違いについて
  • Algoliaを支える技術

参考: Algoliaに寄せられた開発者の反応


StackShare(SS): まずはAlgolia創業当時のお話から。このアイデアをどこから得ましたか?

Julien(J): 元々はベイエリアでスタートアップ企業のコンサルティング業務から出発しました。スタートアップ企業のひとつがGoogle App Engineを採用していて、App Engineのインデックス機能を検索機能に用いた開発を進めていたんです。その当時の問題は、App Engineが提供するドキュメント検索機能のつくりが古風で、人の名前や曲のタイトルといったデータベース内の少数のレコードを「入力と同時に」リアルタイム検索するのに向いていなかったのです。私がこれまでに検索エンジンを3つ開発してきたのもあって、このニーズに合うきわめて小規模な検索用データ構造の開発を依頼されました。

Nicolas(N): Julienはかれこれ10年以上も情報取得や自然言語処理に携わってきています。ちなみに私は15年以上ですが。

SS: それほど長期に渡って検索に携わった末に、もっとよい検索手法が必要だとお考えになったんでしょうか?

N: Julienがそのコンサルティング業務で構築した検索エンジンは実に軽量で、これはもしかするとモバイルアプリにうってつけなんじゃないかと気づきました。この直感は、StackOverflowで見かけた「モバイルアプリに検索エンジンを直に埋め込む方法」の大半にまったく回答が付いていないことで裏付けられました。みんなLuceneを埋め込みたいと思いながら、それまで誰ひとり成功していませんでした。それを元にモバイルアプリに埋め込める検索エンジンSDKの開発にゴーサインを出しました。そんなわけで私たちの最初の製品は、実際にはオフライン検索エンジンだったのです。

J: 元々のアイデアは、開発者がこのSDKを自分たちのモバイルアプリで使えるようになればというものでした。特定の検索バックグラウンドを用いずに、数分で自分たちのアプリに統合できるようにしなければならなかったのです。

N: で、この製品をiOS/Android/Windows Phoneで使えるようにすべく、2012年に会社を立ち上げました。実際、技術的にはきわめて大きな成功を収めましたし、多くの開発者が愛用してくれました。しかし、当時はこの製品を売るためのマーケットがなかったのです。

SS: そのSDKはどんな企業に使われていましたか?

N: 基本的にはコンシューマ向けアプリで、オフラインデータを扱うアプリが主でした、旅行ガイドとか。たとえばパリに旅行するとして、実際に現地に到着したら地元の情報をいろいろ見たいのにローミング費用が惜しいとしましょう。ガイド情報をモバイルデバイスにダウンロードしておけば、パリに着いてから自由にオフライン検索できます。これは実に大きな価値をもたらしてくれました。

当時の問題は、開発者(特にAndroid)がSDKにお金を払ってくれる状態ではなかったことです。オフラインデータにがっつり依存するアプリはほとんどありませんし、開発者はSQLiteで超基本的な検索機能を提供することを好むので。

N: それと同時に、多くの人からこの製品で得られるエクスペリエンスに対する好意的な評価がたくさん押し寄せたのですが、どの方もこのエンジンのオンライン版を欲しがりました。オンライン版で自分たちの全データを同じエクスペリエンスで提供したいということだったのです。

私たちが構築した検索エンジンはモバイルを念頭に置いたもので、スマホのキーボードでタイポしまくっても許容してくれる即時検索でした。

しかしこの検索エクスペリエンスの構築は、モバイルアプリ開発に限らず、多くの開発者にとってどうやらあまりに難しかったようです。開発者たちが実際に価値を見出していたのは、REST APIを自分たちのオンラインデータでさくっと使えるようにすることでした。

基本的にはそのタイミングで、私たちは同製品のSaaS版を構築することを決定しました。2013年前半のことです。3月には開発を開始し、多くの好意的な評価を得ました。そしてこの製品が昔から実によくあるひとつの問題を解決していることに気づいたのです。そこからは、製品の差別化に専念し、ドキュメントではなくデータベースをターゲットに定めました。

SS: モバイルアプリが出発点だったからこそ、ドキュメントではなくデータベースに注力したということですね?

N: おっしゃるとおりです。モバイルアプリを手がけたのは、私たちが求めていたデータの性質から言って当然でした。モバイルデバイスで検索するものといえば、連絡先だの位置情報だのといった類です。モバイルデバイスで使いこなしようもない巨大なドキュメントを検索したいなんて思わないでしょう。エンジンはその用途に最適化していて、SaaS版に移行したときもそれと同じコアテクノロジーを用いたわけです。データベース内での全文検索実行は実に良好でした。すなわち私たちのコアバリューは、データベースレコード向けに設計・最適化されたエンジンの価値にあります。これまで構築してきたものは例外なく、常にデータベースを目標に据えることを念頭に置いています。

SS: エクスペリエンスという観点において、御社の製品が従来のものとどう異なるかをご説明いただいてもよろしいでしょうか?より高速なデータベース検索結果のあたりを中心に、相違点とか、お二人がどうお考えになっているかについて。

N: はい、その違いについて語る人は多くはありませんので、違いをきっちり説明することが重要ですね。多くの検索エンジンは、ドキュメントを対象として設計されているのです。

どういうことかというと、たとえば検索結果をランキングするのであればドキュメントを対象に設計されたランキング規則を使うことになるわけです。このランキングでは、各ドキュメント内のクエリに出現する用語数をカウントし、TF-IDFに基いて統計処理を行います。入力したクエリの用語をなるべく多く含むドキュメントを上位のランクにしたいというわけです。つまりこれは、ドキュメント単位のスコアを生成する統計公式の一種です。このスコアは少々謎めいていて、公式を微調整することで多少ましにはなるものの、最終的に得られるスコアは理解が非常に困難です。ドキュメント向け検索エンジンとして使い物になるよう公式を調整するのは実に困難です。さらに、単語が完全なら問題なく動作するのですが、語の入力途中など、語の冒頭部分ではうまく動きません。

このあたりを適切に修正するには多少のハックが必要です。

J: これは、莫大なテキストを抱えているWikipediaのようなWebページなど、巨大なテキストを検索対象として設計されていたためでした。そのような場合ならこうした統計手法を用いるのは自然です。私たちのようにデータベースを対象とする場合は、まったく異なります。たった1つのシンプルな製品名を検索するだけなら、そうした制約はありません。

N: 対象データが違うのですから、結果のランキング手法も根本的に異なるものを用いる必要がありました。どちらかというと、人間が考えるときの方法に近いやりかたです。たとえば、eコマースWebサイトでiPhoneを1件検索するとしましょう。欲しい結果は、iPhoneという単語を含むタイトルであって、説明文の末尾にあるiPhoneという単語ではないでしょう。つまり、出現回数よりも、その語が結果内のどの位置にあるのかという情報の方がずっとずっと重要なのです。

さらに、iPhone 3Gの検索結果なんか欲しくもないでしょう。欲しいとすれば一番人気の最新型iPhone 5Sです。データベースの対象をこうした点に絞り込むメリットは、この種の情報に配慮できるところです。これは、ドキュメント向け検索エンジンではきわめて困難です。

さらに、クエリで明らかにスペルミスしたときなんかには他の要素にも気を利かせて欲しいものです。eコマースの会社であれば、そんなときに売上一位の製品をトップに表示して欲しいでしょうし、こうしたさまざまな要素の一切合財を組み合わせて欲しいでしょう。こうした組み合わせはあまりに複雑なので、ドキュメント向け検索エンジンでは手に余ります。そこで私たちは、こうした多くの判断基準のすべてを極めて明確な手法によって面倒を見る、新種のアプローチを開発しました。検索で得られる結果セットがそのようになった理由や、そのようにランキングされた理由は、誰でもひと目で理解できます。

SS: その大半はカスタマイズされたものですよね?大半は御社のアプリに特化した形で。具体的にはどんなふうにやっていますか?

J: 他のエンジンと比べて、カスタマイズは本当に楽です。他のソリューションならカスタマイズ項目が数千件にのぼりますが、私たちのはほんのちょっとです。カスタマイズが楽勝でできますし、お客様のデータを元に優秀な検索エクスペリエンスを構築する方法もきちんと説明できます。

N: デフォルトのランキングは、ユースケースの90%で良好な結果を示しています。良好な検索結果を得るには、基本的には次の2つの設定を行います。

  1. インデックス化したいさまざまな属性の重要度を指定する。たとえば、製品名の方が説明文やコメントよりも重要度が高い、とか。
  2. 対象の人気指標(popularity)を定義する。たとえば売上個数とか、「いいね!」の数とか。

この2つを設定すれば、既にこのうえもなく良好な妥当性を誇る検索エンジンを基本的に手にすることができます。

J: たとえば、シリーズもののTV番組を検索するエンジンのデモページをユースケースのひとつとして公開しています。1番目の設定では俳優の名前よりTV番組名の方が重要であること、2番目の設定では人気指標としてフォロワー数を定義してあります。この2つを設定しておけば、後はキーを数回打つだけで的確な結果が飛び出してきます。

N: これだけではありません。たとえば入力に応じて一致する分野名を表示したいのであれば、APIからそれ用の高度な設定を得られます。管理インターフェイスもあるので、あらゆる設定はオンラインで完了します。

SS: 凄いですね!次はそれを支える技術についてお願いします。

J: 基本的には大きく2つの部分に分けられます。顧客が使うAPIと、管理インターフェイスの使えるWebサイトです。この2つのアーキテクチャは大きく異なっています。

このサービスのWebサイトや管理インターフェイスを最初に見てみると、UIがRailsとBootstrapで構築されているのがわかります。ホスティング先として、AWSのRDS(MySQL)EC2を複数のavailability zoneに配置し、ELBでロードバランシングしています。私たちのAWSインスタンスはUS-Eastに配置されており、さらに全アセットをCloudFrontに保存して、世界中どこからでも素晴らしいエクスペリエンスを提供できるようにしています。

J: ダッシュボードでのデータのナビゲーションや利用については、JavaScriptクライアント経由で弊社独自のREST APIを利用しています。ここではAPIから直接取得した分析情報や利用状況の情報もリアルタイムで表示しています。

APIサーバーの方ですが、こちらではリアルタイムデータの一部をRedisに保存し、最新の相互API呼び出しのログを取っています。APIのカウントもリアルタイムで保存しているので、何か操作を行うたびにRedisのカウンタが増加してリアルタイム情報を提供できるしくみになっています。

SS: そのあたりのデータ保存方法についてもう少し詳しくお願いします。

J: ユーザーがデータを私たちのAPIに送信すると、私たちの側では3つの異なるホスト(いずれもAPIサーバー)にデータをレプリケーションします。インデックス作成もそれぞれのホストで独自に行われます。同期を確認するために、RAFTと呼ばれる一種のコンセンサスアルゴリズムを用いています。これはPaxosアルゴリズムの一種でもあります(訳注: いずれもブロックチェーン方面で使われることの多いアルゴリズムです)。

これと同種のアルゴリズムが、Amazon S3Google App Engineでも用いられています。3つの異なるクラスタが完全に同期していることを確認する必要があります。その中のひとつがときおりダウンするとか再起動するなどの事態が発生したとしても、同期が完全であることを確認できなければなりません。

APIに書き込み操作を行うと、3つの異なるサーバーに送信し、この書き込みのトランザクションのIDが影響を受けます。このIDは、コンセンサス(アルゴリズム)によって決定される3つのホストからなるクラスタ全体に渡ってカウントアップされる整数です。このAPIを呼び出すと、ジョブが少なくとも2つのホストに書き込まれた時点でタスクIDが1つ返されます。これにより、ジョブがインデックス化されるタイミングをこのタスクIDでチェックできるようになります。

訳注: 原文では画像リンク切れのため、画像検索で復元しました。以降の画像も同様です。

N: データがレプリケーション完了したことはもちろん、それが完全に同じ順序になっていることも確認しなければなりません。つまり、3つのホストはいついかなる場合であっても同期し、ステートも同じでなければならないのです。

J: これは高可用性のために行っています。ホストの1つがダウンしようと、プロバイダのavailability zoneがダウンしようと、サービスが動き続ける必要があります。つまりS3とまったく同様、常にどれかが動き続けられるようにするために同じ情報を3回レプリケーションしているのです。

この冗長性はパフォーマンス上のためでもあります。というのも、3つの異なるホスト全体でクエリを共有しているからです。たとえば1秒間に1000件のクエリを受信すると、各ホストに33%ずつクエリを送信する形になります。

私たちの検索エンジンはC++モジュールになっており、Nginxに直接埋め込まれています。つまりクエリがNginxに到達すると、そこで検索エンジンが走って結果をクライアントに送り返すのです。

処理速度を落とすような層は存在しません。システムはこのクエリのために徹底的に最適化されています。

SS: データを送信してから戻ってくるまでの流れを説明していただけますか?

J: 私たちはデータセンターごとにクラスタを配置しており、各クラスタは3つの異なるホストで構成されています。私たちのクラスタの大半はマルチテナント型で、複数クライアントをホストします。

使っているのは極めてハイエンドなコンピュータです。基本的に1サーバーあたり256GBのRAM、1TBのRAID-0 SSD、16コア(3.5GHz以上)を用いています。私たちはEC2に頼ることはせず、複数のプロバイダで専用サーバーを運用しています。

クラスタに関連して、通信帯域幅もがっつり確保しています。1クラスタあたりの専用帯域幅は4.5Gbpsとなっており、これもサービスの品質を高く保ちたいがためです。究極のサービスを提供するため、ハードウェアや帯域幅については一切妥協しませんでした。

各インデックスは、弊社独自のフォーマットを持つ1つのバイナリファイルです。情報は特定の順序を保ってここに保存されており、インデックスへのクエリは極めて高速です。このアルゴリズムは私たちがモバイルで培ったものです。

弊社のNginx向けC++モジュールは、このインデックスファイルをメモリマップモードで直接オープンし、Nginxの異なるプロセス間でメモリを共有して、クエリをメモリマップデータ構造に適用できるようにしています。

ロードバランシングを異なる3つのサーバー間で行うため、APIクライアントコード(JavaScriptなど)内で直接ロードバランシングを行っています。埋め込まれているこのJavaScriptコードはサーバーをランダムにひとつ選択し、必要に応じて別のサーバーにフェイルオーバーします。

N: ロケット科学のようなものではありませんが、ハードウェアによるロードバランサーのSPOF(単一障害点)を回避するうまい手法です。弊社のAPIクライアントはMITライセンスでGitHub上に公開されていますので、github.com/algoliaで誰でもご覧いただけますよ。

APIのユーザーは、自分たちのバックエンドのプログラミング言語に合ったAPIクライアントを用いることができます。おすすめの利用法は、このAPIクライアントをインデックス作成やデータのプッシュに用いることです。ただしクエリの実行にはWebアプリケーション向けのJavaScriptクライアント(またはiOSではObjective-C、Windows PhoneならC#)を用いることを強くおすすめいたします。これは、エンドユーザーのブラウザから弊社のサーバーにクエリを直接送信することで途中のホップでの速度低下を回避するためです。

J: これは、極めて良好なパフォーマンスを得るための方法のひとつです。私たちはNginx層でありとあらゆる最適化を施しました。しかし途中に別の層(APIユーザー独自のサーバーなど)が挟まってしまうと、このエクスペリエンスは得られなくなります。そうしたわけで、私たちとしてはぜひともJavaScriptクライアントをお使いいただきたいのです。

SS: ジョブをインデックス化するキューみたいなものはありますか?

J: 正確にはキューではなく、コンセンサスですね。Nginxからのインデックス化ジョブを受け取るビルダープロセスがサーバーごとにあります。インデックス化ジョブを1つ追加すると、ジョブを受け取ったNginxが3つの異なるビルダープロセスに送信してコンセンサスを起動します。言ってみれば、一意のIDを持つ3つの異なるビルダー間で「投票」を行っているわけです。そしてコンセンサスはある種の「選挙」アルゴリズムを元にしていて、どの時点においてもマスターとなるビルダーが1つ、スレーブとなるビルダーが2つ必要です。マスターがダウンすると、生き残りの2つのスレーブ同士が自動的に投票を行ってマスターを選出します。マスターは一意のIDを割り当て、インデックス化キュー内の一時ジョブをすべてのビルダーに移動します。3つのホストのうち2つがこの操作を完了すると、ユーザーに応答を通知します。こうしたID割り当てに使えるオープンソースソフトウェアがあり、その中でもよく知られているのがApache Zookeeperです。Zookeeperの薄い層をクラスタの分散情報に用いることができます。つまり、たとえばクラスタ内でどれがマスターであるかという情報を共有できるわけです。

Zookeeperの問題は、トポロジーが変更されたときの検出に長時間かかる可能性がある点です。たとえばあるホストがダウンすると、検出までに何10秒もかかるかもしれません。1秒間に数千件ものインデックス化ジョブをクラスタで処理するのですから、これでは時間がかかりすぎですし、それらを処理するためにIDを指定するマスターが必要です。そこで弊社ではRAFTをベースに独自の選挙アルゴリズムを構築しました。このアルゴリズムによって、3つの異なるホスト全体で一貫している1つのIDアロケータを持つことができました。このリーダーがダウンすれば、数ms以内に別のリーダーが選出されます。

デプロイも実に簡単で、ビルドプロセスをkillしてシグナルを送ることでNginxをホットリロードします。ビルドプロセスをkillすることで選挙アルゴリズムを常に自動テストしているので、テストのためにホストの停止を待つことはしません。このアプローチはproductionのプロセスをランダムに停止するという点でNetflixのChaos Monkeyに似ています。

SS: なるほど。データをインデックス化する方法を万全にしてからクエリをかけるということですね。

J: 弊社のアーキテクチャにはさらに3番目の要素があります。productionでは重要ではありませんが、テストで重要な要素です。

SS: お、ちょうど御社のビルド/デプロイ/テストプロセスについてお話を伺おうと思っていたところでした。

J: 弊社が提供するAPIステータスページには、サービスの測定結果がリアルタイムで表示されます。このためにGoogle App Engine上で動作するプローブを開発しました。これらのプローブはすべてのクラスタに対して常にインデックス化やクエリを実行し続け、あらゆる問題を自動で検出します。

弊社では特定ユーザー向けに専用のステータスページも提供しています。たとえばHacker News様専用ページです。もちろん自社インフラを抱える大企業のお客様も対照としています。お客様が専用サーバーをお求めの場合は、こうした専用ステータスページも合わせてご提供しております。

それからデプロイですね。GitHubには3つのコードをプッシュしています。主にAPIクライアント向けに、単体テストやリグレッションテスト(non-regression test)のセットを備えたCIも用意しています。どのテストもTravis CIを用いてコミット時に適用されます。コード品質についてはCode Climate、カバレッジについてはCoverallsをそれぞれ用いています。

production向けには、デプロイ前に大規模な単体テストやリグレッションテストを自動適用するデプロイスクリプトも用意しています。また、バージョンが新しくなって問題が発生した場合に自動でロールバックするスクリプトもあります。

SS: ということはstaging環境もありますか?

J: はい。弊社にはテスト専用のクラスタが1つあります。productionアプリは多岐にわたるため、重要なproductionクラスタの他に、staging用のクラスタもあります。

N: アップデートの重要度によっては、最も重要なクラスタでは動かしません。

J: 現時点では、弊社の検索でダウンタイムが生じたことはありません。2013年のサービス開始以来、インデックス化で8分間の問題が1度生じたきりで、ほぼ40億件に近いAPI呼び出しをこなしてきました。

N: このダウンタイムも本当にインデックス化の部分だけで検索は影響を受けませんでしたし、しかも生じたのは1つのクラスタだけでしたので、すべてのお客様に影響が生じたわけではありません。その8分間に影響を受けた可能性のあるお客様は15〜20社ほどでしたでしょうか。これらのお客様に即座に連絡を取ったところ、20社のうち問題の発生に気づいたのはわずか2社で、しかも弊社の迅速な対応にむしろお喜びいただけました。

J: 弊社の透明性を信じています。弊社は、発生した問題と解決方法についてきちんと説明したいと考えました。

N: そこで事後報告としてブログ記事を1本書きました。

SS: ログ機能も提供しているんですか?

J: 弊社ではユーザーの直近のログ、直近のエラーログ、そしてカウンタをRedisに保存しています。実際のAPIサーバーでは、どのユーザーについても直近のアクティビティだけを保存しています。その後弊社は、ユーザーのダッシュボードに統計情報や分析結果を表示するためにすべてのログを処理する専用クラスタを設置しました。

N: 分析は2種類あります。まず、ダッシュボードにその日のレスポンスタイムを表示するための生分析があり、そこではレスポンスタイムの他に、レスポンスタイムの99%パーセンタイルも表示されます。

現在、クエリのコンテンツを対象とするより高レベルの分析にも取り組んでいます。最も頻度の高いクエリ、結果抜きで最も頻度の高いクエリなどを知ることができるというものです。現在作業中ですが、いずれお目にかけられるはずです。

SS: 監視についてですが、皆さんはどういったサーバー監視をお使いですか?

J: 弊社のサーバー監視にはServer Densityを用いています。フロントエンド側にはCloudWatchも使っています。

SS: 他にお使いのサービスなどありましたらどうぞ。

N: 各クラスタにはホストが3つずつ配置されていますが、ヨーロッパ/米国/アジアなどさまざまな地域のデータセンターにまたがる検索も行います。この検索はこうしたさまざまなデータセンターに分散可能です。したがって、エンドユーザーからのクエリは最寄りの地域にあるクラスタに投げられます。これにはAWSのRoute 53を用いています。

J: はい、これはエンドユーザーの地理上の位置に強く依存していて、Route 53が最寄りのサーバーを検出してユーザーをそちらに導くのです。弊社Webサイトにあるデモではまさにこれを行っているので、常に良好なレスポンスタイムを体験できます。

J: ダッシュボード内では、Intercomを用いてユーザーが学べるようにしています。メトリクスの成長のトラッキングや、隘路内の改善可能な手順の特定にはGoogle AnalyticsKISSmetricsを使っています。

社内ではHipChatを使っていて、例外発生時や新規サインアップ時や支払い時などのデータがすべてダンプされる専用のチャットルームがあります。とりたてて珍しいことではありませんが、HipChatはユーザーサポートにも活用しています。弊社Webサイトを開けば、私たちとチャットできるようになっています。チャットルームでは、大勢のユーザーが同じチャットルームに同時参加できます。私たちはここでお客様とチャットし、時にはコードをハイライト付きでそこに投げることもあります。開発者のサポートにはなかなかよいと思います。

プローブが問題を検出したときのSMS送信にはTwilio、支払いにはStripeをそれぞれ使っています。この他にも便利なサービスをいろいろ使っています(Algoliaで使われている全サービスのリストについてはこちらを参照)。

SS: 他にもまだ説明いただいていない、重要かつ非常に有用なオープンソースツールがもしありましたらどうぞ。

J: Google SparseHashライブラリはかなり使っていますね。このライブラリは極めて低レベルかつ高パフォーマンスのハッシュテーブルで、低レベルのC++コードでは全面的に使っています。効率の高さは凄いですよ。

オープンソースのライブラリは他にも山ほど使っています。全部リストアップしたら相当な長さになるでしょうね😀

その他に、開発者のコミュニティへの支援もささやかながら行っています。つい最近はDepstackで支援を行いました。これは人気の高いライブラリを検索したり投票したりできるサイトです(訳注: depstack.ioはサイトがありませんでした)。このときは、さまざまな言語(Ruby、Python、Goなど)について検索できるさまざまなライブラリをすべてインデックス化する作業を裏方として行いました。皆さんもGitHubアカウントでサインアップすればライブラリに投票できますよ。また、どのGitHubプロジェクトがどのライブラリを用いているかを検出してそれらへの投票をカウントすることも行っています。

このように、誰でも利用頻度の高いベストなライブラリを見つけられるよう作業しています。

SS: クールですね!ぐっと親しみが湧いてきました😀

J: Depstackなどでコミュニティを率先してサポートする作業は今後も続けます。そうそう、弊社のAlgoliaはコミュニティのプロジェクトに無償で提供しているんですよ。


Algoliaユーザーによる全レビューはこちらでご覧いただけます

Iecu 2bhenz5uaaaaasuvork5cyii

関連記事

Rails: RedisキャッシュとRackミドルウェアでパフォーマンスを改善(翻訳)

RabbitMQはSidekiqと同等以上だと思う: 前編(翻訳)

RubyのMetaエスケープやControlエスケープを理解する(翻訳)

$
0
0

概要

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


idiosyncratic-ruby.comより

RubyのMetaエスケープやControlエスケープを理解する(翻訳)

2重引用符で囲まれた文字列では、お馴染みの式展開(string interpolation)#{}以外にも、バックスラッシュ\で始まるさまざまなエスケープシーケンスが利用できます。エスケープシーケンスを使って、生のバイトやコードポイント値を埋め込めます。さらに、よく使われる書式文字や制御文字のショートカットも使えます。

1. バイトシーケンス

生バイトの埋め込みには、\x00(16進数)または\000(8進数)の2とおりの方法が使えます。

"\x20" # => " "       # スペース文字
"\xab" # => "\xAB"    # 値171のバイト
"\033" # => "\e"      # エスケープ
"\0"   # => "\u0000"  # nullバイト

1-1. Metaエスケープ

いわゆるMetaエスケープ構文("\M-x"、xはバイト値)が使えます。バイト値が128(\x80)より小さい場合は128を追加し、それ以外の場合は同じ値を返します。言い方を変えると、その値の8ビット目(訳注: 1バイトの最上位ビット)をオンにした値を返します。xの値は再度エスケープされることがあります。

"A".unpack("C")    # => [65]
"\M-A".unpack("C") # => [193]
"\M-\x01"          # => "\x81"
"\M-\x81"          # => "\x81"

訳注: Metaキーを独立して備えているキーボードはあまりありません。スーパーキー(Macだとコマンドキー、WindowsだとWinキー)がMetaキーとして機能することがありますが、アプリによって異なります。上述の「8ビット目を立てる」は、Metaキーの本来の動作とされています。

1-2. Controlエスケープ

Controlエスケープ構文("\C-x"または"\cx"、xはバイト値)は、もうひとつのレガシー構文です。値の下位5ビット部分を返すので、値は0〜31となります。値xは再度エスケープされたり、上述のMetaエスケープと組み合わせられることがあります。

"\C-\x01" # => "\u0001"
"\C-!"    # => "\u0001"
"\C-A"    # => "\u0001"
"\M-\C-A" # => "\x81"
"\C-\M-A" # => "\x81"

2. Unicodeコードポイント

Unicode文字はコードポイント値で表現されます。コードポイント値の数字がわかれば、二重引用符で囲まれた文字列で\uを用いて埋め込めます。値の16進数は正確に4桁でなければなりませんが、大文字小文字の違いは無視されます。

"\u0020" # => " " # スペース文字
"\u00A0" # => " " # nbsp(ノーブレークスペース)
"\u203d" # => "‽" # interrobang(?と!が重なった特殊文字)

\u構文では、より柔軟な{}記法もサポートしています。

"\u{9}"    # => "\t" # tab文字
"\u{2602}" # => "☂" # 傘の絵文字

\u{}構文は、16進数が4桁に収まりきれないコードポイントを表示したい場合に必要です(U+1F6A1 AERIAL TRAMWAYなど)。

"\u{1F6A1}"   # "🚡"

複数の文字を一度に指定することもできます。

"\u{49 64 69 6f 73 79 6e 63 72 e4 74 69 63 20 52 75 62 79}"
# => "Idiosyncrätic Ruby"

3. 書式/制御文字

次のエスケープシーケンスは、よく使われる制御文字(control character)や書式文字(formatting character)のバイト値です。

エスケープ表現 バイト値 説明
\a 7 ターミナルでベルを鳴らす(ベル文字)
\b 8 バックスペース文字(BS)
\t 9 tab文字(水平タブ)
\n 10 改行文字(linefeed、LF、newfeed)
\v 11 垂直tab文字(vtab)
\f 12 フォームフィード文字(FF、ページ送り)
\r 13 キャリッジリターン文字(CR、行頭復帰)
\e 27 エスケープシーケンス開始文字
\s 32 スペース文字

関連記事

Unicodeで絶対知っておくべきセキュリティ5つの注意(翻訳)

Rubyの内部文字コードはUTF-8ではない…だと…?!

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

Ruby 2.5のカスタマイズ可能なWarningモジュール(翻訳)

$
0
0

概要

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


idiosyncratic-ruby.comより

Ruby 2.5のカスタマイズ可能なWarningモジュール(翻訳)

Ruby 2.51から、Warningモジュールを用いてKernel#warnの振る舞いをカスタマイズできるようになりました。やり方をご説明します。

def Warning.warn(w)
  # superは元の振る舞い(#stderrへの出力)を呼び出す
  super "\e[31;1mRUBY WARNING: \e[22m#{w.sub(/warning: /, '')}\e[0m"
end

# # #
# 例

warn "test"
# => RUBY WARNING: test

{ a: 1, a: 2 }
# => RUBY WARNING: (irb):4: key :a is duplicated and overwritten on line 4

$VERBOSE = true # level 2 warningsを表示
def a() end
def a() end
# => RUBY WARNING: (irb):8: method redefined; discarding old a
# => RUBY WARNING: (irb):6: previous definition of a was here

Jeremy Evans氏作の ruby-warning gemを使うと、さらにいくつかのwarning機能を解き放てます。

require "warning"

Warning.ignore /duplicated and overwritten/
{ a: 1, a: 2 }
# => なんも出ない

$VERBOSE = true
Warning.ignore :method_redefined
def a() end
def a() end
# => なんも出ない

参考

関連記事

Ruby: ありそうでなかったRubyリファレンスの決定版を作った(翻訳)

Ruby: シングルトンオブジェクトをデフォルト引数として使う(翻訳)


  1. Warningモジュールそのものは既にRuby 2.4からありますが、Kernel#warnでは利用していませんでした。 

週刊Railsウォッチ(20180622)Railsの需要未だ巨大、Unicode 11.0リリース、WebDriverがW3Cで勧告、Flutter.io、2封筒問題ほか

$
0
0

こんにちは、hachi8833です。私が気まぐれに応援するサッカーチームは必ず負けるので、どこも応援しないようがんばります。

各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
まだ検討段階ですが、週刊Railsウォッチの刊行日を月曜日に移動するかもしれません

Rails: 今週の改修(Rails公式ニュースより)

特記ない限りRails 6.0向けです。

既存association削除時の探索をハッシュに変えて高速化

# activerecord/lib/active_record/associations/collection_association.rb#L391
         def remove_records(existing_records, records, method)
           records.each { |record| callback(:before_remove, record) }

           delete_records(existing_records, method) if existing_records.any?
-          records.each { |record| target.delete(record) }
+          hashed_records = records.group_by { |record| record }
+          target.select! { |record| !hashed_records[record] }

           records.each { |record| callback(:after_remove, record) }
         end

GDPR対応でユーザーや「いいね👍」をごっそり削除することになった人、いるよね? dependent: destroyオプションのアルゴリズムが更新されて(計算量が)二次から一次に削減されました。これ大好き😋
公式ニュースより大意


つっつきボイス:eachで全回しでdeleteしてたのを、group_byしてからhashed_recordsで削除したと」「quadratic(二次)からlinear(一次)って、ここでは計算量のことでいいんですよね?」「ですね🧐O(n^2)からO(n)に削減されたやつ」「お、例のsgrifさんが『self.target -= recordsだけでいけるんじゃ?』ってコメントしてる🤔」「おー、確かにこの書き方は演算子がオーバーライドされてればできるな: 面白い😋」

参考: ランダウの記号 - Wikipedia
参考: 一次方程式 - Wikipedia — linear equation
参考: 二次方程式 - Wikipedia — quadratic equation

storeアクセサで従来のprefixの他にsuffixも使えるようになった

# 同PRより
store :settings, accessors: [ :two_factor_auth ], coder: JSON, _prefix: true
# => accessor will be model.settings_two_factor_auth
store_accessor :settings, :secret_question, _prefix: 'config'
# => accessor will be model.config_secret_question
store :settings, accessors: [ :login_retry ], _suffix: 'setting'
# => accessor will be model.login_retry_setting

つっつきボイス: 「おー、storeにsuffixもねー: 既存のデータベースでsuffixを使うやつがあるんだろうし、prefixが使えるならsuffixも欲しいというのはまあワカル」「指定したsuffixが自動で追加されるんですね」「自分はあんまり使わないかなー?😆」

Active Modelのデフォルトのエラーメッセージ表示方法を改良

#{attribute} #{message}#{message}, full_messageに変更し、以下のどれでも使えるようにしつつ高速化も図ったようです。

en:
  errors:
    format: '%{message}'
en:
  activemodel:
    errors:
      models:
        person:
          format: '%{message}'
en:
  activemodel:
    errors:
      models:
        person:
          attributes:
            name:
              format: '%{message}'

つっつきボイス:full_messageがオーバーライドできるようになって、しかもi18nなのか!そういえば今まではデフォルトのエラーメッセージが生書きされてたような気がする」「この改修はどの辺がうれしいんでしょうか?」「今まではデフォルトのエラーメッセージが完全に英語の語順になっちゃってたんで、日本語環境だとeachで回してゴニョゴニョしないといけないとか、いろいろ使いにくかったんですよ」「あー!なるほど😲ローカライズでもメッセージ内のプレースホルダーの位置なんかを言語によって変えないといけないけど、まさにその問題か」「そのあたりをi18nでもう少し何とかできるようになったということのようだ」「上はyamlファイルなんですね?」「ですです: i18nのfull_messageのテンプレートをyamlでオーバーライドできるようになったということでしょうね: 嬉しい人には嬉しいのかも?」「私は嬉しいです😄」

developmentモードのeager loadingを修正

Rails 5.1/5.2でconfig.eager_load = trueするとサーバーがロックすることがあったそうです。

# actionpack/lib/action_dispatch/journey/routes.rb#L51
       def ast
         @ast ||= begin
           asts = anchored_routes.map(&:ast)
-          Nodes::Or.new(asts) unless asts.empty?
+          Nodes::Or.new(asts)
         end
       end

つっつきボイス:config.eager_load = trueでロックとか恐ろしいw💀」「修正はめちゃシンプル: astだから抽象構文木ですね」「どうやらこいつが何かのはずみで同時に呼ばれるとヤバかったんだろうなー: コミットメッセージを見ると、Railsエンジンが複数ある場合にRouteSetsが空になる問題だったのか!」「😲」「自分はあまり踏まなそうなバグかな?」

参考: Rails エンジン入門 | Rails ガイド
参考: 抽象構文木 - Wikipedia

テスト中のパラメータエンコーディングにto_queryではなくRackを使用

# actionpack/lib/action_controller/test_case.rb#L104
           case content_mime_type.to_sym
           when nil
             raise "Unknown Content-Type: #{content_type}"
           when :json
             data = ActiveSupport::JSON.encode(non_path_parameters)
           when :xml
             data = non_path_parameters.to_xml
           when :url_encoded_form
-            data = non_path_parameters.to_query
+            data = Rack::Utils.build_nested_query(non_path_parameters)
           else
             @custom_param_parsers[content_mime_type.symbol] = ->(_) { non_path_parameters }
             data = non_path_parameters.to_query
           end

お馴染みAaron Pattersonさんによる修正です。


つっつきボイス: 「ちょうどMatz vs Aaron Patterson対談の記事があったので貼ってみました↑💎」「GitHub Satellite!😃HTTPのクエリパラメータのあたりで、to_queryだとエンコーディング前にソートされてたのをRackのに切り替えて修正したということかな」

associationのコレクション更新に-=を使うよう修正

これは自分で見繕いました。

この修正によってメモリ上で変更されたassociationもloadedとしてマーキングされるので、次回のアクセスでデータベースクエリの発生を回避できる。
同commitより大意

# activerecord/lib/active_record/associations/collection_association.rb#L399
         def remove_records(existing_records, records, method)
           records.each { |record| callback(:before_remove, record) }

           delete_records(existing_records, method) if existing_records.any?
-          hashed_records = records.group_by { |record| record }
-          target.select! { |record| !hashed_records[record] }
+          self.target -= records

           records.each { |record| callback(:after_remove, record) }
         end

つっつきボイス: 「あ、これちょうど今回の一番上の#29939の続きじゃないですか❤️」「ほんとだ: sgrifさんのサジェスチョンどおりですね🙂」「どこまで最適化されてるかはわかりませんが😆」「😆」

Rails

Railsへの需要は未だ巨大

サクッと読める記事です。


つっつきボイス: 「実際に使われている既存のRailsアプリがとても増えたし、エンジニアの需要も普通に多いというか減ってないという実感」「LinkedInの求職の件数(サンフランシスコのベイエリアなど)をチェックするというのはうまいですね: 言語ではRubyは必ずしも上位ではないけど、フレームワークでの求職数は圧勝しているというあたり」「ところでMatzがRailsネタでツイートするのは割と珍しいかも?」「確かにー」

tokaido: RailsアプリをmacOSアプリにする

Railsガイドに載ってました。東海道?


つっつきボイス: 「単独のMacアプリにできるということみたいです: こういうのがあったって知りませんでした」「自分も😆」「よく見ると最後の更新が3年前…Railsをまるっと飲み込んだら結構なサイズになりそう?」

AMPって今どうよ(Frontend Weeklyより)


つっつきボイス: 「AMPはもう普通に使われてると思うし: あと最近AMP JSも使えるようになるみたいだし」「あ、そうだった😲」

参考: AMP とは – AMP


ampproject.orgより

GraphQLは未来なのか?(Frontend Weeklyより)

「GraphQL is not your data model」という見出しがとりあえず気になりました。


つっつきボイス: 「日本だとGraphQLに夢を見ている人が多い印象🌠」「😆」「日本はフロントエンドとサーバーサイドを別の人がやってることが多いという事情もあるし: もしかするとソシャゲ界隈でGraphQL使いたい人が多いかもね🕶」

RailsでGraphQL APIをつくる: Part 1 – GraphQLとは何か(翻訳)

アセットパイプラインディレクトリの脆弱性


つっつきボイス: 「Herokuの記事でした」「productionでconfig.assets.compile = trueしなければ大丈夫だそうです(オレオレアプリもセーフ🤑)」「アセットのコンパイルって、ごくごく稀にオンにしてみることがあったりしたナ: コンパイルがうまく動かなかったときとか😆」「やったことなかった…」「Directory Traversalはなかなかエグい☠️けど、アプリがコンテナに置かれててchrootしているとかならそこまで深刻ではないのかもしれないけど、ね」

参考: ディレクトリトラバーサル - Wikipedia

git push -fはヤバい

参考: git push -f をやめて --force-with-lease を使おう - Qiita


つっつきボイス: 「実はこの間Gobyちゃんのリポジトリでgit push -fやらかしちゃって😓」「マジでw🤣」「今後やらかさないようgit config --system receive.denyNonFastForwards trueを唱えました」「master権限持っててぶっ壊したらどうしようもないけど、開発者ならprotectedブランチ的なものを立てるとか、masterブランチとdevelopブランチに直接pushできないようプロテクトしておくべきではあったかな😎」「そうでした😵」

「最近よく聞くのは、『featureブランチでは基本的にpush -fしない』という運用ですね: 重複更新が入ってもとりあえずコミットでつなげて、最後マージするときにsquashする」「なるほど!」「もちろんプロジェクト次第ですけどね: 個人的には、そのfeatureブランチを自分しか触ってないならpush -fしてもいいんじゃね?とは思うけど」

「いつもは自分のブランチでpush -fしてたんですが、今回はうっかりst0012さんのPRブランチにpushしちゃって💦」「人のブランチにgit push -fは、そりゃ戦争🔫💣ものだな🤣」「さすがにムッてたと思います…ほんとすみません🙏」「最初にfeatureブランチができたらそれ以外の人はpushできないようになったりするといいかもしれないですけどね😎、まあgit push -fは使わざるを得ないときもあるんでドンマイ」「😃」

Goby: Rubyライクな言語(2)Goby言語の全貌を一発で理解できる解説スライドを公開しました!

Railsアプリのデータベースロジックを失わない方法(RubyFlowより)

# 同記事より
app/
  sql/
    application.sql
    user.sql
    product.sql

つっつきボイス: 「sql/ディレクトリ掘ってるのが気になって」「それは普通に生SQLを置くところでしょうね: ははぁこれはマイグレーションについての記事か」「というと?」「通常のdb:migrateに限らない、データベースの更新とかも含むマイグレーションで使う生SQLはこういうところに置こうよという趣旨」「😃」

「ついでですが、これみたいに1回しか実行しない生SQLをどこに置くかっていつも悩ましいんですよ」「確かにー」「1回こっきりならスニペットで書くか、あるいはActiveRecord使うならrakeタスクにするとか: SQL一発だけならこの記事みたいな管理方法もありかなとは思う」

5つの手順で完璧なenumを作る(RubyFlowより)


つっつきボイス: 「完璧なenum🤣: まずはarrayじゃなくてhashでやろうとある、自分もhashでやるのが好きだし😋」

# 同記事より
class Catalog < ActiveRecord::Base
  enum localization: [:home, :foreign, :none]
end
0 -> home
1 -> foreign
2 -> none

「そうそう、業務システムなんかで、enumをデータベースに0とか1の値で入れることにこだわるケースがあったりするんですが、それだとソース見ないと値の意味がわからないんじゃね?って思うことしばしば😆」「それ確かにー」「自分はstringで保存したい派: PostgreSQLだと確か文字列enumが使えて、MySQLにもenum型が確かあって、そういうのを使えばできるし」

# 同記事より
class Catalog < ActiveRecord::Base
  enum localization: { home: 0, foreign: 1, none: 2 }
end

「お、この記事はPostgreSQL enum使ってるじゃん↓: エライ!」「これだとデータベース内部は数値でもクエリでは文字列が使えるんですね」「そうそう、やるなら自分はぜひこっちにしたい😋」「😃」

# 同記事より
class AddStatusToCatalogs < ActiveRecord::Migration[5.1]
  def up
    execute <<-SQL
      CREATE TYPE catalog_status AS ENUM ('published', 'unpublished', 'not_set');
    SQL
    add_column :catalogs, :status, :catalogs_status
  end

  def down
    remove_column :catalogs, :status
    execute <<-SQL
      DROP TYPE catalog_status;
    SQL
  end
end

「で究極のソリューションは、と: おー、これはたぶんPostgreSQLの機能だと思うんですが、型を追加してますね、CREATE TYPE catalog_status AS ENUMで」「😃」「createのenum定義に直接型を書く代わりに、catalog_statusという名前を付けてそれを使ってマイグレーションをすると: これは確かにキレイに書ける」「おー、ぽすぐれ側でやろうということですね」「まあ今度はこのcatalog_statusって定義はどこなんだ?と思われるかもしれないし、ここまでぽすぐれに頼っていいんだろうかとも思いますが😆」

# 同記事より
# マイグレーション
class AddStatusToCatalogs < ActiveRecord::Migration[5.1]
  def up
    execute <<-SQL
      CREATE TYPE catalog_status AS ENUM ('published', 'unpublished', 'not_set');
    SQL
    add_column :catalogs, :status, :catalogs_status
    add_index :catalogs, :status
  end

  def down
    remove_column :catalogs, :status
    execute <<-SQL
      DROP TYPE catalog_status;
    SQL
  end
end

# ValueObject:
class CatalogStatus
  STATUSES = %w(published unpublished not_set).freeze

  def initialize(status)
    @status = status
  end

  # what you need here
end

RailsアプリでReactをホットリロードに使う(RubyFlowより)


同記事より


つっつきボイス: 「ホットリロードは最近のWebアプリではよく見かけますね: フロントだと欲しいヤツ」「Webpackも使ってるし」

Railsのcredential

先週Railsのcredentialの話が出てたので。


つっつきボイス: 「この間の話は要するにcredentialをリポジトリにコミットする場合にマスターキーをチームでどううまく扱うかという問題でしたが、この記事みたいにマスターキーをAWSのKMSに保存すれば、KMS内の生の鍵には開発者のアクセスを許す必要がないし、KMSに置かれた鍵をIAM経由で使うようにすれば、プロジェクトから抜けたメンバーをIAM Groupから外すだけでマスターキーにアクセスできなくなるということですね」「😃」

Rails APIとJWT認証とVuejsでSPAする(RubyFlowより)


つっつきボイス: 「JWTはWeb界隈では新し目の認証方式ですね」



jwt.ioより

最近よかったRuby/Rails記事: 2018年前半(RubyFlowより)


つっつきボイス: 「ざっと見たところこれまでウォッチで既に取り上げたり翻訳したりした記事も載ってて、ちょっとだけ『勝った』感ありました😆」「😆」

RabbitMQはSidekiqと同等以上だと思う: 前編(翻訳)

search_flip: ElasticsearchでクエリをチェインするDSL(RubyFlowより)

Finally, SearchFlip supports ElasticSearch 1.x, 2.x, 5.x, 6.x. Check section Feature Support for version dependent features.
同リポジトリより


つっつきボイス: 「よくあるやつなのかなと思って」「お、これElasticsearchの6.xに対応してるのがエライですね!: たしかこの間のRails Developers Meetupだったかな、Elasticsearch向けの何かのRubyのライブラリがElasticsearchの5.xまでしか対応してなくて新しい機能はまだ使えないねみたいな話があったと思ったんだけど、6.xに対応しているという理由でこのgemを使う人がいるかも?」

ActiveSupport::MessageEncryptor


つっつきボイス: 「そうそう、Rubyでencrypter/decrypter使うとだいたいこんな感じになりますね: しかしActiveSupportの割にはローレベルな書き方😆」

# 同記事より
ENCRYPT_CIPHER = 'aes-256-cbc'
ENCRYPTOR = begin
  key_len = ActiveSupport::MessageEncryptor
            .key_len(ENCRYPT_CIPHER)
  key = ActiveSupport::KeyGenerator
        .new("<環境から挿入したkey>")
        .generate_key("<環境から挿入したsalt>", key_len)
  ActiveSupport::MessageEncryptor.new(
    key,
    cipher:     ENCRYPT_CIPHER,
    digest:     'SHA1',
    serializer: Marshal,
  )
end

「その後に『デフォルト値には頼らない』って書いてますけど、これは使う処理系やライブラリによってデフォルト値が違うことがあるんですよ」「というと?」「以前一度はまったのが、PHPのライブラリでencryptしたものをRubyのライブラリでdecryptしようとしたら、どっちかのライブラリでゼロフィル(ゼロ値で埋めること)やられてて復元できなかったことあったんですよ😭: たしか相当無理やりに突破した」「ありゃ~」「ライブラリを素直に無加工で使ってくれればいいものを、そうやってデフォルト値変えられたりするとつらい」

「そういえば私も正規表現ライブラリを異なる言語間で共通で使おうとして似たようなハマり方したことありました😢」「それもあるある: eregなのかpregなのかとかね、といっても最近pregしか見かけないけど😎」

参考: preg_match関数と正規表現の理解を再度見直す - Qiita

その他Rails



trello.comより

Ruby trunkより

提案: 第2のGCヒープ「Transient heap」

ko1さんによる提案です。

mallocが管理するヒープに代わる第2のGCヒープ「Transient heap」のMRIへの導入を提案します。利用されるGCアルゴリズムは「世代的」「コピー」GCアルゴリズムに近いものです。これによってmallocされたヒープの問題を軽減できると見込みます。
同issueより大意


つっつきボイス: 「mallocの手前に別のメモリ管理レイヤを置くという感じ: 結局OSのメモリ管理とRubyのメモリ管理という2層があるから、mallocだといろいろうまくいかないところがあるんでしょうね」「ふーむ🤔」「今日の社内勉強会でキャッシュのしくみの話をしたけど、キャッシュの場合と似た感じで、同じ仕事をする層が複数あるとそれぞれがてんでに局所最適に動作して全体としてうまくいかない、なんてことが起きがちですね」

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

Time.strptime%jを使うユリウス暦で正しく動かない

# 同issueより
require "time"
require "date"

### Works for Dates ###
parsed_date = Date.strptime("15300", "%y%j")
expected_date = Date.new(2015, 10, 27)
# Does not raise
raise "dates not equal" if expected_date != parsed_date


### Does not work for Time ###
parsed_time = Time.strptime("15300", "%y%j")
expected_time = Time.new(2015, 10, 27)
# Raises
raise "times not equal" if expected_time != parsed_time

つっつきボイス:Dateだと動くのにTimeだと動かないというバグ」「Julian date=ユリウス暦の日付」

参考: ユリウス暦 - Wikipedia

ローマ教皇グレゴリウス13世が1582年、ユリウス暦に換えて、太陽年との誤差を修正したグレゴリオ暦を制定・実施したが、今でもグレゴリオ暦を採用せずユリウス暦を使用している教会・地域が存在する。
Wikipediaより

ツイートより

Timeにタイムゾーンを設定する公式のAPIが欲しい

# 同issueより
>> ENV['TZ'] = 'America/New_York'
>> Time.now.zone
=> "EDT"
>> ENV['TZ'] = 'Europe/London'
>> Time.now.zone
=> "BST"

ActiveSupport::TimeWithZoneでやってるようなのを定めたいそうです。

参考: ActiveSupport::TimeWithZone


つっつきボイス: 「タイムゾーンは誰がというかどこで一元管理するのかという問題がそもそもあるんですけど、最終的にはOSの環境変数が頼りですね: この機能はActiveSupportから持ってきてもいいと思う」

Ruby

StripeのRuby lint


sorbet.runより


つっつきボイス: 「Stripeはこの辺に力入れてますね: RubyKaigiのこの発表見られなかった😢」「この間のウォッチでもちょっとだけ扱いましたが一応」「sig(foo: Integer).returns(String)みたいにシグネチャを指定するのか: Matz好みでないやり方😆」「構文変わっちゃいますもんね…」

# sorget.runより
class A
  sig(foo: Integer).returns(String)
  def bar(foo)
    foo.to_s
  end
end

def main
  A.new.barr(91)
  A.new.bar("91")
end

グループ名がない場合にスキップする修正

Windows対応のようです。

mruby-meta-circularとは


つっつきボイス: 「RiteVMってmrubyのVMなんですね: Riteというとつい春の祭典を連想してしまいます🙂」

参考: mruby Virtual Machine(RiteVM) — mrubook 1.0 documentation

「今回のRubyKaigi 2018を見てて思ったんですが、mrubyって組み込み系という印象がそれまで強かったけど、最近はむしろH2Oみたいなミドルウェア系での活躍が目立っててそちらで見直されてる感」「確かにH2Oの成功がアピールした感じありますね😃」

参考: H2O - the optimized HTTP/2 server


h2o.examp1e.netより

別件ですがこんなのも。

dining-table: Rubyでテーブルをきれいに作るgem

# 同リポジトリより
class CarTableWithConfigBlocks < DiningTable::Table
  def define
    table_id = options[:table_id]  # custom option, see 'Options' above

    presenter.table_config do |config|
      config.table.class = 'table-class'
      config.table.id    = table_id || 'table-id'
      config.thead.class = 'thead-class'
    end if presenter.type?(:html)

    presenter.row_config do |config, index, object|
      if index == :header
        config.tr.class = 'header-tr'
        config.th.class = 'header-th'
      elsif index == :footer
        config.tr.class = 'footer-tr'
      else  # normal row
        config.tr.class = index.odd? ? 'odd' : 'even'
        config.tr.class += ' lowstock' if object.stock < 10
      end
    end if presenter.type?(:html)

    column :brand
    column :stock, footer: 'Footer text'
  end
end

つっつきボイス: 「名前が😆」「一度こういう感じに作っておけばHTMLテーブルの他にも後でcsvとかxlsxとかいろんな形式で出せるのか: カスタマイズしたテーブルを扱うときなんかにちょっといいかも😋」

MRuby-Zest: mrubyのオーディオGUIフレームワーク

長ったらしいif-else条件のリファクタリング(RubyFlowより)

# 同記事より
# Before
# Metrics/MethodLength: Method has too many lines. [13/10]
def foo1(number)
  if number == 1
    'one'
  elsif number == 2
    'two'
  elsif number == 3
    'three'
  elsif number == 4
    'four'
  elsif number == 5
    'five'
  else
    'many'
  end
end

# After
DICTIONARY = {
  1 => 'one',
  2 => 'two',
  3 => 'three',
  4 => 'four',
  5 => 'five'
}.freeze

def foo2(number)
  DICTIONARY[number] || 'many'
end

つっつきボイス: 「うん、これは定番のリファクタリングですね: よく使うというか最初からこう書くし」「そうでしたか!😲」「DICTIONARYを作ることで意味が明確になるし、条件を後で変えたり追加したりするのも楽だし、何より読みやすい❤️」

「↓こんなふうにlambda使うのはちょっとスゴイな😉、でもこれも自分使ってるわ」「😀」「こうやってlambda使ったり出力をevalしたりするとかすると最強感💪」

# 同記事より
# After
module SearchService2
  PARAMS_MAP = {
    'id' => ->(value) { { id: value } },
    'first_name' => ->(value) { { 'first_name' => value } },
    'last_name' => ->(value) { { 'last_name' => value } },
    'email' => ->(value) { { 'email' => value } },
    'city' => ->(value) { { 'city' => value } },
    'gender' => ->(value) { { gender: value } },
    'height_after' => ->(value) { { height: { '$gt' => value } } },
    'height_before' => ->(value) { { height: { '$lt' => value } } },
    'weight_after' => ->(value) { { weight: { '$gt' => value } } },
    'weight_before' => ->(value) { { weight: { '$lt' => value } } },
  }.freeze
...

その他Ruby



つっつきボイス: 「言われてみればそうだったわー😲: あんまり使わないけど…」






TCFM出演?

  • 追記(2018/06/23): しまったこれでした↓


RubyKaigi 2018での生ペアプロ動画も上がってました。

クラウド/コンテナ/インフラ/Linux

Linux高速化の話題2つ


つっつきボイス: 「最近のLinuxネットワークの詳細は正直よくわからん😆: まともに追いかけてたのはカーネル2.6ぐらいまでだったし」「あー、じゃLinuxも結構がっつり更新されてるんですね」「ちゃんと更新されてますよ😎」

書籍「実践 パケット解析 第3版」

OSI階層とは

このネタ割と前からあったんですね。

参考: OSI参照モデル: 比喩 - Wikipedia

Google I/O 2018全動画

その他クラウド/コンテナ/インフラ/Linux





つっつきボイス: 「これ定期的に出回るネタ😎」「🤣」




SQL

試さないと損するPostgreSQLの機能(Postgres Weeklyより)

AWS RedshitとPostgreSQLはどこが違うか(Postgres Weeklyより)

pgdeltastream: PostgreSQLの更新をリアルタイムに観測(Postgres Weeklyより)


同リポジトリより

PostgreSQLの関数

はてブで見かけました。

JavaScript

AirbnbがReact Nativeをやめた話

はてブなど各所で話題ですね。ついAirBnBと書いてしまいそうです。

↑先越されちゃいました(´・ω・`)。


つっつきボイス: 「これもmizchiさんが早速アンサー記事↓書いてくれてますね」

参考: いつ ReactNative を使っても大丈夫か - mizchi’s blog

flutter.ioとAndroid fuchsia OS

社内Slackで密かに見守られています。Dart言語ベースだそうです。


より

参考: Dart - Wikipedia

Flutterが何らかの形でFuchsia OSに採用されるかもしれないとも。

参考: iOSとAndroid、両方のアプリを一度に作れちゃう優れモノ! Googleの隠れた戦略が見える「Flutter SDK」 | ギズモード・ジャパン

Parcel 1.9.0リリース(JSer.infoより)

その他JavaScript


CSS/HTML/フロントエンド/テスト

「WebDriver」がW3CでRECに

参考: 「WebDriver」がW3Cの勧告に到達。Webブラウザのテスト自動化などを実現 - Publickey

GDPRどころじゃないのかも

Creative Commonsも反対を表明しています。つかTechRachoも他人事ではありません。

参考: ハイパーリンクを貼るだけで著作権料がかかる通称「リンク税」がEUで導入されようとしている - GIGAZINE

CSS+JSのアニメーションロゴ

Unicode 11.0リリース


blog.unicode.orgより


つっつきボイス: 「とりあえず絵文字増えましたね❤️」「海老🍤」

参考: Emoji Recently Added, v11.0


unicode.orgより

リグレッションとデグレーションの違い

その他CSS/HTML/フロントエンド/テスト


言語よろずの間

マイクロソフトのBlazorとは

GoからPHPに帰ってきた話

話は少し逸れますが、先日のRubyKaigi 2018のドリンクアップでPHP開発者の方とお話しする機会があり、PHPでは英語情報を探さないといけない状況がほぼまったくないと伺いました。

書籍「Go言語でつくるインタプリタ」の続編「Go言語でつくるコンパイラ」がリリース

↓参考まで: こちらは例のGobyちゃんに最も強く影響を与えたリファレンスです。

SHA1コリジョン

各所でバズってますね。

モンティ・ホール問題より手ごわい「2封筒問題」

BPS社内でも盛り上がりました。

↑この問題について最も引用されているという短い論文です。読んでもいませんが、三浦俊彦『可能世界の哲学』によると、「交換すると双方が得になるのは事前期待値が無限大の場合に限られる」ことがこの論文で示されているんだそうです。

同書によると、この2封筒問題を扱うサイトや書籍は必ず炎上するらしく、出版界では密かに疎まれているんだそうです。Railsウォッチもついに炎上?

参考: 数学 - 二封筒問題
参考: 可能世界論 - Wikipedia

同書で紹介されていた以下の本もちょっと気になります。命名の助けになるか混乱の元になるかわかりませんが。

参考: 藤川直也『名前に何の意味があるのか 固有名の哲学』 - logical cypher scape

Rust 1.27リリース

// 同記事より
// SIMDなし
let lots_of_3s = (&[-123.456f32; 128][..]).iter()
    .map(|v| {
        9.0 * v.abs().sqrt().sqrt().recip().ceil().sqrt() - 4.0 - 2.0
    })
    .collect::<Vec<f32>>();

// SIMDあり
let lots_of_3s = (&[-123.456f32; 128][..]).simd_iter()
    .simd_map(f32s(0.0), |v| {
        f32s(9.0) * v.abs().sqrt().rsqrt().ceil().sqrt() - f32s(4.0) - f32s(2.0)
    })
    .scalar_collect();

参考: SIMD - Wikipedia

その他言語


たぶん反語?
私は.vimrcは最小限しか設定してません。



あらゆるGoリポジトリにgofmt -w .をかけてまわるようです。



その他

3D XPointメモリとは

macOSの「クイックルック」は残る

SQLite使ってるって初めて知りました。

参考: macOSの「クイックルック」は暗号化ドライブのファイルまでキャッシュしており、データは永続的に保管されいつでも閲覧できる - GIGAZINE

人感センサー

これ改造がとっても楽なんだそうです。

その他のその他


純粋に欲しいです。けどCD-ROM販売…









つっつきボイス: 「あーこれ何だっけ??」「タイガー式計算機ですね: 自分も実物見たことありませんが😭」

朝永振一郎のエッセイでこれと朝晩格闘してたという記述を見たぐらいでした。

参考: 機械式計算機 - Wikipedia



↑後はワイヤレスになってくれれば完璧ですね。


番外

氷河期の写実的な壁画は自閉症スペクトラムの表れという説

参考: 氷河期の壁画が驚くほどに写実的だったのは「描き手が自閉症だったから」という説明 - GIGAZINE

明和電機健在

参考: 明和電機 - Wikipedia

特大ステッカー

変わり筐体

極太マジックハンド

過酷な環境で酸素を生むバクテリア

参考: 人類の火星移住で酸素の供給源として活躍するかもしれないバクテリアが発見される - GIGAZINE

WiFiで壁の向こうを透視

既にWiFiでの室内の物体の位置特定ってできてた気も。

材料は星

極大から極小までをインタラクティブに


htwins.netより

元になった以下の「Powers of Ten」はかなり昔にIBMが制作した定番の教育コンテンツだそうです。


つっつきボイス: 「やっぱり最後は宇宙ネタ?」「そう決めてるわけではないんですが、何となくで😆」


今週は以上です。

おたより発掘

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

週刊Railsウォッチ(20180615)TTY gemとHTTPClient gemは優秀、Rubyの謎フリップフロップ、ちょいゆるRubyスタイルガイドほか

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

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

Rails公式ニュース

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

JavaScript Weekly

javascriptweekly_logo_captured

JSer.info

jser.info_logo_captured

Ruby: 500バイトで書けるSHA256生成スクリプト(翻訳)

$
0
0

概要

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

Ruby: 500バイトで書けるSHA256生成スクリプト(翻訳)

STDIN(標準入力)のSHA 256ハッシュサムを生成できるRubyコードは何バイト(=ASCII文字数)あれば書けるでしょうか?

答え: 500バイト1

q,z=[3,2].map{|t|i=l=1;(2..330).select{i-1<(l*=i)%i+=1}.map{|e|(e**t**-1*X=2**32).to_i&X-=1}}
s=proc{|n,*m|a=0
m.map{|e|a^=n>>e|n<<32-e}
a}
i=$<.read.b<<128
(i+"\0"*(56.-(w=i.size)%64)+[~-w*8].pack('Q>')).gsub(/.{64}/m){w=$&.unpack'N*'
y=z
64.times{|i|i>15&&w[i]=w[i-16]+(s[v=w[i-15],7,18]^v>>3)+w[i-7]+(s[w[i-2],17,19]^w[i-2]>>10)&X
f=y[7]+s[u=y[4],6,11,25]+(u&y[5]^~u&y[6])+q[i]+w[i]
y=[f+s[y[0],2,13,22]+(y[0]&y[1]^y[0]&y[2]^y[1]&y[2])&X]+y
y[4]=y[4]+f&X}
z=z.zip(y).map{|a,b|a+b&X}}
$><<'%.8x'*8%z

上は、とあるコードゴルフコンテスト(現在はありません)に送信されたコードをベースにしており、これよりもさらに短いものも送信されていたので、これをさらに短くすることも可能です。プルリクを探してみてください!

テストスクリプト

上のコードをsha256.rbというファイルに保存して、同じディレクトリに以下のテストスクリプトを作成します。

# encoding: utf-8

require "digest/sha2"
require "open3"

TEST_SCRIPT = "ruby sha256.rb"
TEST_CASES  = [
  "Idiosyncrätic Ruby",
  "\n",
  "",
  "a"*6000,
  "a"*57
]

TEST_CASES.each{ |test_case|
  expected = Digest::SHA256.hexdigest(test_case)
  actual = nil
  Open3.popen3(TEST_SCRIPT){ |in_, out,|
    in_ << test_case
    in_.close
    actual = out.read
  }
  if expected == actual
    print "."
  else
    $stderr.puts "Fail:
- Expected: #{expected.inspect}
- Actual:   #{actual.inspect}
"
  end
}

参考

関連記事

Ruby 2.5の`yield_self`が想像以上に何だかスゴい件について(翻訳)

Ruby: 自分のクラスに`#to_a`を実装すべきじゃない(翻訳)


  1. 上のコードは実際には501バイトですが、"\0"を本物のnullバイトに置き換えることもできます。 
Viewing all 1895 articles
Browse latest View live