(2015年までの)odaillyjp blog

イベント参加記録とプログラミング系の雑記

ActiveSupport::Callbacks を使ってメソッドに前処理を挟み込む

Rails の Controller には、アクションメソッドに前処理を挟み込むための before_action というメソッドが存在します。ActiveRecord などを継承していない普通の Ruby のクラスでも簡単に前処理を挟み込めるようにしたいと思う場面がありまして、便利なモジュールがないか調べたところ、ActiveSupport::Callbacks というモジュールを見つけました。このエントリでは、このモジュールの使い方を簡単に紹介していきます。

ActiveSupport::Callbacks

ActiveSupport::Callbacks

ActiveSupport::Callbacks は「あるコードが実行されることをきっかけにして、別の処理を呼び出す」という処理が簡単に定義できるようになるモジュールです。エントリの冒頭で話したように「あるメソッドに前処理を挟み込む」といった実装が簡単に作れるようになります。ちなみに、実行されるきっかけを「フック」、フックによって呼び出されるメソッド「コールバックメソッドと呼びます。

早速、ActiveSupport::Callbacks を利用した単純なクラスを定義し、処理を挟み込むことができていることを確認します。

class Entry
  include ActiveSupport::Callbacks
  define_callbacks :before_check
  set_callback :before_check, :before, :check

  def check
    puts 'Checking.'
  end

  def submit
    puts 'Submit entry.'

    run_callbacks :before_check do
      puts 'Submitting.'
    end

    puts "You've successfully submit."
  end
end

Entry.new.submit
# Submit entry.
# Checking.
# Submitting.
# You've successfully submit.

submit メソッドを呼び出したときの様子を見ると、run_callbacks ブロック内のコードが実行される直前で check メソッドが実行されていることがわかります。ActiveSupport::Callbacks を使うときに大事なことは次の3つです。

  • define_callbacks メソッドでフック名を定義しておく
define_callbacks :hooks_name
  • set_callback メソッドで「どのフック名の」「どのイベントをトリガーにして」「どのコールバックメソッドを呼び出すか」を定義しておく
set_callback :hooks_name, :before, :callback_mehotd
  • run_callbacks メソッドでフックが発生する処理を指定しておく
run_callback :hooks_name do
  /* 処理 */
end

使い方はわかりましたでしょうか。今回紹介した使い方以外に、set_callback メソッドに if というオプションを渡すことで「特定の条件に当てはまるときだけコールバックメソッドを呼び出す」ということもできるようになりますので、より詳しく知りたい方はぜひドキュメントをご一読ください。

MySQLのインデックスについて学びました

前回の記事に続き、MySQLに関するネタを書きます。今回の内容のほとんどは『実践ハイパフォーマンスMySQL』の読みながらまとめた内容になりますので、より詳しく知りたい方はそちらをご一読ください。

実践ハイパフォーマンスMySQL 第3版

実践ハイパフォーマンスMySQL 第3版

確認したMySQLのバージョン: 5.6.22

インデックスの基礎知識

インデックスについて一言で説明するのは難しいので、まずはMySQLが目的のデータをどのようにして見つけるのかを説明します。MySQLに次のSQLクエリを発行したとします。

SELECT * FROM users WHERE name = 'foo';

これはnameというカラムに'foo'という値を持つデータを取得するSQLクエリです。このSQLクエリを発行したとき、まずMySQLはusersテーブル内の全てのデータを取得します。つぎに、取得したデータを1件ずつ順番に見ていき、nameが'foo'であるデータだけを抽出します。最後に、抽出したデータを結果として返します。これがMySQLが目的のデータを見つけるときの動作の流れになります。

途中で「テーブル内の全てのデータを1件ずつ順番に見ていき、条件に合うデータだけを抽出する」という動作が行われましたが、この動作のことを「フルテーブルスキャン」と呼びます。検索対象のテーブルのデータ数が少なければ、フルテーブルスキャンにかかる時間は短いですが、データ数が多くなればなるほど、フルテーブルスキャンにかかる時間は長くなっていきます。仮に1件のデータの値を確認するのに1秒かかるとすると、10,000件のデータから目的のデータを抽出するのには、単純計算で10,000秒かかることになります。アプリを運用していけばデータは徐々に増えていきますので、抽出にかかる時間はさらに長くなることが予想されます。

この問題を解決するために、データを保存したときに「指定したカラムの値」と「そのデータの保存位置情報」を本の索引のような構造で保存しておき、目的のデータを抽出するときにその索引を使って探すことができる仕組みがデータベースに用意されました。この索引のことを「インデックス」と呼びます。インデックスを使うことで、フルテーブルスキャンを行うよりも早くデータを抽出することができます。インデックスは下記のSQLクエリで定義することができます。

ALTER TABLE users ADD INDEX(name);

検索時にインデックスが使われているかはSQLのEXPLAIN句で調べることができます。

EXPLAIN SELECT * FROM users WHERE name = 'foo';

結果がこのように出力されます。

id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE users ref name name 767 const 1 Using index condition


keyの項目が検索に使われたインデックス名になります。ここがNULLでなければ、インデックスが正しく使われていると考えて良さそうです。

インデックスには様々な種類があります。インデックスの種類によって、インデックスを使うことができる状況や、検索にかかる時間が変わります。先ほど紹介したALTER TABLE構文ではインデックスの種類を指定していませんでしたので、MySQLInnoDBストレージエンジンのデフォルトである「Bツリーインデックス」という種類のインデックスが作られました。Bツリーインデックスの構造を説明するにはいくつかの前提知識が必要ですので、ここでは触れないことにします。

複合インデックス

下記のように記述することで、複数のカラムの値を組み合わせたインデックスを作ることできます。

ALTER TABLE users ADD INDEX(last_name, first_name);

このようなインデックスを「複合インデックス」と呼びます。複合インデックスは、ADD INDEXの中で指定したカラムの順番に従って、キーが作られます。last_nameが「まさや」、first_nameが「さとう」でデータベースに登録した場合、このデータのキーは「まさや-さとう」になります。インデックスが使われるかどうかに、カラムの順番が影響することがありますので、この仕様は覚えておいたほうが良いと思います。

ユニークインデックス

通常のインデックスは主にデータの検索時に使われますが、データの登録時に値がユニークであるかを確認するためにも使われるインデックスがあります。それが「ユニークインデックス」です。ユニークインデックスは下記のように記述することで作ることができます。

ALTER TABLE users ADD UNIQUE(first_name, last_name);

ユニークインデックスでは、キーが同じになるデータを登録したときにエラーが発生します。

INSERT INTO `users` (`id`, `last_name`, `first_name`) VALUES ('2', 'まさや', 'さとう');
/* => Duplicate entry 'まさや-さとう' for key 'last_name' */

Bツリーインデックスを使うことができる状況

作ったインデックスは使われなければ意味がありません。どのようなクエリを発行したときにインデックスが使われるのか、まとめていきます。例として、下記の構造のテーブルを使います。

CREATE TABLE users (
  id VARCHAR(50) PRIMARY KEY,
  last_name VARCHAR(50) NOT NULL,
  first_name VARCHAR(50) NOT NULL,
  gender enum('male', 'female') NOT NULL,
  key(last_name, first_name)
);

基本は、キーの前方部分に一致する条件で検索を行ったときにインデックスが使われます。インデックスが使われる具体的なケースをいくつか紹介します。

キーと完全一致する検索

last_nameとfirst_nameの2つの条件に完全一致する検索では、インデックスが使わます。

SELECT * FROM users WHERE last_name = 'まさや' AND first_name = 'かとう';

キーの先頭に指定したカラムでの検索

今回は「last_name、first_name」の順番で複合インデックスを作成しています。1番目に記述したlast_nameだけでの検索であれば、このインデックスを使うことはできます。

SELECT * FROM users WHERE last_name = 'まさや';

しかし、2番目に記述したfirst_nameだけでの検索では、このインデックスを使うことはできません。

/* インデックスは使われない */
SELECT * FROM users WHERE first_name = 'かとう';

先ほど「インデックスが使われるかどうかに、カラムの順番が影響することがある」と言いましたが、このような場面のときに影響するということです。

キーの前方に一致する検索

LIKEを使って前方に一致する検索を行ったときもインデックスを使うことはできます。

SELECT * FROM users WHERE last_name LIKE 'まさ%';

しかし、後方に一致する検索ではインデックスを使うことはできません。

/* インデックスは使われない */
SELECT * FROM users WHERE last_name LIKE '%や';

範囲を指定した検索

BETWEEN句などを使った範囲を条件にした検索でもインデックスは使うことができます。

SELECT * FROM users WHERE last_name BETWEEN 'あきら' AND 'みつる';

上記のSQLクエリのように文字列の範囲検索を行うことは少ないと思いますが、数値の範囲検索を行うことは多いと思います。そのときは対象のカラムのインデックスを作成しておくと良さそう。

GROUP BY を使った検索

対象のカラムをグルーピングした検索でも、インデックスを使うことができます。

SELECT * FROM users GROUP BY last_name;

その他

他にも ORDER BY を使った検索でもインデックスが使われることがあるようです。(必ず使われるというわけではなく、いくつか制約があるようですので、今回は紹介できませんでした。)

まとめ

  • インデックスとは、MySQLが目的のデータを効率的に探すために作られたデータ構造
  • インデックスが使われない場合は全てのデータを一つ一つ確認していく「フルテーブルスキャン」が行われる
  • インデックスが使われているか確認したいときは、EXPLAIN句を使ってkeyの値を見る
  • 複数のカラムを組み合わせたインデックスを作成できる(複合インデックス)
  • 複合インデックスを作るときは、カラムを指定する順番が重要になる
  • ユニークインデックスを使うことで、データの登録時に対象の値がユニークであるかを確認することができる
  • Bツリーインデックスを使っていて、キーの前方に一致する検索を行ったときは、インデックスは使われる
  • Bツリーインデックスを使っていて、キーの後方に一致する検索を行ったときは、インデックスは使われない

MySQLの「InnoDB」と「MyISAM」についての易しめな違い

今回はMySQLのストレージエンジンという重箱の隅をつつくようなネタをメモしておきます。
私はMySQLについてはよちよちレベルなので、込み入った内容には触れられません。初心者でも理解しやすい部分での違いを中心にまとめていこうと思います。

【復習】ストレージエンジンとは

MySQLなどのデータベース管理システムが、データベースにデータを書き込んだり、読み込んだりするときに使われる基盤を「ストレージエンジン」と呼びます。初心者同士の会話の中で「MySQLを使ってデータを保存する」と表現することがありますが、実際にデータの保存処理を行っているのが、このストレージエンジンになります。MySQLの論理構造を図で載せます。

f:id:Shindo_Masaya:20150401154842p:plain:w500

MySQLは「データベース管理システム」ですので、データベースを管理するために必要である様々な機能が搭載されています。その中の「データの読み書き」という部分を担当しているのがストレージエンジンになります。

MySQLには様々なストレージエンジンが最初から組み込まれています。その中でも特に有名なのがInnoDBMyISAMです。どちらも「データの読み書き」というストレージエンジンに必須である機能は搭載されていますが、細かい機能に違いがあります。初心者はどちらを使えばいいのか迷うと思います。さて、どちらを使えばいいのでしょうか。

MyISAMInnoDBの違いの中で、初心者が理解しやすいところ

いよいよ本題に入ります。MySQLのバージョン5.5以降では、InnoDBというストレージエンジンがデフォルトとなっています。InnoDBの特徴を調べて紹介することは私には難し過ぎますので、InnoDBMyISAMの違いの中で、わりと理解しやすいものを2つだけを紹介します。

ロックの粒度

1つ目はロックの粒度です。ロックとは何でしょうか。例えば、複数のクライアントから同時に「このレコードを更新してください」という内容のリクエストが来たとします。ストレージエンジンが1つ目のリクエストの内容に沿って、更新処理を始めました。その最中に2つ目のリクエストの更新処理も始めてしまいました。こうなると、1つ目のリクエストと2つ目のリクエストが混ざり合って、誰も意図していなかった内容で更新されてしまうかもしれません。このようなことを防ぐために、ストレージエンジンは更新対象のテーブルやレコードに対して書き込みや読み込みを一時的に止めるように命令を出します。これを「ロック」と呼びます。InnoDBは対象のレコードに対してロックを行いますが、MyISAMは対象のテーブルに対してロックを行います。InnoDBは同じテーブルであっても異なるレコードであれば同時にリクエストを処理することができますが、MyISAMはそうはいきません。InnoDBのがロックの粒度が低いので、MyISAMより処理が早くなると考えられているようです。ただし、マシンへの負担はMyISAMのが小さいようですので、複数のクライアントから同時に更新などのリクエストを受けることが少ないアプリであれば、MyISAMのが良いという意見もあるようです。

トランザクション

2つ目はトランザクションの有無についてです。トランザクションを一言で説明すると複数SQLクエリを1つの作業としてまとめたもの」です。例えば、Aさんが自分の銀行口座からBさんの銀行口座に10,000円を振り込むとします。これをプラグラムに処理させる場合、手順はこのようになります。

1. Aさんの銀行口座の残高が10,000円以上であることを確認する。

2. Aさんの銀行口座の残高から10,000円を引く。

3. Bさんの銀行口座の残高に10,000円を足す。

この一連の手順が正常に終われば、振り込みは完了です。しかし、途中でトラブルが発生し、処理が中断されることがあるかもしれません。いずれかのステップで失敗した場合は、ロールバックしたいですね。そこで登場するのがトランザクションです。上記の一連の手順をトランザクションとしてまとめておくと、いずれかのステップで失敗した場合にロールバックを行ってくれます。InnoDBではトランザクション機能をサポートしていますが、MyISAMではサポートしていません。トランザクション機能を使いたいならば、InnoDB(もしくは、他のトランザクション機能があるストレージエンジン)を選ばないといけません。

トランザクションの実験

念のために、トランザクション機能の有無をRailsを使って確認してみます。InnoDBのテーブルを用意し、適当なコントローラーに下記のメソッドを追加して、このメソッドを呼び出してみます。

def test_transaction
  ActiveRecord::Base.transaction do
    User.create(name: 'hoge')
    fail
  end
end

メソッド呼び出し後に、Userの総数を見てみます。

# InnoDBのとき
User.count
=> 0

0件です。途中で処理が失敗していますので、ロールバックされています。同じ処理をMyISAMでも行ってみます。

# MyISAMのとき
User.count
=> 1

データが1件残っています。トランザクションが機能していませんね。やはりMyISAMにはトランザクション機能がないようです。

InnoDBMyISAMの使い分け

今回は2つの機能面による違いでInnoDBMyISAMを見てきましたが、どちらを使うのがよいのでしょうか。書き込みが多いアプリケーションの場合はInnoDBのが有利そうな感じがしますね。では、書き込みが多いアプリケーションの場合はMyISAMのが良いのでしょうか。いろいろと調べてみると、答えはノーらしいです。MyISAMはサーバーがクラッシュしたときの対応策が少ないようです。今回は初心者向けの記事ですので、これ以上の内容には触れませんが、より詳しく知りたい方は『実践ハイパフォーマンスMySQL』の第1章で説明されていますので、そちらをご一読ください。

まとめ

  • データの読み書きを行う基盤のことを「ストレージエンジン」と呼ぶ
  • ストレージエンジンには「InnoDB」や「MyISAM」など様々な種類がある
  • MySQL5.5以降でのデフォルトのストレージエンジンはInnoDB
  • InnoDBは対象のレコードだけをロックする(行ロック)
  • MyISAMは対象のテーブル自体をロックする(テーブルロック)
  • InnoDBにはトランザクション機能がある
  • MyISAMにはトランザクション機能がない
  • 更新処理が多いアプリケーションの場合はInnoDBを選ぶと良さそう。そうではない場合もInnoDBのが色々と楽かも?

PointDNSでHerokuアプリにNaked domainを割り当てる

HerokuアプリにNaked domain(www無しのドメイン)を割り当てる方法は色々あります。あるアプリで「PointDNS」というHerokuのアドオンを利用してNaked domainを割り当ててみました。とても簡単に割り当てることができましたので、設定方法のメモと、名前解決にまつわるよちよち向けな話をブログに残しておきます。

PointDNS | Add-ons | Heroku

PointDNSの特徴

簡単にですが、PointDNSの特徴を書きます。

  • Herokuのアドオンとして提供
  • 1アプリ毎で1ドメインにつき10レコードまで無料で登録できる
  • Herokuアプリで必要となるレコードをまとめて登録できる機能がある
  • 管理画面からドメインのリクエスト数を確認することができる

1アプリ毎に無料で使えるところがとても魅了的です。1ドメインにつき10レコードまで登録できますが、NSレコードとSOAレコードのために6レコードが最初から登録されていますので、無料で自由に使えるのは4レコードまでだと思ってください。(もしかしたら、一部のNSレコードを削る方法があるかもしれません。)

Naked domainを割り当てるまでの手順

下記のものが必要になりますので、事前に用意しておいてください。

  • Herokuアカウント(クレジット情報を入力して、アドオンが利用できる状態にしておくこと)
  • Herokuアプリ
  • ドメイン(好みのレジストラからドメインを登録しておくこと)

アドオンを導入する

アドオンはHerokuコマンドを使って導入できます。(PointDNSのページから導入する方法もあります。)

$ heroku addons:add pointdns --app アプリ名

ドメインを設定する

Herokuアプリの管理画面上にPointDNSアドオンが追加されていますので、PointDNSのリンクをクリックして、PointDNSの管理画面に入ります。

f:id:Shindo_Masaya:20150121204403j:plain:w500

右上に見える「Domain name」のフォームにドメイン名(Naked domain)を入力して、「Add」ボタンを押します。その後、ドメインごとの管理画面に移動しますので、「1-click integrations」ボタン(下の写真の赤枠で囲んだボタン)を押します。

f:id:Shindo_Masaya:20150121210918j:plain:w500

モーダルウィンドウが表示されますので、Herokuのフォームに自分のHerokuアプリ名を入力して、「Add」ボタンを押します。

f:id:Shindo_Masaya:20150121212155j:plain:w300

下の写真のように、ALIASレコードとCNAMEレコードが自動的に追加されます。これでPointDNS側の設定は完了です。とても簡単でした!ところで、自動的に追加されたレコードを見て「CNAMEは知っているけど、ALIASは初めて見たよ。何これ?」と疑問を持つ方がいると思います。はっきりとした答えは見つかりませんでしたが、調べてみましたので、気になる方は「おまけ」の項目をご覧ください。

f:id:Shindo_Masaya:20150121212756j:plain:w400

後はレジストラドメイン登録業者)のサービスで、ドメインにPointDNSのネームサーバーを登録すれば、ドメインの設定は完了です。しばらく時間をおいてから、Naked domainでHerokuアプリにアクセスできるか確認してみましょう。

以上でHerokuアプリにNaked domainを割り当てる手順の説明は終わりです。

【よちよち向け】ドメイン名から目的のサーバーを見つけるまでの流れ

ネットワークについてあまり詳しくなくても「DNSという仕組みを利用して、ドメイン名から目的のサーバーのIPアドレスを探し出す」という漠然とした知識をお持ちの方は多いと思います。良い機会ですので、ドメイン名から目的のサーバーを見つけるまでの流れについて入門者レベルでまとめます。

サンプルストーリーがあった方が想像しやすいかと思いますので、私が作成した「ことばの探索機」というサービスのドメイン名である「kototan.info」にクライアントがアクセスするときの流れを例にします。

クライアントが「kototan.info」というドメイン名にアクセスするとき、まずは、「キャッシュDNSサーバー」というサーバーに対してドメインの情報を尋ねにいきます。キャッシュDNSサーバーは、ドメイン情報を調査するときの窓口になったり、過去に調査したドメインの情報をキャッシュを行ってくれます。「kototan.info」ドメインのキャッシュが残っていれば、それをクライアントに教えくれるだけで目的のサーバーが判明するのですが、今回はキャッシュが無かったことを想定して話を続けます。

キャッシュDNSサーバーにキャッシュが無かった場合、「ルートサーバー」というサーバーに対してドメインの情報を尋ねにいきます。ルートサーバーはドメイン名をドットで分割した際の最後の項目のドメイン情報を管理しています。ちなみに、この項目をトップレベルドメインTLD)」と呼びます。「kototan.info」ドメインの場合は「.info」がTLDになります。ルートサーバーはドメインの情報を尋ねられると、対象のTLDを管理している機関の情報を教えてくれます。ちなみに、この機関のことをレジストリと呼びます。

次に、キャッシュDNSサーバーはレジストリに対して「kototan.info」ドメインの情報を尋ねにいきます。お名前.comなどのレジストラ「『kototan.info』というドメインは『dns11.pointhq.com』というネームサーバーを利用しています」といった情報をレジストリのデータベースに登録しているはずですので、レジストリに「kototan.info」ドメインの情報を尋ねると、そのドメインに登録されているネームサーバーを教えてもらうことができます。

f:id:Shindo_Masaya:20150201224703j:plain:w500

ターミナルではdigコマンドを使うことで、ドメインに登録されているネームサーバーを知ることができます。

$ dig kototan.info. ns

; <<>> DiG 9.8.3-P1 <<>> kototan.info. ns
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17770
;; flags: qr rd ra; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;kototan.info.			IN	NS

;; ANSWER SECTION:
kototan.info.		3600	IN	NS	dns11.pointhq.com.
kototan.info.		3600	IN	NS	dns10.pointhq.com.
kototan.info.		3600	IN	NS	dns8.pointhq.com.
kototan.info.		3600	IN	NS	dns4.pointhq.com.
kototan.info.		3600	IN	NS	dns9.pointhq.com.

;; Query time: 131 msec
;; SERVER: 220.220.248.1#53(220.220.248.1)
;; WHEN: Sun Feb  1 16:21:10 2015
;; MSG SIZE  rcvd: 138

ネームサーバーが分かったら、キャッシュDNSサーバーはそのネームサーバーに対して「kototan.info」の情報を尋ねにいきます。ネームサーバーは各DNSサービスの設定画面で登録した通りの情報を返します。今回のケースでは、PointDNSの設定画面で「『kototan.info』は『kototan.herokuapp.com』というドメインと同じIPアドレスです」という情報を登録しましたので、ネームサーバーに「kototan.info」の情報を尋ねると、「kototan.herokuapp.com」というドメインIPアドレスを教えてもらうことができます。このように、ドメイン名からIPアドレスを見つけることを「名前解決」と呼びます。

ターミナルではhostコマンドを使うことで、名前解決を行うことができます。

$ host kototan.info dns11.pointhq.com
Using domain server:
Name: dns11.pointhq.com
Address: 119.81.97.170#53
Aliases:

kototan.info has address 54.243.85.192

目的のサーバーのIPアドレスを知ることができました。最後に、キャッシュDNSサーバーはこの情報をクライアントに返してあげます。このような方法でドメイン名から目的のサーバーを探し出していたのですね。

【おまけ】ALIASレコードとは

wikipediaDNSレコードタイプを調べても、ALIASというレコードタイプは見つかりません。alias(別名)と聞くと、CNAMEレコードのことが思い浮かびますが、ALIASレコードとは何なのでしょうか。ALIASレコードで登録した「kototan.info」と、CNAMEレコードで登録した「www.kototan.info」に対して、名前解決を行ってみます。

PointDNSで設定したレコード

f:id:Shindo_Masaya:20150201225254j:plain:w500

ALIASレコードに対しての名前解決

$ host kototan.info
kototan.info has address 54.243.85.192

CNAMEレコードに対しての名前解決

$ host www.kototan.info
www.kototan.info is an alias for kototan.herokuapp.com.
kototan.herokuapp.com is an alias for us-east-1-a.route.herokuapp.com.
us-east-1-a.route.herokuapp.com has address 23.23.170.22

CNAMEレコードとは違い、ALIASレコードはIPアドレスが一回で返ってきています。挙動はAレコードに似ていますね。気になります。この後、PointDNSのドキュメントを読んでみたのですが、探し方が良くないせいか、ALIASレコードの説明を見つけることができませんでした。

さらに色々と調べたところ、Amazon Route 53にもALIASレコードは存在するそうで、それについて説明されているブログが見つかりました。

Amazon Route 53のALIASレコード利用のススメ | Developers.IO

一言でまとめると、Amazon Route 53のALIASレコードとは「ALIAS先のドメインに名前解決を行い、そこで判明したIPアドレスを使って、Aレコードとして登録する」という独自拡張のレコードタイプなようです。PointDNSのALIASレコードがAmazon Route 53と同じ仕様かはわかりませんが、挙動だけを見るとそれに近い気がします。

もしAmazon Route 53と同じ仕様であれば、Naked domainにはCNAMEレコードよりALIASレコードを利用したほうが良さそうですね。

Rubyで文字列を連結するのにString#+を使うと、状況によっては遅くなる

試したRubyのバージョン: 2.2.0

文字列を連結するのにString#+を使わないこと

Rubyで文字列の後ろに別の文字列を破壊的に連結するには、String#+を使う方法と、String#concatを使う方法があります。

str = 'foo'
puts str += 'foo'
# => foofoo

str = 'bar'
puts str.concat('bar')
# => barbar

どちらも結果は同じなのですが、状況によってはString#+を使うと処理に時間がかかることがあります。具体的には、長い文字列の連結を行ったときにこの問題が起こります。

文字数が3万の文字列に対して連結

require 'benchmark'

n = 10_000
str_foo = 'foo' * n
str_bar = 'bar' * n

Benchmark.bmbm(8) do |x|
  x.report('+:')      { str_foo += 'foo' }
  x.report('concat:') { str_bar.concat('bar') }
end
# => 2回目の結果のみ抜き出し
#                user     system      total        real
# +:         0.000000   0.000000   0.000000 (  0.000014)
# concat:    0.000000   0.000000   0.000000 (  0.000006)

文字数が30万の文字列に対して連結

require 'benchmark'

n = 100_000
str_foo = 'foo' * n
str_bar = 'bar' * n

Benchmark.bmbm(8) do |x|
  x.report('+:')      { str_foo += 'foo' }
  x.report('concat:') { str_bar.concat('bar') }
end
# => 2回目の結果のみ抜き出し
#                user     system      total        real
# +:         0.000000   0.000000   0.000000 (  0.000165)
# concat:    0.000000   0.000000   0.000000 (  0.000006)

配列を連結するのにもArray#+を使わないこと

同じように、配列の後ろに別の配列を破壊的に連結する方法には、Array#+を使う方法と、Array#concatを使う方法があります。

ary = [1, 2]
p ary += [3, 4]
# => [1, 2, 3, 4]

ary = [1, 2]
p ary.concat([3, 4])
# => [1, 2, 3, 4]

こちらもArray#+を使うと処理に時間がかかることがありますので、Array#concatを使った方が良さそうです。

require 'benchmark'

n = 1_000
m = 100
ary1 = [*1..n]
ary2 = [*1..n]

Benchmark.bmbm(8) do |x|
  x.report('+:')      { m.times { ary1 += [1] } }
  x.report('concat:') { m.times { ary2.concat([1]) } }
end
# => 2回目の結果のみ抜き出し
#                user     system      total        real
# +:         0.000000   0.000000   0.000000 (  0.000169)
# concat:    0.000000   0.000000   0.000000 (  0.000024)

かなり細かいことだと思うのですが、String#+とArray#+の影響で1時間ほど無駄な待ち時間を生み出してしまったコードがありましたので、ブログに残しておきます。

Ruby2.2で新たに加わったitselfメソッドについて

2014年12月25日にRuby2.2.0がリリースされました。Rubyがアップデートされるたびに、新機能の紹介エントリを書いていたのですが、今回はitselfメソッドの追加が気になりましたので、このメソッドだけに絞ってエントリを書きます。

Kernel#itself

itselfメソッドはレシーバ自身を返すだけのメソッドです。Ruby2.2.0未満のバージョンでも、以下のように実装することで、itselfメソッドを使うことができます。

class Object
  def itself
    self
  end
end

実は、ActiveSupport4.2.0にもこっそりと追加されていますので、Rails4.2.0以降を使っていれば、Ruby2.2.0未満でもitselfメソッドを使うことができます。

ところで、このメソッドはどのような場面で使うのでしょうか。RailsAPIドキュメントを参考にして、ユースケースを考えてみました。

ユースケース

例えば、ActiveRecordを使って、あるモデルの全てオブジェクトを取得する処理があるとします。
基本的には全てのオブジェクトを取得してほしいのですが、ユーザーがオプションとして取得条件を付けたときだけは、その条件で絞り込みを行って欲しいとします。この場合、今までは以下のように実装していたかと思います。

# Model
class Entry
  scope :published, lambda do
    # 公開済み記事のみを取得する条件が書かれたスコープ
  end

  scope :drafted, lambda do
    # 下書き記事のみを取得する条件が書かれたスコープ
  end
end

# Controller
class EntriesController
  def index
    @entries = Entry
    # 「公開済み記事のみ」か「下書き記事のみ」で絞り込むように指定されているならば、絞り込みを行う
    if state.presence_in(%w(published drafted))
      @entries.public_send(state)
    end
    @entries.order(:created_at)
  end
end

このままでも複雑ではありませんが、itselfメソッドを使うことで、このようにコーディングできるようになりました。

# Model
# 先ほどと同じなので省略

# Controller
class EntriesController
  @entries = Entry.public_send(state.presence_in(%w(published drafted)) || :itself).order(:created_at)
end

いかがでしょうか。public_sendメソッドやsort_byメソッドなど、メソッド名を引数とするメソッドのデフォルト値としてitselfメソッドを使うと良いのではないでしょうか。

以上、itselfメソッドの紹介でした。

2015/01/22 記事修正

エントリ公開当初のコード。

# Controller
class EntriesController
  @entries = Entry.public_send(params[:state].presence_in(%w(published drafted)) || :itself).order(:created_at)
end

「presence_inメソッドで値をチェックしているとはいえ、paramsの値をそのままpublic_sendメソッドの引数に使うのは怖いのでは?」という意見をいただきまして、コードを修正しました。

Yochiyochi Committers Who's Who in 2014

この記事は「よちよちAdventCalender」の2日目の記事です。昨日の記事は『Precompile - じゃがいもエンジニアのブログ』でした。

毎年のRubyKaigiで、近永さんという方が、1年間の内でCRubyに対してのコミット数が多かった方々を紹介するCRuby Committers Who's Whoという講演をされています。とてもクールな講演で、「どこかで似たようなことをやってみたい」と思っていました。現在、よちよち.rbのmeetupsリポジトリにコントリビュータとして登録されているメンバーは55人います。多いですね。今回は「よちよちAdventCalender」というイベントに乗じて、『CRuby Committers Who's Who』の真似ごととして、よちよち.rbのリポジトリに対してコミット数が多かった方々の名前と今年の活躍ぶりを勝手ながら紹介していきます。

ルール

  • 2014年11月30日の段階で記録を取っています
  • 自分自身を紹介しても仕方がないので、自分はランキングから省いています

紹介

1位から順番に発表していきます。

1位 yucao24hoursさん

1位はご存知、よちよち.rbの主催者であるyucao24hoursさんです。「全員が初心者である」という今までにあまり無かった特徴を持つコミュニティを一から立ち上げ、毎週休まずに開催されています。その活躍ぶりから、よちよち.rbのRubyKajaに選ばれました。yucao24hoursさんの数多い活躍ぶりから1つだけをピックアップするならば、2013年の年末に行われた「よちよちプルリクエスト大会」の開催だと思います。「よちよちプルリクエスト大会」はよちよち.rbが始まって、まだ間も無い頃に開催されました。

「 Pull Request を送ってみたいんです!」
「普段は怖くてできなかったので、みんなで PR を試せたらいいな!」

yucao24hoursさんはそんな参加者の思いを感じ取り、一人で企画して準備し、開催まで話を持っていきました。私も参加者の内の一人だったのですが、そのカリスマ性に驚かされました。

2位 katorieさん

2位のkatorieさんを紹介します。katorieさんは「よちよち.rb」フィヨルドインターン「RubyKaigi2014」などを通じてコミュニティにコミットされています。katorieさんは第0回よちよち.rbからの参加者で、今もほぼ毎回参加しています。(もしかして、皆勤賞???)katorieさんの何よりも凄い所は、よちよち.rbの参加レポートを毎回欠かさずにブログに書いているところです。一度でもブログを書いたことがある人でしたら、ネット上に公開する文章を書くことがどんなに大変なことであるのか分かるかと思います。毎回欠かさずにブログを書き続けることができる情熱の元を教えていただきたいです。

katorieさんのブログはこちらです。
プログラミングお勉強記録

3位 ta1kt0meさん

3位のta1kt0meさんの紹介します。ta1kt0meさんはRubyKaigi2014のgihyo.jpレポート班を担当されていました。よちよち.rbの中のRubyKajaを決めるために、皆でRailsを使って「kajaeru」という投票サービスを作ったのですが、「kajaeru」という面白い名前はta1kt0meさんが付けられました。今は『エイゴ・ギジュツ・ドクショカイ MetaNight』というイベントを開催されています。ta1kt0meさんの活躍の中で紹介したいことと言えば、ブログだと思います。ta1kt0meさんは技術のキャッチアップがとても早く、新たに学んだことを優しい文章でブログに書きますので、読んでいてすごく面白いです。

そんなta1kt0meさんのブログはこちらです。
JanGaJan.com

4位 upinetreeさん

4位は第0回よちよち.rbからの参加者であるupintreeさんです。upinetreeさんの活躍・・・というよりは個人的に助けられたことなのですが、あえてマイナーなところから紹介したいと思います。

よちよち.rbでは開始して間も無い頃から、自己紹介ファイルをGithubで管理し、参加するたびに自分の自己紹介ファイルを更新してプルリクを送り、それを皆でレビューするという方法を行っています。Gitの使い方、特にコミットの粒度に関してはかなり細かくレビューが入りまして、だいたい「一つの作業に対して、一つのコミットを作る」という考えでレビューがされていると思っています。よちよちでのコミットの粒度の考えが面白かったので、僕は他のプロジェクトに参加したときにも同じ粒度でコミットしてみたのですが、メンバーから「こまめにgit commitすると、リポジトリが太ってロググラフが見え難くなるから止めてね」と言われてしまいました。そのことをよちよちで話したみたところ、よちよちメンバーの皆さんが色々な意見を出してくださったのですが、その中でも特にupinetreeさんは親身になって相談にのっていただきました。

(チャットの一部だけを引用)

個人的には一つのコミットに一つのことをするようなるべく心がけてます

(コミットが細か過ぎるとコミットメッセージを読むのに)多少は苦労しますが、それだけ内容があるPRということですからねぇ…むしろコミットメッセージにない修正が一緒に入っていた時のほうが混乱するかなと思います。その指摘の意図がどのレベルでまとめるかわからないのですが…

ある種チームの文化によるものだとも思うので、話しあって考え方を共有しておくと良いかもです

すごく嬉しかったです。個人的なことですが、紹介しておきたいことでしたので、あえて書かせていただきました。

5位 bonbon0605さん

5位のbonbon0605さんを紹介します。bonbon0605さんの活躍の中で紹介したいことは、一度よちよち.rbで発表してくださったLTについてです。bonbon0605さんがTokyuRuby会議07の抽選LT用として「よちよち.rbで知ったコミュニティの素晴らしさ」という内容のスライドを作られていました。このスライドの中で「抽選LTは怖いけど、ゆかおさん、おだいさん、松さんがいるし、行こうかなと思った」と僕の名前を書いてくださったことがすごく嬉しかったです。

bonbon0605さんのスライドはこちらです。
よちよち.rbで知ったコミュニティの素晴らしさ

6位 pupupopoさん

6位はpupupopoさんです。今回はコミット数で順位付けをしてしまいましたので6位という扱いになりましたが、参加回数は2位のkatorieさんとほぼ同じくらいだと思います。(2〜10位までのコミット数の差はほとんどありませんでしたので、順位はあまり気にしないでください。)pupupopoさんの活躍と言えば、よちよち.rbが始まってからちょうど1周年目にあたる第45回ミートアップのときに、会場係を担当されたことだと思います。素敵な会場を用意してくださり、ありがとうございました。

7位 to0526さん

7位は「ミスタードアラ」ことto0526さんです。to0526さんはよちよち.rb初参加者にとても優しい方です。to0526さんの活躍と言えば、11月に開催された開発合宿でカードゲーム『ニムト』を持ってきて、よちよちメンバーに流行させたことだと思います。僕も合宿帰りの電車の中でRuby版ニムトを作ることになるなんて、想像していませんでした。

8位 ryotashiraishiさん

8位は「うめぼし」ことryotashiraishiさんです。ryotashiraishiさんがよちよち参加され始めたのは、7月くらいからだったかと思うのですが、コミット数では上位にランクインされました。ryotashiraishiさんの活躍で紹介したいことは、11月のよちよちbeerで幹事を担当されたことです。よちよち.rbでは、ビールを飲みながら技術ネタとか普段興味があることとかを話し合って交流を深める「よちよちbeer」というイベントを月1回くらいのペースで開催しています。ryotashiraishiさんは、ある開発コンテストに応募する予定のアプリを開発している最中で、11月はとても忙しかったようですが、そんな忙しい中でもよちよちbeerの幹事を担当してくれました。ありがとうございました。

9位 highwideさん

9位はhighwideさんです。今回はmeetupリポジトリだけを見て順位を付けてみましたが、kajaeruリポジトリのコミット数で見ると2位に入っています。11月に開催された開発合宿にも参加されていて、そこで面白いサービスを企画し、開発に力を入れられていました。highwideさんの活躍と言えば、やはり先述のkajaeruだと思います。yucao24hoursさんやto0526さんが厳しいレビューを行う中で、何度もプルリクエストを送り、リリースまで頑張ってくれました。

10位 yuki3738さん

10位はyuki3738さんです。yuki3738さんはスマートニュースさんでよちよち.rbを開催するときの会場係を担当されています。とても優しい方で、僕が転職活動でうまくいかないことがあって憂鬱になっていたときに飲みに誘ってくれました。ありがとうございました。

他にも

今回はよちよち.rbのリポジトリに対するコミット数だけを見て上位10名の紹介をしてみましたが、よちよち.rbには他にも素敵な方々がいます。時間の都合により紹介できなくて、ごめんなさい。どんな素敵な方がいるのか気になりましたら、ぜひよちよち.rbに遊びに来てみてください。