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

WordでUnicodeコードポイントを直接入力する

$
0
0

文字を入力するとき、IMEの不確実な変換に頼らずに直接コードポイント入れたくなることはよくありますよね。

たとえば

  • U+2D(HYPHEN-MINUS) -
  • U+2012(FIGURE DASH) ‒
  • U+2013(EN DASH) –
  • U+2014(EM DASH) —
  • U+2015(HORIZONTAL BAR) ―
  • U+2500(BOX DRAWINGS LIGHT HORIZONTAL) ─
  • U+30FC(KATAKANA-HIRAGANA PROLONGED SOUND MARK) ー
  • U+00FD(FULLWIDTH HYPHEN-MINUS) -
  • U+FF70(HALFWIDTH KATAKANA-HIRAGANA PROLONGED SOUND MARK) ー

とか、IMEでうまく入力し分けて目視で違いを判別できたらカッコいいと思いますが、僕にはできません。
うっかり、直前に変換した履歴に引きずられて長音がダッシュになってたり。

Word 2003以降(ワードパッド含む)では、次の方法で、簡単にコードポイントを入力できます。

まずはコードポイントを16進数で入力します。「U+」は無くても良いです。

Alt + Xを押すと、文字に変換されました。

素晴らしく簡単ですね。エディタとして結構便利。

HTMLで同じことをする

この方法使う人も多いと思います。
適当なHTMLファイルを作って、以下のように文字参照を入力してブラウザで開く方法です。

―

Rubyで同じことをする

いつでもirbやpryが開いている人はこれもお手軽です。

[0x2015].pack('U')

# ブラケットを入力するのは地味にかったるいので、こっちの方が楽かも
0x2015.to_a.pack('U')

Rubyならついでに文字をコードポイントに変換するのが楽なので、やっぱりirbは常時起動に限ります。

# to_s(16)しないと10進数になってしまうので読みづらい
'―'.unpack('U').first.to_s(16)

# 1.9以降ならこっちでも良いです
'―'.codepoints.first.to_s(16)

Pythonで同じことをする

僕はpythoniaではないのですが、RubyよりPythonのほうがタイプ数が少ないので(・ω・)

u"\u2015"

文字をコードポイントに変換するのもpythonのほうが簡単です。

hex(ord(u'―'))

でも、strとunicodeで型が違うの気持ち悪いですよね。Rubyの方が可愛いですよね。

Python 3でも良いですが…


RailsでPostgresを使おうとしてはまった

$
0
0

こんにちは。ここ1年くらいで3回くらい開発環境を入れ替えているのですが、1度環境を構築してしまうと最初に設定した部分とかは忘れがちになるんですよね。
いつもRails開発するときはMySQLを使っているのですが、今回、Postgresを使うことになりインストールしたのですが、はまりました。。。
なので、備忘録としてやった作業を書いておきます。
ちなみにOSはUbuntu12.04です。

まずはPostgresのインストール

sudo apt-get install postgresql

Postgresのパスワード変更

sudo passwd postgres

PostgresSQL用の開発プラットフォームpgAdminをインストール

sudo apt-get install pgadmin3

データベース上のユーザのパスワードを変更する

su - postgres
psql template1
alter user postgres with password '○○○';

Postgresの再起動

sudo /etc/init.d/postgresql restart

さて、準備は整ったでは早速DBを作ってみる

bundle exec rake db:create

なんかエラー出た。
FATAL: Peer authentication failed for user “postgres”

調べてみる
結論としてはPeer認証が有効になっていたのが原因でした。
Peer認証が有効になっている場合は、ユーザ名とUnixユーザ名が一致している必要があります。

解決方法
/etc/postgresql/9.1/main/pg_hba.conf

local all postgres peer
の記述があります。
ここを
local all postgres md5
に直しましょう。

FactoryGirlでhas_manyな関係を定義する方法

$
0
0

FactoryGirl でhas_manyな関連を定義する方法が、ドキュメントをみただけでは分かりにくかったのでメモを残します

gemのVersionはそれぞれ
Ruby: 1.9.3-p392
Rails: 3.2.13
FactoryGirl: 4.2.0

サンプルとして科目(Subject)が複数の単元(Unit)を持っているというデータを作成します
Model

# app/models/subject.rb
  class Subject < ActiveRecord::Base
    has_many :units

    validates_uniquness_of :code

    attr_accessible :code, :name    
  end

# app/models/unit.rb
  class Unit < ActiveRecord::Base
    belongs_to :subject

    validates_uniquness_of :code

    attr_accessible :code, :name    
  end

Subject、Unitともに codeとnameを持っていて、codeの重複は禁止

ファクトリーの定義を作ってみます
ファクトリーの定義は spec/factories.rb というファイルに記述します
spec/factories.rb

# coding: utf-8
FactoryGirl.define do
  factory :english, class: Subject do
    code "subject_01"
    name "英語"
  end

  factory :grammar, class: Unit do
    association :subject, factory: :english
    code "unit_01"
    name "文法"
  end

  factory :idiom, class: Unit do
    association :subject, factory: :english
    code "unit_02"
    name "熟語"
  end
end

spec/model/sample_spec.rb

describe 'sample' do
  it 'Subject:英語 が作成できる' do
    english = FactoryGirl.create(:english) # 無事作成されました
    english.should_not be_nil # 結果はgreenです
  end
  it 'Unit:文法、熟語 が作成できる' do
    FactoryGirl.create(:grammar) # 無事作成されました
    FactoryGirl.create(:idiom) # uniquness制約によるエラーが出ました
    Unit.all.should have(2).items # 「熟語」にあたるUnitが作成されなかったので結果はredになります
  end
end

うまくいきませんね
Subjectのcodeが重複してしまいます
今の定義だとUnitを作成する時点で毎回Subjectを作成しようとするようです
Subjectを複数回作成してしまうと、code(subject_01)が重複するのでエラーになりますね

重複を回避するには、Sequencesという機能が使えます
これは自動的に連番を生成してくれる機能なので
code1 code2 code3… と重複しないcodeを設定することが出来ます

Subjectの定義を変更してみます

  factory :english, class: Subject do
    code sequence(:code) {|n| "subject_#{n}" }
    name "英語"
  end

今度は成功しました
しかし、今の状態ではUnitがそれぞれ別のSubjectを持ってしまっています
SubjectとUnitが1対1(has_one) という関係に近い状態ですね
一つのSubjectが複数のUnitを持っているという状態(has_many)を実現できていません

同じSubjectを参照するようにするためには、テストコード側で対策が必要です
FactoryGirlは定義されたattributesをoverride する機能があるので、
Unit作成時にSubjectのインスタンスを渡すことで同一のSubjectをセットすることができます

      it 'Unit:文法、熟語 が作成できる' do
        @english = FactoryGirl.create(:english)
        FactoryGirl.create(:grammar, subject: @english)
        FactoryGirl.create(:idiom, subject: @english)
        Unit.all.should have(2).items
      end

これで、文法と熟語が同じSubjectに関連する状態になりました
ファクトリーの定義だけでこれと同じことが出来ると良いのですが、やり方が見当たらなかったのでこうなっています
ファクトリーの定義だけで完結する書き方を知っていたら教えて下さい

ActiveMerchant を使ってPayPal Express Checkout の与信取得と回収機能を導入する

$
0
0

どうも、shibuso です。前回「PayPal のSandbox に挑んでみた」を書きましたが、今回はRuby のライブラリ「ActiveMerchant」を用いてPayPal Express Checkout に対応した大まかな流れをまとめてみたいと思います。GitHub にあるActiveMerchant のWiki からリンクが張られているCody Fauser – PayPal Express Payments with ActiveMerchant (以降Cody Fauser と呼びます)を参考にました、順番も基本はこれと同じです。

この記事を読むにあたって、前提条件としてある程度Rails が使えることと、PayPal Sandbox 環境が整っていることが求められます。また、今回の実装は即時課金を行うのとは少し違う「与信取得→後日回収」という実装を行なっていますが、こうしたPayPal の最低限の仕様理解も必要です。なお、PayPal の解説ページも所々説明で活用します。また、実装環境はRails 3.2.12, ActiveMerchant 1.31.1 です。

では始めましょう。まずActiveMerchant のgem を入れます、ここはActiveMerchant のReadme と同じです。続いてCody Fauser と同じくconfig/environments/development.rb にSandbox を使用するように指定します。

# config/environments/development.rb
config.after_initialize do
  ActiveMerchant::Billing::Base.mode = :test
end

更に日本円を扱う場合は、通貨を設定します。config/environment.rbに下記を記述しました。

# config/environment.rb
ActiveMerchant::Billing::PaypalExpressGateway.default_currency = "JPY"

続いてgateway の準備をします。Cody Fauser では使用するクラス内に記述していましたが、私は複数箇所で使用する想定だったのでモデルに書きました。ここで必要な情報は全てPayPal のSandbox で取得出来ます。

# app/models/paypal_transaction.rb
class PaypalTransaction < ActiveRecord::Base
  include ActiveMerchant::Billing

  # 省略

  def self.gateway
    @gateway ||= PaypalExpressGateway.new(
      :login => username,
      :password => password,
      :signature => signature
    )
  end
end

ここまでで準備完了です。以降の実装はPayPal とのやり取りが発生します。PayPal の解説ページの「STEP 2. 導入・ご利用方法(導入イメージ)」の図が大まかな動作順を表しています。ユーザから見たら以下の図ような流れになります。
実行順序

それではこれからA. SetExpressCheckout にあたる部分を実装します。この後は基本的にコントローラでの実装です。

# app/controllers/payment_controller.rb
def checkout

  # 省略

  setup_response = PaypalTransaction.gateway.setup_authorization(
    price,
    :ip                => request.remote_ip,
    :return_url        => url_for(:action => 'confirm', :only_path => false),
    :cancel_return_url => url_for(:action => 'cancel', :only_path => false),
    :items             => items
  )

  paypal_transaction.token = setup_response.token
  paypal_transaction.save!

  redirect_to PaypalTransaction.gateway.redirect_url_for(setup_response.token)
end

上記でprice は日本円を指定した場合、数値を100 倍する必要があるようです。return_url とcancel_return_url はそれぞれ用意して下さい。items には様々な情報が入ります。gem の中身を見ると確認できます、場所はactive_merchant/billing/gateways/paypal/paypal_common_api.rb のadd_payment_details_items_xml メソッドです。

redirect されるとユーザはしばらくPayPal の画面での処理になります。次にこちらに返ってくるのはPayPal 上でログインしたり支払いの意思を明確にした後です。呼ばれるのは先程return_url で指定したページなので、次はその実装についてです。

# app/controllers/payment_controller.rb
def confirm
  if params[:token].blank?
    # エラー処理
  end

  details_response = PaypalTransaction.gateway.details_for(params[:token])

  if !details_response.success?
    # エラー処理
  end

  # 省略
  # params[:token] と同じtoken を持つレコードを探す(先程token を保存したレコード)、無い場合はエラー処理

  paypal_transaction.payer_id = params[:PayerID]
  paypal_transaction.save!

  # details_response から必要な情報を修得
end

ここでは各種情報の確認や保存を行います。payer_id はしっかり保存しましょう。また、このコントローラに対応したビューでは支払いの最終確認を行うので、ユーザに確認してもらうために必要な情報を表示できるよう取得する必要があります。

この次、ユーザが確認ボタンを押した時に実行する処理が重要です。「与信取得→後日回収」を実装する場合は以下のようになります。

# app/controllers/payment_controller.rb
def complete

  # 省略
  # 各種エラーチェック等

  authorize = PaypalTransaction.gateway.authorize(
    price,
    :ip       => request.remote_ip,
    :payer_id => paypal_transaction[:payer_id],
    :token    => paypal_transaction[:token]
  )

  if !authorize.success?
    # エラー処理
  end

  paypal_transaction.authorization_id = authorize.authorization
  paypal_transaction.save!
end

Cody Fauser ではpurchase になっている部分がauthorize に変わっていることが確認できると思います。ここをauthorize にすることにより、即時課金ではなく与信取得状態となります。authorization_id は重要なのできちんと保存して下さい。

この後は、いつ与信を回収するかで実装が変わります。PayPal は3 日以内ですと残額が保証されているのでそのまま回収することができますが、それを過ぎると再与信する必要があります。状態によっては再与信の取得に失敗するので、それを想定した実装が必要になります。また、再与信が可能なのは1 回だけです。以下は回収するメソッドです。

# app/models/paypal_transaction.rb
def self.exec_capture

  # 省略

  if 前回与信した日から3 日以上経過していた場合
    reauthorize = PaypalTransaction.gateway.reauthorize(price, paypal_transaction.authorization_id)
    if !reauthorize.success?
      # エラー処理
    end
  end

  capture = PaypalTransaction.gateway.capture(price, paypal_transaction.authorization_id)
  if !capture.success?
    # エラー処理
  end
end

reauthorize で再与信取得となります。その後capture することにより回収完了です。

おまけとして、与信を取得したけれどキャンセルする場合は以下のような実装で可能です。

# app/models/paypal_transaction.rb
def self.exec_reject

  # 省略

  rejected = PaypalTransaction.gateway.void(paypal_transaction.authorization_id)

  if !rejected.success?
    # エラー処理
  end
end

void を実行することにより回収をキャンセルすることができます。PayPal のSandbox では「拒否」というステータスになります。

最後に。上記の実装は2013 年4 月現在で確認したものです。今後使用変更等で動作しなくなる可能性もあるので、その際は適宜修正して下さい。以上、PayPal Express Checkout の与信取得と回収機能の導入解説でした。

関連記事:PayPal のSandbox に挑んでみた

PHPとRubyを徹底比較!開発効率をあげて収益を増やす

$
0
0

弊社ではRubyというプログラミング言語を使ってシステムを開発しています。そして車輪の再発明を回避しつつ開発効率を最大限引き出すためにRuby on Railsというフレームワークを活用してます。別にRuby以外触れないわけではないのですが、腰を据えて社内のノウハウを構築していくことが組織力を高める要因になるのでそうしています。ただスタッフはそれぞれあいた時間やプライベートで別の言語やフレームワーク、技術を積極的に探しては社内に共有をかけてくれるため、1年度には別の言語・フレームワーク・技術体系を採用しているかもしれません。

現時点でRuby on Railsを毎日活用していて、お客様や同業他社に推薦しまくっているため、「Rubyっていいの?みんなPHP使ってるみたいだけど。」といった質問や相談を頻繁に受けます。お客様からはほぼ100%聞かれる内容なので実際この業界にいる方々以外にとっては意外と情報が少ないのかもしれません。そんな方々のために比較を下記にまとめてみました。普段口下手なのでお会いする前にサイトを見ていただいたり、そもそもこういった情報が広がってくれると嬉しいなと感じています。

まずは主にプログラミング言語の面から比較してみます。手段としてどちらを採用した方がいいかは目的が何かで答えが異なるので、最後にまとめます。

比較対象

Webシステムでは、サーバOSにはLinuxが、プログラミング言語には俗にLL(LightweightLanguage)と呼ばれるerl/PHP/Ruby/Pythonなどのスクリプト型言語が多く利用されます。サーバOSとしてWindowsServer、言語としてJavaなども利用されますが、一般的に開発工数が激増するため、スピード重視のWeb業界ではLinux+スクリプト言語が主流です。

今回は、国内でWebシステム開発によく使われている、PHPとRubyを比較してみることにします。
(Perlは新規開発での採用が減っていること、Pythonは研究用途を除くと国内での採用事例が比較的少ないことから、今回は対象外にしました)

PHPによるWeb開発の選択肢

PHPはもともとWebのために開発された言語で、簡単なWebページから本格的なWebシステムまで、非常に多く採用されています。PHPでは、多数のフレームワークが存在し、フレームワークを利用しない(俗に「生PHP」と呼ばれる)開発も広く行われていることが特徴です。参考までに一般的に採用されているフレームワークや開発スタイルはこちら。
http://www.phpframeworks.com/top-10-php-frameworks/

Yii、CodeIgniter、CakePHP、Zend、Symfonyなどのフレームワーク

日本で多く採用されているフレームワークの特徴として、始めるための敷居が低く、設置が簡単、があります。特にCakePHPはXAMPPなどのお手軽LAMP環境とも相性がよく、WEBアプリ開発の経験しかなくても、またデザイナであっても手軽にWEBシステムを開発できるのが最大の特徴であり利点です。受託開発のうち、小規模またはキャンペーンなど期限付きの作りきりのシステムを、多少の保守性を残しながら開発するときには良い選択肢です。

生PHPでの開発

小規模システムを中心に、フレームワークを利用しない開発も広く行われています。デザイナーが制作したHTMLに、1ページだけ問い合わせフォームをつけるなどの用途には相変わらず有効です。ただし、当初の予定を超えて開発規模が大きくなり、破綻するという流れも良く見受けられます。

PHPでの開発によるメリット

開発者の数が多い

始めるための敷居が低いため、「PHPプログラマー」で募集をかけると、他の言語に比べて非常に多く集まりやすいです。人員確保しやすいのは大きなメリットです。ただし、あくまで敷居が低いことによるため、一定レベル以上の人材の数は、他のメジャー言語と同程度と思われます。

日本語の情報が多い

ドキュメント類が豊富なうえ、その多くが丁寧に日本語翻訳されています。そのため、英語にアレルギーがある開発者でも情報収集しやすくなっています。

導入の敷居が低い

最近は安価なレンタルサーバでもPHPが組み込まれているため、「納品されたソースコードをFTPで置けば動く」「コマンドラインを触らなくても一通りのことができる」のは、サーバ管理スキルが不要という点で大きなメリットです。生PHPはもちろん、CakePHPもこの点で多く支持されているように見受けられます。
※YiiやSymfonyのような本格的なフレームワークでは、バージョンやライブラリ要件などが厳しくなってきており、このメリットは享受しにくくなります。

PHPが向いている開発

デザイナーとの分業が重要な体制

基本的にHTMLで構成し、一部必要なページのみをPHPで出力するといった構成が特に工夫無く実現できます。キャンペーンサイトなどで、基本的にデザイナーのみでデザインからHTMLまでを制作し、一部に問い合わせフォームをつけるなどの場合では、分業化の敷居が低いことがメリットになります。

超小規模開発

画面数が極端に少なくデータベースも不要なシステムの場合、高度なフレームワークの生産性よりもスピード重視で作りきるという割り切りに適した手段が多いです。

共用サーバーで動かすことが要件の場合

PHP、特に生PHPやCakePHPでは、レンタルサーバーでも多くの場合設置するだけで動きます。諸事情で共用サーバにインストールが必要、追加ソフトウェアのインストールが禁止されているなどの状況では、この点がメリットになります。
なお、システムは「置いて終わり」でもしばらくは稼働しますが、メンテナンスを怠ることはおすすめできません。どのような環境でもセキュリティアップデートは日々更新されており、追従しなれば大きな問題を引き起こす可能性があります。

RubyでのWeb開発

RubyでWeb開発と言えば、Ruby on Railsフレームワークを使うのが主流です。

デザインからインフラまでをワンストップで

Rails開発では、デザイナー/HTMLコーダー/プログラマ/DBエンジニア/サーバエンジニアのような過度の分業をせず、各開発者がほぼすべての工程を担当するのが普通です。開発者に求められる基本スキルセットが高くなる傾向はありますが、要求を実現するために最適な方法を分担にとらわれず追求することができます。

最新技術・スタイルを常時取り入れる

Rails開発は、良くも悪くも工業生産的にはなっていません。テストやデプロイの自動化、より効率的なデザインフレームワークの採用などにより、ルーチンワークを減らし、常にビジネスロジックの開発に注力できるようになっています。

Rubyでの開発によるメリット/デメリット

[メリット] ノウハウの集約

RubyでのWeb開発と言えばRailsなので、ノウハウがRailsに集結します。言語では無くフレームワーク単位で見た場合に、Railsの人気は圧倒的です。開発者数の多さは、多数のライブラリによる開発効率の向上、安定性の向上など多くのメリットをもたらします。

[メリット] 圧倒的な開発効率

Railsはその登場当時から一貫したポリシーによりアップデートを重ねており、現在ではあらゆるWebシステムへ対応できる基盤になっています。ノウハウの蓄積により、他の追随を許さない開発効率を実現しています。

[デメリット] 導入の敷居の高さ

Railsはその万能性ゆえ、開発者にとってやや敷居が高く、国内でのプログラマー数はPHPと比べると少なめになります。チームメンバーとして参加するにもサーバの基礎知識やコマンドライン操作が必要になるため、工業的な分業もしにくく、必要な時に人員を追加するのに苦労することがあります。またサーバ環境への要求も高く、共有レンタルサーバーなどでは動作させられないことも多いですが、この点は昨今のクラウドサービスの普及により大きな問題にはならなくなってきました。

Rubyが向いている開発

継続的に開発し続けるシステム

システムは生き物です。状況の変化に応じて日々変化を続けていくようなシステムには、Ruby/Railsの効率性と柔軟性が大きな武器になります。特に、立ち上げ時には規模の読めないスタートアップサービスには適しています。
逆に、当初の仕様で変更無く作りきり、いったん完成したら二度と更新しないようなシステムだとしたら、Railsのメリットはあまり享受できないでしょう。バージョンアップが速く、旧バージョンのサポート停止などに対応するコストのほうが高く付いてしまう可能性があります。

で、どんなシステムにPHP/Rubyは向いてるの?

上記の事実をもとに、ぼくの主観で振り分けていきます。弊社にはRuby on Rails経験の豊富なエンジニアがいるため、採用しにくいことや開発環境の準備にコストがかかる部分があまりないため、多少バイアスがかかっているかもしれません。

社内業務システム

社内にオールラウンドなエンジニアがいれば(Ruby経験不問)、基盤部分だけ発注して、納品後に機能を足していくことをおすすめします。社内システムは常に要望が変化するため初期のデータ設計部分に携わり、基盤部分だけはしっかりと良い物を作っておき、その後何年もかけて自社の要件にあわせて開発を継続していくことが最も安価でリスクの少ない選択肢とおもいます。社内にそういったエンジニアがいない場合、採用できない場合、または継続的に開発していうことが面倒などの理由で実現できない場合、ぜひ弊社を検討ください。既にあるものをさくっとRuby on Railsシステムに作り変えることも可能です。

自社で運用するWebサービスの場合

社内業務システムのように初期の最も大きな要件が変化しない・また社内で継続的に運用していくような類のものであれば、Ruby on Railsはとても良い選択肢です。逆に、そこまで要件・機能が多くないサービスのプロトタイプを開発するような場合は、すこし注意が必要です。作ってもすぐ全部壊して作りなおす可能性がある場合(例:プロトタイピングして要件を洗い出す場合など)はスピード重視かつ保守性を捨てたほうが良い場合があるため、PHPも良い選択肢と言えます。機能の量でフレームワークを活用するかどうかを決めれば良いかと思います。

キャンペーンなど期限付きのサイト

キャンペーンが終われば正常かどうする必要がないなど、保守性をある程度無視して開発できるものはPHPやアリモノのシステムを採用すべきです。PHPはツールや関連ソフトが無料な場合が多くスモールスタートする場合に適しています。

Excel で楽にSQL やseed を管理する方法

$
0
0

Rails + PostgreSQL の組み合わせが多いなと最近感じているshibuso です。Heroku もこの組み合わせですしRails 界隈では流行りなのでしょうか、個人的にはMySQL の方が長く触れてきたので好きなのですが。とは言えどちらもSQL です。今回はExcel を用いてテストデータがいくつか必要になった時にSQL を手軽に生成したり、管理する方法を紹介したいと思います。

この記事を読んで幸せになれそうなのは、プログラマ及びデータベースでデータを管理している方(ソーシャルゲームでのプランナーに当たるような方)です。ただしこれを読むにあたって、最低限のSQL を書ける知識が必要になります。SQL を書けない方は学習するか書ける方に手伝ってもらいつつ進めると、きっとそのうち書けるようになります、いつか、多分。それに加えて少しだけExcel を理解している必要があります。

実用例

いきなり「テストデータを作る」「データを管理する」と言われてもなかなか想像できないと思うので、欲しいデータとSQL の例を先に出します。

例1. 一貫性があるデータを大量に作る

テストをする際に、殆ど同じデータで構わないものの、見分けを付けたいために少し違うデータを用意したい場合があったりします。例えばメールを送るテストがあるとして、gmailを使えば「ユーザ名+適当な文字列@gmail.com」みたいなフォーマットで送信可能です。この「適当な文字列」の部分を楽に用意することができます。

excel_sql_shibuso excel_sql_shibuso excel_sql_shibuso
sample+1 sample+2 sample+3

あるいはゲームで言えば「モンスター1」「モンスター2」のように、とりあえず見分けを付けたい場合があります。

excel_sql_monster excel_sql_monster excel_sql_monster
モンスター1 モンスター2 モンスター3

そんな時のSQL 文は以下のようになります。(テーブルのCREATE 文は適当にご用意下さい)

-- SQL
-- 送信しないようにわざと「@gmail」としています
-- しかしRails + Devise のような環境ですとvalidate に失敗するのでご注意下さい
INSERT INTO users (id, email) VALUES (1, 'sample+1@gmail');
INSERT INTO users (id, email) VALUES (2, 'sample+2@gmail');
INSERT INTO users (id, email) VALUES (3, 'sample+3@gmail');

INSERT INTO monsters (id, name) VALUES (1, 'モンスター1');
INSERT INTO monsters (id, name) VALUES (2, 'モンスター2');
INSERT INTO monsters (id, name) VALUES (3, 'モンスター3');

例2. マスターデータを管理する

運用側が用意し必要がある時以外は更新を行わない、いわゆるマスターデータを管理する事も多々あると思います。

例えば全都道府県の名前は、私がこれまで生きてきた中で変更はありませんでした。しかし例えば「大阪府」が「大阪都」になるかもしれません。こういう情報の管理も楽に行えます。

SQL 文は以下のようなものを想定しています。

-- SQL
INSERT INTO prefectures (id, name) VALUS (1, '北海道');
INSERT INTO prefectures (id, name) VALUS (2, '青森県');
INSERT INTO prefectures (id, name) VALUS (3, '岩手県');

UPDATE prefectures SET name = '北海道' WHERE id = 1;
UPDATE prefectures SET name = '青森県' WHERE id = 2;
UPDATE prefectures SET name = '岩手県' WHERE id = 3;

実現方法

勘がいい人はもうここまでで実現方法が想像できているかもしれませんね。カラム名、値をそれぞれ決まったセルに入力し、最後にそれらをまとめてSQL を書き出す式を作ればいいのです。

excel_sql_excel1

上の画像は一番最初に挙げた例を実際にExcel で管理してみたものです。「=」で書き始めるとそれが式だと判断されますが、その後に「”」を付けることで文字列として扱ってくれます。これと途中途中に参照式を組み合わせることにより、SQL 文を作り出すことが可能です。書いた式は以下のとおりです。

="INSERT INTO "&$B$1&" ("&$B$2&", "&$C$2&") VALUES ("&B3&", '"&C3&"');"

注意すべき点としては

  • テーブル名やカラム名は絶対参照にする
  • カラムが文字列を扱う場合は「’」を前後にはさむ
  • NULL やNOW() 等SQL 文に直接書きたいものはセルを参照せずにそのまま書き込んでしまうのも手

ぐらいでしょうか。上記の例ではINSERT 文を書きましたが、UPDATE 文にする場合は式の形が変わるものの、基本的にやることは同じです。

Excel を利用する利点としては、コピペが簡単にできることと、値を少しずつ増やすことが容易な点です。上記の図でのB4 は、普通に「2」と書いても良いですが、「=B3+1」と式を書くことも可能です。そしてそれを下にコピーしていくとどんどん数字が増えていきます。正に手間いらず。

式を書かなくても、セルの右下を引っ張ってオートフィル機能を使うのも便利です。C3 に当たる「sample+1@gmail」を書けば、その後は「sample+2@gmail」「sample+3@gmail」…と好きなだけ書けます。テストデータを大量に用意するのはもちろん、マスターデータを作る時にも活用できます。

おまけ1:seed を生成する

無理矢理感が少しありますが、書き換えればRails で利用するseed も書き出せます。例えば都道府県のデータを作ろうとします。

# seed
# 冪等を意識して書いています
prefecture_data = [
  { id: 1, name: '北海道' },
  { id: 2, name: '青森県' },
  { id: 3, name: '岩手県' },
# あとは略
]

prefecture_data.each do |d|
  next if Prefecture.find_by_id(d[:id]).present?
  h = Prefecture.new
  d.each do |k, v|
    h.send("#{k}=", v)
  end
  h.save!
end

このようなものが必要になった時に、「{ id: 1, name: ‘北海道’ },」の部分を書き出すように、先程の要領で書き換えればいいのです。

マスターデータはもちろんSQL を流すことで対応可能ですが、seed を用いることのメリットとしてはRails のvalidate を通すことができるということがあります。validate を通すことでデータに不整合があった場合に気づくことが可能です。システムが意図していない値でもSQL 上問題ない場合があります。例えば最初に紹介したメールアドレスの例もSQL としては問題がありませんが、システムとしておかしいです。validate が通らないということで気づくことが可能です。

excel_sql_excel2

おまけ2:複数のテーブルと連携する

例えば1 シートに1 テーブルという管理方法をとれば、複数のテーブルを1 ファイルで管理することが可能です。また、別シートも参照することが可能なので、関連するテーブルの値を参照することも可能です。これにより関連テーブルを簡単に再現することが可能です。

Googleさんから読み取るPHPとRails開発の比較

$
0
0

渡辺さんPHPとRubyを徹底比較!開発効率をあげて収益を増やすという記事を出していたので,反応してみます.

この記事は,PHPとRails開発について,PHPはメジャーでRailsは開発者が少ないとは言うけど,証拠はあるのかよ!とか具体的にどれくらいの開発者数の差があるの?という質問にデータを用いて答えてみます.
PHPとRails開発の得意分野やメリット・デメリットについては前述の記事に良くまとまっていますので,そちらを参照すると良いです.

情報ソースについて

今回の比較にはGoogle検索のヒット数,及びGoogleトレンドを利用しました.Google検索は今までの蓄積された情報の累計,Googleトレンドは今流行っているかどうか(勢いがあるか)という指標としてある程度の信頼性があるのではないかと思います.
「ヒット数==開発者の数」というのは多少乱暴ですが,それなりの指標にはなるのではないかと思います.

PHP v.s. Rails

では,PHPとRailsを比較してみましょう.まずは「〜〜開発」「〜〜 development」で調べてみました.

Google検索

- “PHP開発”: 41,500,000
- “Rails開発”: 6,730,000

Screen Shot 2013-05-23 at 18.25.20

- “PHP development”: 279,000,000
- “Rails development”: 62,500,000
Screen Shot 2013-05-23 at 18.18.31

Googleトレンド

日本語

英語

・・・うーん,Railsさんが息をしていないようです.PHP v.s. Railsの結果はどう見てもPHPの圧勝と言えるでしょう.Google検索で4〜7倍,Googleトレンドでもやはり7倍程度の情報量の差があるようです.
Googleトレンドから読み取れる他の情報としては,PHPはインドの情報が多いのに対し,Railsはアメリカとインドの情報が多い様です.

これは僕の主観になりますが,実際PHPとRailsという軸で見てみると,国内・海外問わずPHPの案件やサイトの方が多いです.有名どころではFacebookのような超大規模サイトでもPHPは採用されており,まだまだ一線で使える現役言語であることが分かります.

だがしかし

しかし,ちょっと待って下さい.PHPはプログラミング言語,RailsはWebアプリケーションフレームワークです.種類の違う二つのものを比べるのはフェアじゃないですね.
軸を合わせるために,PHPのWebアプリケーションフレームワークとRailsで比較してみましょう.

PHPフレームワーク v.s. Rails

ここでは,3大メジャーPHPフレームワークのSymfonyZend FrameworkCakePHPとRailsを比較してみます.

Google検索

- “Symfony”: 7,060,000
- “Zend Framework”: 5,430,000
- “CakePHP”: 6,470,000
- 上記3フレームワークの合計: 18,960,000
- “Rails”: 93,400,000

Screen Shot 2013-05-23 at 19.33.24

Googleトレンド

 

・・・いかがでしょうか?先ほどとは打って変わってPHPフレームワーク涙目のRails圧勝の結果となりました.
Google検索では個別フレームワークで見て13倍以上,PHPの3フレームワークの合計と比較しても5倍以上の開きがあります.また,Googleトレンドでも9倍程度の開きがあることが見て取れます.

考察など

これらの結果をまとめると,PHPでの開発Railsの開発ではPHPが圧倒的に多い一方,フレームワークを使うという条件を付けた場合においてはこの順位が逆転するということがわかりました.
ここで気になるのは,PHP開発におけるフレームワーク利用率です.世の中のPHPプログラムのうち,どの程度の割合がフレームワークを使った開発をしているのでしょうか?

PHP : Rails = 7 : 1
PHPフレームワーク : Rails = 1 : 5

という結果から,単純に計算すると,

PHP : Rails : PHPフレームワーク = 35 : 5 : 1

という計算結果が導き出せます.
つまり,世の中のPHPプログラムのうち,フレームワークを使った開発は1/35程度という仮説が導き出せるわけですね.
もしこれが単純に開発者数と比例すると仮定した場合,PHPプログラマの中でフレームワークを扱えるエンジニアは35人に一人ということになります.
一般的な小中規模Web開発でプログラマを35人も雇うことは無いので,なかなか厳しい割合といえます.また,上記の割合は3種類のフレームワークの合計と比較しているので,実際に任意のPHPプログラマ集合から同じフレームワークを使っている人を見つけるには,さらに難易度が高いことになるわけですね.

一方,RailsプログラマはRails自体がフレームワークなので,全員フレームワークを扱えます.Railsは比較的規約の厳しいフレームワークのため,Railsの規約にさえ従っていればある程度のコードの質は自動的に担保されます.
また,Railsエンジニアは,上記の式からPHPフレームワークの使えるPHPエンジニアの5倍の人数がいると考えられますので,フレームワークを使った開発が必要な場合はPHPよりRailsの方が開発に向いているのではないか,という結論になります.

終わりに

今回使ったGoogle検索とGoogleトレンドを使った比較は相当乱暴な方法ですが,PHPの世界で未だにフレームワークを使った開発がメジャーではないということを証明することはできたと思います.
スクリプトベタ書きによるプログラミングは敷居が低く,小さいコードならコピペが容易ですが,後々システムが大きくなればなるほど負の遺産としてシステムにのし掛かってきます.

こちらの記事にあるように,フレームワークを使うのに適した開発を行う場合は,PHPにこだわらずRailsを採用するのを考えてみるのも良いのではないでしょうか.

僕はPHPフレームワークの世界からRailsに移行した身として両方の世界を知っていますが,今のRails開発は一度経験するともうPHPには戻れない位の勢いをひしひしと感じます.これからきっちりとしたWeb開発を勉強する人は,Railsを検討してみるのも良いと思いますよ.

おまけ

社内で見せたら「PHP v.s. Rubyはないのか?」と言われたので測ってみました.
※計測した時期が異なるので上記と結果が多少違います.

Google検索

日本語
- “PHP開発”: 36,400,000
- “Ruby開発”: 18,700,000
Screen Shot 2013-05-23 at 20.16.44

英語
- “PHP development”: 257,000,000
- “Ruby development”: 109,000,000
Screen Shot 2013-05-23 at 21.15.02

Googleトレンド

日本語

英語

というわけで,言語レベルで見るとGoogle検索では国内で約2倍,英語圏で2.5倍の開きがあるようです.日本の方が言語自体の普及度が高いのは,日本製ということもあるんでしょうね.
Googleトレンドでみると,海外では7倍近くの開きがあり,PHPにはやはり遠く及ばないですね.

Rubyでプログラミングするのは楽しいので,もっと普及するといいな.

Arel::Tableを使ってなるべく生のSQLを書かずに済ます方法

$
0
0

Railsでプログラムを書いるとSQLを直接記述する機会が意外と多いので、
なるべくRubyらしく書く方法がないか調べてみました。
ORなどかなり基本的な構文でもarel_tableを使う必要があるのですね。
というわけで、Arel::Table を使ってみました。

動作確認環境は以下の通りです。
ruby: 1.9.3-p392
Rails: 3.2.13
arel: 3.0.2
MySQL: 5.1.65

検索条件をORで繋げたい

基本的な構文ですがSQLを直接記述する場合が多いのではないでしょうか?

User.where("name = ? OR name = ?", "太郎", "花子").to_sql

#=> "SELECT `users`.* FROM `users`  WHERE (name = '太郎' OR name = '花子')"

なるべくRubyで書こうとするとこうなります。

user_table = User.arel_table

User.where(user_table[:name].eq("太郎").or(user_table[:name].eq("花子"))).to_sql
#=> "SELECT `users`.* FROM `users`  WHERE ((`users`.`name` = '太郎' OR `users`.`name` = '花子'))"

ActiveRecordに orメソッドを取り込んで欲しいというリクエストについて議論もされているようなので、
今後はもっと簡単に書けるようになるかもしれません。

Arel::Tableで使えるメソッド

サンプルコードに eq というメソッドが出て来ましたが、これは ‘=’(イコール)に変換されます。
他によく使うものでは不等号がありますが、これは以下のメソッドが対応しています。

user_table = User.arel_table

User.where(user_table[:age].lt(2)).to_sql
#=> "SELECT `users`.* FROM `users`  WHERE (`users`.`age` < 60)"

User.where(user_table[:age].gt(2)).to_sql
#=> "SELECT `users`.* FROM `users`  WHERE (`users`.`age` > 20)"

‘<=’ と ‘>=’ はそれぞれ

User.where(user_table[:age].lteq(60)).to_sql

#=> "SELECT `users`.* FROM `users`  WHERE (`users`.`age` <= 60)"

User.where(user_table[:age].gteq(20)).to_sql
#=> "SELECT `users`.* FROM `users`  WHERE (`users`.`age` >= 20)"

他にもLIMIT OFFSET JOIN 等に独自のメソッドが存在します。
詳しくはGitHubのページ等を参照して下さい。

サブクエリを使う

Arel::Tableを利用すれば、サブクエリを使ったSQLを生成することができます。
以下はホワイトリストにまだ登録されていないユーザーを選択する場合を例にしたサンプルです。

ActiveRecord単体ではこのように書いていました。

white_listed_users = WhiteList.group(:user_id).pluck(:user_id)
#=> [1, 2, 3]

User.where("id NOT IN (?)", white_listed_users).to_sql
#=> "SELECT `users`.* FROM `users`  WHERE (id NOT IN (1,2,3))"

Arel::Tableでサブクエリを利用した場合は以下のようになります。

user_table = User.arel_table
white_list_table = WhiteList.arel_table

User.where(user_table[:id].not_in(white_list_table.project(:user_id) ) ).to_sql

#=> "SELECT `users`.* FROM `users`  WHERE (`users`.`id` NOT IN (SELECT user_id FROM `white_lists` ))"

ActiveRecord単体で生成する場合は2度クエリを発行していますが、サブクエリをつかえば一度発行するだけで済みます。
単純なクエリでは影響は少ないかもしれませんが、複雑なテーブル構成のデータの場合はパフォーマンスの向上が見込める場合もあるのでは無いでしょうか。

Arel::Table を使うメリット

コードの見た目がスッキリする

コード内でSQLを記述した場合

  @admin = [true, false][rand(2)]
  sql = <<-ENDSQL
    SELECT *
    FROM users
    WHERE
    #{ 'id IS NOT NULL' if @admin  }
    #{ '(id NOT IN (SELECT user_id FROM white_lists))' unless @admin }
  ENDSQL

この例は短いですが、複雑なqueryではかなりの行をSQLの記述が占めてしまうのではないでしょうか。
また、パラメータによってqueryを書き換えたい場合 #{} を多用してしまうと見にくくなってしまいます。

スコープやクラスメソッドとして定義して再利用できる

query生成をメソッドに切りだすことで、Controller等から呼び出すことができるので
似たような検索を何度も行う場合はDRYに書くことができます。

class WhiteList < ActiveRecord::Base
  ...
  def self.whitelisted_users
    white_list_table.project(arel_table[:user_id])
  end
end

class SampleController < ApplicationController
  def show
    user_table = User.arel_table
    @users = User.where(user_table[:id].not_in(WhiteList.whitelisted_users))
  end
end

Railsで検索を行うと検索結果が ActiveRecord::Relation のオブジェクトとして取得されます。
ActiveRecord::Relation には高機能なメソッドがいろいろ定義されていますが、query生成については
ActiveRecord::Relation#where_values が便利です。

このメソッドを使うとWHERE句をArelのオブジェクトとして取得できるので、
複数のscopeをAND やOR で連結したり
検索条件を追加したりといったことができるようになります。

class User < ActiveRecord::Base
  scope :teenage, where(arel_table[:age].gteq(13).and(arel_table[:age].lteq(19)))
  scope :senior, where(arel_table[:age].gt(60))
end

class SampleController < ApplicationController
  def show
    # 13歳から19歳の間もしくは61歳以上
    teen_or_senior = User.teenage.senior.where_values.inject(:or)
    @teen_or_senior = User.where(teen_or_senior)
    @teen_or_senior.to_sql
    #=> "SELECT `users`.* FROM `users`  WHERE ((`users`.`age` >= 13 AND `users`.`age` <= 19 OR `users`.`age` > 60))"

    # 60歳も含むという条件を後から追加
    senior = User.senior.where_values.first
    @senior = User.where(senior.or(User.arel_table[:age].eq(60)))
    @senior.to_sql
    #=> "SELECT `users`.* FROM `users`  WHERE ((`users`.`age` < 60 OR `users`.`age` = 60))"
  end
end

成人 未成年 男性 女性 etc… よく使う検索条件をscopeとして定義しておいて、
複数のscopeを組み合わせて複雑なqueryを作り上げるという使い方ができそうですね。

RDBMSが変わっても同じように使える

PostgreSQL 9.2.2で動作を確認しました。

MySQLと PostgreSQL ではboolean型のデータが違ったり
ORDER BY句でランダムに並べ替える際に使う関数名が違ったりと、
RDBMSによって動くqueryと動かないqueryがあるため、
生のSQLを使っているとサーバ移転等で環境が変わった際にソースコードの修正が必要になる場合があります。
しかし、Arelを適切に使っていればそういった変更が不要になるかもしれません。

Rails4でも動きます

そろそろRails4の正式版がリリースされますね。
せっかくなので、Ruby2.0 Rails4 の環境で動作するか確認してみました。

動作確認環境
ruby: 2.0.0p195
Rails: 4.0.0.rc1
arel: 4.0.0
MySQL: 5.1.65

例に上げたような単純なqueryであれば問題なく動作するようです。
scopeを利用した場合の例だけは少し問題があります。
そのままでも動作はするのですが、DEPRECATION WARNING が出てしまいます。
scopeの書き方が変わったようなので、指示の通りに書き換えます。

# DEPRECATION WARNING: Using #scope without passing a callable object is deprecated. For example `scope :red, where(color: 'red')` should be changed to `scope :red, -> { where(color: 'red') }`. There are numerous gotchas in the former usage and it makes the implementation more complicated and buggy. (If you prefer, you can just define a class method named `self.red`.).

class User < ActiveRecord::Base
  scope :teenage, -> { where(arel_table[:age].gteq(13).and(arel_table[:age].lteq(19))) }
  scope :senior, -> { where(arel_table[:age].gt(60)) }
end

これで警告も出なくなるはずです。
以上、簡単な例でしたがArelの使い方をまとめてみました。

便利なのでどんどん使いましょうとは言い難いですが、Arelのメソッドはなんだかかっこいいですね。


Ruby文字列のUnicodeエスケープシーケンスをデコードする

$
0
0

JSON APIで取得した文字列は、普通日本語部分がUnicodeコードポイントにエスケープされています。

たとえばTwitterAPIで取得したJSONは、単純に文字列として見ると以下のようになります。

# wget http://api.twitter.com/1/users/lookup.json?screen_name=TwitterJP

[{"name":"TwitterJP","time_zone":"Tokyo","follow_request_sent":null,"location":"\u6771\u4eac\u8d64\u5742","profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/3407356865\/62f0d53222361fbd2c1fe9889f4cc559_normal.png","profile_sidebar_border_color":"FFFFFF","id":7080152,"followers_count":1211377,"status":{"possibly_sensitive":false,"in_reply_to_status_id":null,"truncated":false,"in_reply_to_status_id_str":null,"in_reply_to_screen_name":null,"geo":null,"source":"web","in_reply_to_user_id_str":null,"place":null,"favorited":false,"coordinates":null,"id":337745769207570433,"contributors":null,"in_reply_to_user_id":null,"id_str":"337745769207570433","retweet_count":54,"text":"1\u9031\u9593\u306e\u9593\u306b\u30ea\u30c4\u30a4\u30fc\u30c8\u304c\u591a\u304b\u3063\u305f\u30c4\u30a4\u30fc\u30c8\u3067\u3059\u3002\u30e9\u30c6\u30a2\u30fc\u30c8\u306e\u9032\u6b69\u306b\u3082\u9a5a\u304d\u307e\u3059\u304c\u3001\u3053\u3093\u306a\u3068\u3053\u308d\u3067\u30d2\u30fc\u30ed\u30fc\u306b\u51fa\u4f1a\u3063\u305f\u6642\u306e\u30b7\u30e7\u30c3\u30af\u306f\u2026\u3002http:\/\/t.co\/ycbubP2iY6","retweeted":false,"created_at":"Fri May 24 01:44:01 +0000 2013"},"profile_background_tile":true,"default_profile_image":false,"profile_sidebar_fill_color":"DDEEF6","is_translator":false,"profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/7080152\/1348004306","geo_enabled":true,"lang":"ja","favourites_count":5,"utc_offset":32400,"profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/3407356865\/62f0d53222361fbd2c1fe9889f4cc559_normal.png","screen_name":"TwitterJP","profile_background_color":"C0DEED","created_at":"Tue Jun 26 01:54:35 +0000 2007","profile_background_image_url_https":"https:\/\/si0.twimg.com\/profile_background_images\/669680452\/4158abf21ac976cdb0459ccf14acd5f2.jpeg","protected":false,"description":"\u65e5\u672c\u8a9e\u7248Twitter\u516c\u5f0f\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u3059\u3002","profile_background_image_url":"http:\/\/a0.twimg.com\/profile_background_images\/669680452\/4158abf21ac976cdb0459ccf14acd5f2.jpeg","default_profile":false,"following":null,"verified":true,"profile_link_color":"0084B4","contributors_enabled":false,"friends_count":4307,"statuses_count":2143,"id_str":"7080152","profile_use_background_image":true,"profile_text_color":"333333","listed_count":16516,"url":"http:\/\/blog.jp.twitter.com","notifications":null}]

JSONライブラリで戻す

Rubyを使っているなら、特に難しいことを考えずにJSONに読み込ませればunescapeしてくれます。

require 'json'
require 'open-uri'
url = 'http://api.twitter.com/1/users/lookup.json?screen_name=TwitterJP'
JSON.load(open(uri).read)

[{"name"=>"TwitterJP", "time_zone"=>"Tokyo", "follow_request_sent"=>false, "location"=>"東京赤坂", "profile_image_url"=>"http://a0.twimg.com/profile_images/3407356865/62f0d53222361fbd2c1fe9889f4cc559_normal.png", "profile_sidebar_border_color"=>"FFFFFF", "profile_image_url_https"=>"https://si0.twimg.com/profile_images/3407356865/62f0d53222361fbd2c1fe9889f4cc559_normal.png", "id"=>7080152, "followers_count"=>1211378, "status"=>{"possibly_sensitive"=>false, "in_reply_to_status_id"=>nil, "truncated"=>false, "in_reply_to_screen_name"=>nil, "geo"=>nil, "source"=>"web", "place"=>nil, "favorited"=>false, "coordinates"=>nil, "id"=>337745769207570433, "in_reply_to_status_id_str"=>nil, "contributors"=>nil, "in_reply_to_user_id"=>nil, "id_str"=>"337745769207570433", "retweet_count"=>54, "text"=>"1週間の間にリツイートが多かったツイートです。ラテアートの進歩にも驚きますが、こんなところでヒーローに出会った時のショックは…。http://t.co/ycbubP2iY6", "in_reply_to_user_id_str"=>nil, "retweeted"=>false, "created_at"=>"Fri May 24 01:44:01 +0000 2013"}, "profile_background_tile"=>true, "default_profile_image"=>false, "profile_sidebar_fill_color"=>"DDEEF6", "profile_banner_url"=>"https://pbs.twimg.com/profile_banners/7080152/1348004306", "geo_enabled"=>true, "lang"=>"ja", "favourites_count"=>5, "utc_offset"=>32400, "screen_name"=>"TwitterJP", "profile_background_color"=>"C0DEED", "created_at"=>"Tue Jun 26 01:54:35 +0000 2007", "profile_background_image_url_https"=>"https://si0.twimg.com/profile_background_images/669680452/4158abf21ac976cdb0459ccf14acd5f2.jpeg", "protected"=>false, "description"=>"日本語版Twitter公式 カウントです。", "profile_background_image_url"=>"http://a0.twimg.com/profile_background_images/669680452/4158abf21ac976cdb0459ccf14acd5f2.jpeg", "default_profile"=>false, "following"=>false, "verified"=>true, "profile_link_color"=>"0084B4", "contributors_enabled"=>false, "friends_count"=>4307, "statuses_count"=>2143, "is_translator"=>false, "id_str"=>"7080152", "profile_use_background_image"=>true, "profile_text_color"=>"333333", "listed_count"=>16516, "url"=>"http://blog.jp.twitter.com", "notifications"=>false}]

手動で戻す

しかし、何らかの事情で、通常ファイルやデータベースにこのUnicodeエスケープシーケンスが含まれていることもあるでしょう。
そんなときは、packを使って手動で処理してやります。

str = 'Twitter\u516c\u5f0f\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u3059\u3002'
puts str.gsub(/\\u([\da-fA-F]{4})/) { [$1].pack('H*').unpack('n*').pack('U*') }
# => 日本語版Twitter公式アカウントです。

文字参照(16進表記)を解決する

HTMLなどで、文字参照の形式になっていることがあります。

Twitter&#x516c;&#x5f0f;&#x30a2;&#x30ab;&#x30a6;&#x30f3;&#x30c8;&#x3067;&#x3059;&#x3002;

HTMLのほか、PDFから文字列を抽出するライブラリで、文字列の途中から急に文字参照になっていることもありました。

これも、先ほどと全く同じ方法で戻せます。

str.gsub(/&#x([\da-fA-F]+);/) { [$1].pack('H*').unpack('n*').pack('U') }

文字参照(10進表記)を解決する

文字参照は、10進表記もよく使われます。

Twitter&#20844;&#24335;&#12450;&#12459;&#12454;&#12531;&#12488;&#12391;&#12377;&#12290;

この場合も似たような感じで処理してやりましょう。

str.gsub(/&#(\d+);/) { [$1.to_i].pack('U') }

文字参照に関してはhtmlentitiesを使う方が簡単ですが、やっていることは似たような感じです。

Rails3でhttpsからhttpへのリダイレクトを行う方法

$
0
0

ここ最近記事書くのをサボってました.すみません.今回はSSL redirectの話.

Rails 3.1以降ではforce_sslを使うことで,サイト全体や特定のactionについて,SSLを強制することができます.
しかし,force_sslではHTTP -> HTTPSへのリダイレクトはできるのですが,その逆のHTTPS -> HTTPへのリダイレクトをサポートしていません.
つまり,force_sslを使った場合,一度httpsのURLに入ってしまった後に相対パスでリンクを遷移すると,httpアクセスで構わないページもhttpsでアクセスしてしまうことになります.
場合によってはHTTPSでアクセスする必要の無いページはできるだけHTTPでアクセスしてほしい,というケースがあるので,今回はそういったことを実現しようぜ,という話です.

このような処理が必要とされるケースとしては以下のものが考えられます.

  • アクセスの多いサイトにおいて,サーバ側でのSSL/TLSの暗号化処理が処理上のボトルネックになる場合
  • サイト上の一部のページにおいて外部HTTPサイトに置かれた画像やJavaScriptを読み込む必要がある

1番目はSSLアクセラレータやAWS ELB等を使っていれば関係ないですが,Webサーバ側でSSLサーバを兼任している場合に問題になることがあります.SSL/TLSの処理はアクセスが多いと意外と馬鹿にならないのでサイトの規模によっては必要になるでしょう.
2番目は外部APIやWidgetを使ったりするケースです.サービスによっては証明書付きのHTTPSサーバを用意していないサービスや,HTTPSのURLは有償サービスになっているものもあるため,そういったサービスを使っている場合には警告が表示されてしまいます.

そんなわけで,上記に当てはまるような問題を抱えている人はどうぞ.

Rails付属のforce_ssl

簡単にまとめると,force_sslができることは以下の二点です.

  • force_ssl指定したアクションにHTTPアクセスされた場合,HTTPSにリダイレクトさせる
  • force_ssl指定されていないアクションについては,HTTP/HTTPSどちらでもアクセスできる

図とサンプルコードにすると以下の様になります.

force_ssl

上記の例で言うと,hoge#baaなどにHTTPアクセスしているときにはHTTPのURLを行き来しているのですが,一度hoge#fooにアクセスしてHTTPSにリダイレクトしてからhoge#baaにアクセスすると,HTTPSである必要は無いのにHTTPSで通信が行われます.
ViewのリンクURLが「XXX_path」で書かれている場合は相対パスになるので,一度HTTPSに入ってしまうとHTTPに戻って来れないという状態になってしまうわけです. 

bartt/ssl_requirement

こうした問題を回避するにはbartt/ssl_requirementを使うことで,httpsアクセスさせたくないURLにアクセスされたときにはhttpリダイレクトするといった処理を実現することができます.

ssl_requirementはもともとRailsの公式ツリーの中でdhhが作成したものだったのですが,この機能がRails 3.1で取り込まれる際にforce_sslというDSLを使う形式になりました.
そのため,Railsツリーにあるssl_requirementが対応しているRailsバージョンは古く,最近のRails 3.2.13等で使うためにはfork版のbartt/ssl_requirementを使う必要があります.
基本的な使い方はGithubのREADME通りなので省略します.

bartt/ssl_requirementを使うには,DSLとしてssl_requiredとssl_allowedをcontrollerの先頭に記述することで,sample#required_actionにHTTPでアクセスすると自動的にhttps付きのURLにリダイレクトされます.
また,,sample#allowed_actionへはHTTP/HTTPSどちらでもアクセスすることができます.
一点注意としては,ssl_requirementをincludeすると,特にssl_requrired/ssl_allowedの指定が無いactionはHTTPアクセスが強要されます(HTTPSでアクセスするとHTTPにリダイレクトされる).

図にすると以下の様になります.

ssl_requirement

bartt/ssl_requirementの問題点

通常のサイトであれば,bartt/ssl_requirementを使えばやりたいことは実現できるのですが,さらにもうちょっと気の利いたことをやろうとすると,困ってしまいます.
bartt/ssl_requirementはactionに対してSSL設定の有無を決めるので,そのままではactionの内容や特定の条件でSSL設定を切り替えることができません

具体的に直面したケースとしては,PC/スマートフォン両対応サイトで,同じactionを共有しているケースで,PCの場合にはHTTP強制,スマートフォンの場合はHTTPSでアクセスさせたいといったことがありました.
PCでHTTP強制したかった理由は前述の通り外部APIの制限があったためです.

bartt/ssl_requirementをhackして融通を利かせる

bartt/ssl_requirementのデフォルトではやりたいことができなかったので元のソースを眺めてみました.
すると,SslRequirementモジュールは読み込み時にensure_proper_protocolをbefore_filterとしてセットし,SSL必須かどうかのチェックはssl_required?メソッドによって判別していることが分かりました.
使っているメソッドまで分かれば後はoverrideするだけで良いので,以下の様に実装してみました.

ssl_required_renew

これで,想定通りやりたかったことが実現できました.

Railsで発行されたSQLを監視する

$
0
0

操作ログや簡易的なパフォーマンス測定などの目的で、特定アクションで実行されたSQLを監視したいことがあります。

filterやviewで実行された分を含まず、単純に指定ブロックの中を監視するなら、以下のような方法で簡単に実現可能です。

# application_controllerにでも定義しておく
def watch_queries
  events = []
  callback = -> name,start,finish,id,payload { events << payload[:sql] }
  ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
    yield
  end
  events
end

使い方はこんな感じ

def update
  @user = User.find(params[:id])
  queries = watch_queries do
    @user.update_attributes(user_params)
  end

  puts queries
  # => ["UPDATE users SET name='NEW NAME' WHERE id=1"]
end

payload[:name]を受け取ることで、Load/Schemaなどの実行種別もとれたりします。Schemaはいらないからフィルタしたい時はこちらで。

RubyKaigi 2013 に参加してきました

$
0
0

RubyKaigi2013に参加してきました。
個人的に面白かったセッション、業務で参考になりそうだと思ったセッションを紹介します。

How to create Ruby

MatzによるKeynote
後半の「言語を設計する事は普段書くプログラムにも必ず良い影響を与える
なぜなら言語はプログラミングの本質だから」という話に惹きつけられました。

Inside RubyMotion

RubyでiPhoneアプリが書ける RubyMotionの内部動作の紹介でした。
Objective-CのクラスとRubyのクラスを同じように扱うための仕組み等が紹介されていて
興味深かったです。
個人的にiPhoneアプリを作る機会があれば購入を検討しようと思います。
RubyMotion

High Performance Rails

高速化の為にGCをOFFにしているという発言が特に印象に残ってしまいましたが
その他のパフォーマンスチューニングについてはどの現場でも参考にできる内容だったと思います。
ボトルネックはDBと決めつけがちだけれど、実はActiveRecordの生成が遅い場合が多いであるとか
routingを見直すことで高速化に繋がるという2点はすぐにでも適用できる内容でした。
high_speed_rails
slide: https://speakerdeck.com/mirakui/high-performance-rails

Concerning `Applications’

Rails4から導入されるConcernを使って FatなModelをスリムにする方法を紹介していました。
実際には以前のバージョンから同様の機能を書くことはできたようなのですが
Railsが正式にConcernという機能として導入した事により今後は使われる機会が増えていくでしょう。
今のうちに予習しておきたいですね。
slide: https://speakerdeck.com/moro/concerning-application

Ruby’s GC 2.0

一緒に行ったbabaさんが大絶賛でした。
一般的にクヌースのアルゴリズムと言われている古典的なアルゴリズムからからより高度なアルゴリズムを採用したGCに進化したそうです。
エンタープライズの分野で使われている言語では既に一般的になっているアルゴリズらしいのですが、
それを採用したことでRubyがもっと仕事で使いやすくなっていくのではないでしょうか。
ruby_gc
slide: http://www.slideshare.net/authorNari/rubys-gc-20

Rails Gems realize RESTful modeling patterns

個人的には一番興味深かったセッションでした。
有名なGemの RESTfulな設計を参考にすることでRailsのベストプラクティスを適用できるという内容でした。

認証
devise https://github.com/plataformatec/devise
Authlogic https://github.com/binarylogic/authlogic

検索
Ransack https://github.com/ernie/ransack

ウィザード
Wicked https://github.com/schneems/wicked

以上のgemが具体例として紹介されていました
これらは全てリソース設計にRESTfulなパターンを採用しているそうです。
認証のような一見するとCRUDとは関係ないように見えるものでも
session#create(PUT) == login
session#destroy(DELETE) == logout
と表現することでRESTfulに表現できるというのは設計を考える上で参考にできる考え方だと思います。
設計に悩んでしまうことが多いので身につけたい考え方です。
こちらも参考に http://rest-pattern.hatenablog.com/
slide: http://www.slideshare.net/tkawa1/rubykaigi2013-rails-gems-realize-restful-modeling-patterns

これから主流になりそうな技術

Fluentdという名前を会期中に耳にすることが多かったです。
実際に業務で使っているという参加者も多かったですね。
Fluentd について触れているセッションもありました。
Casual Log Collection and Querying with fluent-plugin-riak
Complex Event Processing on Ruby and Fluentd
特に tagomorisさん のComplex Event Processing on Ruby and Fluentd は内容が充実していたのでオススメです。

全体を通して

参加者が500人以上で海外からの参加者も1割以上居たという事で、完全国際カンファレンスという雰囲気でした。
(海外のカンファレンスに参加した事はないので実際の所は分からないのですが。)
趣味のRubyというよりは仕事で使うRubyについて発表するという印象を受けました。
それでもRuby初心者も受け入れてくれる空気は健在だったと思います。
来年以降も開催される可能性が高いようなので、最近Rubyを始めた人は来年の参加を検討してみてください。
holeA
ちなみに、今回の参加費は会社が負担してくれました。
来年のRubuKaigiは無料で参加したいという方はこちらも検討してみてください。

Rails3アプリケーション開発で良く使うgemまとめ

$
0
0

Rails4がリリースされてしばらく経ちましたが,一部のgemが対応に追いついていないこともあり,まだ本番系のシステムに適用するのは様子見かなというところです.社内システムや個人的に遊ぶアプリで予行練習中.

というわけで,まだもうしばらくはRails3のお世話になりそうなので,Rails3の総まとめというわけではないですが,良く使うgemをまとめてみました.社内のチームMTGでの発表資料をSlideShareにアップロードしてあります.

既にRails開発に慣れた人にとってはおなじみのgemが多いと思いますが,これからRailsでの開発を始める人にとってはそれなりに有用かと思います.
また,弊社では数多くのRailsアプリケーションをcapistranoでdeployしている実績がありますので,deploy関連のgem等も参考になればどうぞ.

================以下追記(2013/07/13)================

紹介するgem達

Rails開発経験者なら誰もが知っているもの

社内での利用率が高く,汎用性の高いもの

案件によっては利用するもの

スライドの中では,それぞれのgemについてもう少し詳しく解説してますので,詳しくはそちらを参照して下さい.

Gemを使う場合の注意

スライドの最後にもまとめましたが,rubygemsから落とせるgemは玉石混交です.利用者が多くてよくメンテナンスされているものもあれば,最終更新が数年前のgemもあります.利用者が多いからと言ってバグがないというわけでは無いですが,少なくとも解決策を探すときには情報が多いですし,実装例も見つかるでしょう.
また,Gemfileの中に更新が適当なgemを混ぜると,最悪capistrano deploy時のbundle installでこける様になったりもするので,production level qualityを目指すのであれば,gemの選定には気を使った方が良いでしょう.

弊社でも,使っているgemがうまく動かないという事例は少なくないので,その時はgemのソースを追いかけてモンキーパッチを当てたりして使っています.もっとpull requestも投げられるようになると良いのですが,なかなかきれいな解決策まで実装する時間的余裕が無いのが少し残念な所です.

まとめ

最初は社内向けの技術資料を軽く公開しただけのつもりだったのですが,思ったよりPV伸びたので軽くまとめを書きました.
普段Web開発チームは皆忙しいのですが,目の前の仕事を片付けているだけだとエンジニアとして腐っていくので,こういった情報共有は積極的に続けて行きたいですね.

Rails 3.2.14がリリースされました

$
0
0

本日、Ruby on Rails 3.2.14がリリースされました

3.2.14は数々のバグフィックスが含まれたバージョンになります。大きなセキュリティフィックスは含まれていませんが、早めのアップデートを検討すると良いと思います。

なお3.2系は、次のリリース(3.2.15)が最後のバグフィックスになります。3.2.16以降はセキュリティパッチしか提供されない予定なので注意しましょう。

変更点

3.2.14には約40のバグフィックスが含まれていますが、いくつか見てみます。

routing

routes.rbにおいて、scopeでcontroller/actionを指定すれば、その内部のgetなどに反映されるようになりました。以下のような記述が可能になります。

# config/routes.rb
# ...
  scope '/job', :controller => 'job' do
    scope ':id', :action => 'manage_applicant' do
      get '/active'
    end
  end
# ...

# app/controllers/job_controller.rb
class JobController < ApplicationController
  # GET /job/5/active
  def manage_applicant
    @id = params[:id] # 5
  end
end

Rails 3.2.13で同じ記述をして「/job/5/active」にアクセスすると、”Routing Error: uninitialized constant ActiveController”になります。

link_to_unlessのescape

link_to_unlessに指定した文字列が、条件に関わらず常時HTMLエスケープされるようになりました。
第1引数が真の場合、aタグで囲まれることはありませんが、文字列はHTMLエスケープされて返されます。なお、Viewで普通に使う分には、元々表示する部分でHTMLエスケープされるため、3.2.13にXSS脆弱性があったという意味ではありません。

file_field

file_fieldにmultileオプションを指定した場合、名前に[]が付与されてArrayとして受け取れますが、明示的にnameを指定した際には[]が追加されないように修正されました。

action_missing

ActionController#action_missingが呼ばれないバグが修正されました。

number_to_human

number_to_humanで:unitsを指定し、数値に適用できるunitが無いとき、そのまま数値を返すように修正されました。従来はエラーになっていました。

# Rails 3.2.13
number_to_human 2000, unit: { thousand: 'k' } # => "2k"
number_to_human 100, unit: { thousand: 'k' } # => TypeError: no implicit conversion of nil into String

# Rails 3.2.14
number_to_human 2000, unit: { thousand: 'k' } # => "2k"
number_to_human 100, unit: { thousand: 'k' } # => "100"

i18n.fallback

viewファイルを検索するとき、I18n.fallbackが参照されるようになりました。

たとえばapplication.rbで以下のような設定がされているとします。

config.i18n.fallbacks = [:ja, :en]

index.html.erb, index.en.html.erbの2つのファイルがあるとき、I18n.locale = ‘de’の状態でアクセスすると、以下のような挙動になります。

  • Rails 3.2.13: index.html.erbが表示される
  • Rails 3.2.14: index.en.html.erbが表示される

従来の挙動を想定しているコードもありそうなので、注意が必要ですね。

has_many

new_recordなオブジェクトでhas_manyなrelationのidsを取得すると、foreign_id IS NOT NULLで検索されてしまうバグが修正されました。
Company has many contractsのときに以下のようになります。

company = Company.new
company.id # => nil

# Rails 3.2.13
company.contract_ids # => SELECT ... WHERE `contracts`.`company_id` IS NULL

# Rails 3.2.14
company.contract_ids # => []

まとめ

以上、ざっくりと変更点を流し読みしてみました。
Rails 4のプロジェクトも増えてきた今日この頃ですが、Rails 3.2があと数ヶ月でセキュリティメンテナンスモードに入るとは、相変わらず気持ちの良いスピード感です。手元のプロジェクトも、早めにアップデートしておこうと思います。

Railsでフォームオブジェクトを使った検索を簡単に実装する方法

$
0
0

RailsでFat ControllerになったらForm Classを作れ,という記事はあちこちで見るのですが,今一つ参考になるような実装があまり見つからなかったので記事にしてみました.
Rails 3.2.13,Ruby 2.0.0系で動作確認済みです.

Fat Controller問題

Railsで特に何も考えずに検索機能を作っていると,検索ロジックでcontrollerが膨らんできてしまうと思います(Fat Controller問題).

例えば,検索フォームの内容として最初は名前だけで検索するとのことで,

HogeController < ApplicationController
  def index
    @hoges = Hoge.where("name LIKE ?", "%#{params[:name]}%")
  end
end

と書いていたのが,そのうちメールアドレスや住所でも絞り込みしたくなり,

HogeController < ApplicationController
  def index
    @hoges = Hoge.where("name LIKE ?", "%#{params[:name]}%")
    @hoges = @hoge.where("email LIKE ?", "%#{params[:email]}%")
    @hoges = @hoge.where("address LIKE ?", "%#{params[:address]}%")
  end
end

と,どんどん内容が増えていきます.ちなみに,当該パラメータが空の場合に絞り込みたくなければ,

HogeController < ApplicationController
  def index
    @hoges = Hoge.scoped
    @hoges = @hoge.where("name LIKE ?", "%#{params[:name]}%") if params[:name].present?
    @hoges = @hoge.where("email LIKE ?", "%#{params[:email]}%") if params[:email].present?
    @hoges = @hoge.where("address LIKE ?", "%#{params[:address]}%") if params[:address].present?
  end
end

と,scopedと後置ifを使って書くと少しはマシに見えますが,検索フィールド数が増えたり,パラメータを前処理してからwhereに渡したりしたくなってくるとどんどんcontrollerが太っていきます.
また,この方式の場合,検索結果ページにも検索フォームを表示したい,といったときには検索パラメータをViewに渡す必要があり,その辺りも含めて実装していくと,さらに太ります.

フォームクラスを作る

こんな時,検索フォーム自体をクラスとして実装するフォームクラスを検討せよ,というのが良く言われる話です.
フォームクラス自体はWebアプリケーションフレームワークでは割合一般的で,PHPでも古くはPEAR::HTML_Quickformから最近ではSymfony 1.x系のsfFormなど,フォームを1クラスとして実装する概念は一般的です.

RailsではCode Climate Blog7 Patterns to Refactor Fat ActiveRecord Modelsでフォームクラスに関する言及がありますし,RailsCastsでもForm Objectに関する配信があります(これはPro向け配信なので,有償登録しないと観られません).
Code Climate Blogの例では,solnic/virtusというgemを利用するやり方が書いてあり,実際に上記の例で実装してみると,以下の様になると思います.

Form Class

# app/models/hoge_form.rb
class HogeForm
  include Virtus

  extend ActiveModel::Naming
  include ActiveModel::Conversion
  include ActiveModel::Validations

  attribute :name, String
  attribute :email, String
  attribute :address, String

  # validationが必要ならここにvalidatesを書ける
  #  validates :name, presence: true

  def search
    scoped = Hoge.scoped
    scoped = Hoge.where("name LIKE ?", "%#{name}%") if name.present?
    scoped = Hoge.where("email LIKE ?", "%#{name}%") if email.present?
    scoped = Hoge.where("address LIKE ?", "%#{name}%") if address.present?
    scoped
  end
end

Controller

# app/controllers/hoge_controller.rb
HogeController < ApplicationController
  def index
    @hoge_form = HogeForm.new params[:hoge_form]
    @hoges = @hoge_form.search
  end
end

View

# app/views/hoges/index.html
<h1>Hoge search</h1>
<div class="search_form">
  <%= form_for @hoge_form, url: hoges_path, method: :get do |f| %>
    <%= f.text_field :name %>
    <%= f.text_field :email %>
    <%= f.text_field :address %>  
    <%= f.submit %>
  <% end %>
</div>
<div class="result">
  <table>
    <tr>
      <th>名前</th>
      <th>メールアドレス</th>
      <th>住所</th>
    </tr>
    <% @hoges.each do |hoge| %>
      <tr>
        <td><%= hoge.name %></td>
        <td><%= hoge.email %></td>
        <td><%= hoge.address %></td>
      </tr>
    <% end %>
  </table>
</div>

Controllerにあった検索ロジックがフォームクラスに移ったのが分かると思います.
また,もしパラメータの前処理がしたければ,フォームクラス側のvalidatorなどで実装すれば良いわけですね.

フォームクラスを一般化する

ここまでに上げたフォームクラスの実装方法により,Controllerに実装されていったロジックがフォームクラスに移り,見通しが良くなりました.
しかし,こういった特定Modelに対する検索機能は非常によく行う処理のため,いちいちScaffoldごとに書いているとしんどいです.

そこで,一般的な検索に使うフォームクラスを作成するのに便利なクラスを作ってみました.内部でActiveAttrを使っているので,利用する場合はGemfileにactive_attrを追加してbundle installして使って下さい.

# 検索フォームのための汎用モデル
class SearchForm
  include ActiveAttr::Model

  class_attribute :_search_model
  class_attribute :_like_attributes
  class_attribute :_equal_attributes
  class_attribute :_join_tables

  self._like_attributes  = []
  self._equal_attributes = []
  self._join_tables   = []

  class << self
    private
    def inherited(child)
      child._search_model = child.name.gsub('SearchForm', '').constantize
    end

    def define_attribute(*attrs)
      attrs.each do |attr|
        if attr.respond_to?(:each)
          attr.each do |attr2|
            __send__(:attribute, attr2) unless attributes.include?(attr2)
          end
        else
          __send__(:attribute, attr) unless attributes.include?(attr)
        end
      end
    end

    def search_model(attr)
      self._search_model = attr
    end

    def like_attributes(*attrs)
      define_attribute attrs
      if attrs.respond_to?(:each)
        attrs.each do |attr|
          self._like_attributes += [attr]
        end
      else
        self._like_attributes += [attrs[0]]
      end
    end

    def equal_attributes(*attrs)
      define_attribute attrs
      if attrs.respond_to?(:each)
        attrs.each do |attr|
          self._equal_attributes += [attr]
        end
      else
        self._equal_attributes += [attrs[0]]
      end
    end

    def join_tables(*attrs)
      define_attribute attrs
      if attrs.respond_to?(:each)
        attrs.each do |attr|
          self._join_tables += [attr]
        end
      else
        self._join_tables += [attrs[0]]
      end
    end
  end

  def search
    scoped = _search_model.scoped
    _like_attributes.each do |attr|
      scoped = scoped.where _search_model.arel_table[attr].matches("%#{send(attr)}%") if send(attr).present?
    end
    _equal_attributes.each do |attr|
      scoped = scoped.where _search_model.arel_table[attr].eq(send(attr)) if send(attr).present?
    end

    _join_tables.each do |model|
      scoped = scoped.includes model
    end
    scoped
  end
end

このSearchFormを使うと,先ほど書いた例は以下の様に書けます.

Form Class

class HogeSearchForm < SearchForm
  like_attributes :name, :email, :address
end

Controller

# app/controllers/hoge_controller.rb
HogeController < ApplicationController
  def index
    @hoge_search_form = HogeSearchForm.new params[:hoge_search_form]
    @hoges = @hoge_search_form.search
  end
end

View

# app/views/hoges/index.html
<h1>Hoge search</h1>
<div class="search_form">
  <%= form_for @hoge_search_form, url: hoges_path, method: :get do |f| %>
    <%= f.text_field :name %>
    <%= f.text_field :email %>
    <%= f.text_field :address %>  
    <%= f.submit %>
  <% end %>
</div>
<div class="result">
  <table>
    <tr>
      <th>名前</th>
      <th>メールアドレス</th>
      <th>住所</th>
    </tr>
    <% @hoges.each do |hoge| %>
      <tr>
        <td><%= hoge.name %></td>
        <td><%= hoge.email %></td>
        <td><%= hoge.address %></td>
      </tr>
    <% end %>
  </table>
</div>

フォームオブジェクト(HogeSearchForm)の実装がDSLを使って非常に単純に書けるようになりました.この状態だとLIKEとEQUAL検索しかできませんが,SearchFormを拡張していくことで他の検索条件にも耐えられるようになるかと思います.
まだまだブラッシュアップしたい所はたくさんありますが,とりあえず動くので公開しておきます.もっと良いものになったらまたアップデートして,また記事にしようと思います.

ではでは,いつも通りご利用は自己責任でお願いします.


初めてのGemの読み方

$
0
0

Web開発においてRailsの開発効率は圧倒的ですが、これは「Ruby言語の素晴らしさ」「Railsの優れた設計」はもちろんのこと、「やりたいことがたいてい見つかる豊富なGem」にも支えられています。

しかし、不慣れなうちはRails黒魔術やGemの挙動が理解できず、振り回されているのもよく目にします。
ライブラリのアップデートが速く、ググって得られる情報だけでは更新に追いつけないことも多いですね。

そこで今回の社内プチ勉強会は、「初めてのGemの読み方」を実施しました。
Gemを使うだけでなく、自在にソースを読めるようになるのが目標です。

使った資料をSlideShareにアップしておきました。

まとめ

簡単なGemは、初めてでも簡単に読むことができます。
今までGemを使うだけだった人は、ステップアップに是非読んでみると良いと思います。
はまったときのトラブル解決に役立つだけでなく、高品質なコードに触れることで自分のスキルアップにも必ず役立ちます。

CodeSchool Summer Campのバックパックが届いたので晒してみる

$
0
0

Code SchoolCodecademyの様な感じでWebブラウザ上でstep-by-stepなプログラミング講座が受講できるサイトです.
僕が今まで見てきている中では最も「中級者以上に勧められる」自習講座が揃っているサイトだと思います.

この手のプログラミング講座の類いは初心者向けのものが多く,中級者にとっては「そんなん知ってるよ」といった内容が多いのですが,Code Schoolの講座はかなり「分かっている」人が資料を作っており,Video,PDFのスライド,問題共に質が高いです.
特にRuby Bits Part 2はRubyの醍醐味である動的なメソッド追加やブロック関数,DSLの記述方法などを扱っているので,社内でもオススメ教材として利用しています.

Code SchoolはNew RelicやEngine Yard等と一緒にキャンペーンをやることが多く,5月末頃にはCode School Summer Campの案内が送られてきました(リンク先のキャンペーンは既に終わっているので,今から申し込むことはできません).
このパッケージには以下のものが含まれていました.

  • Code SchoolオリジナルバックパックとTシャツ3枚,ステッカーなど
  • その他大量の提携Webサービスクーポン

ちなみに値段は99 USDで,日本から注文すると送料が29 USDの,合計128 USDが価格になります.
これだけの内容で13,000円近くは高い!と普通は思うのですが,実は,パッケージには

  • Code Schoolのライセンス3ヶ月分(本来は1月25 USD * 3 = 75 USD)
  • New Relicの3ヶ月分のProライセンス(本来は199 USD * 3 = 597 USD)
  • Engine Yard Cloudの500時間分のアクセス(smallでも最低4,000円相当)
  • Githubの3-month Microクーポン(7 USD * 3 = 21 USD)

が含まれており,これだけでバックパックが無くても十分元が取れる(特にNewRelic)ので,買うことにしました.

中身の写真など

送料をケチって船便で頼んだので,7/30に届きました.ちなみに発送日は7/5付けになっていました.

IMG_1660

中身に入っていたものたちを出して取った写真.

IMG_1674

Promo Codeが書かれた紙やステッカー,Tシャツなどですね.怪しげなおっさんのマウスパッドはBenchmarkというメールマーケティングサービス?のおっさんの様です.
Tシャツはまあ,何かのハッカーイベントの時にでも着ていこうかなあ.

Railsでbefore_filter/before_actionがアクションを中止する仕組みを読んでみる

$
0
0

大昔のRailsでは、before_filterでfalseを返すとそこでchainが終わる、とやっていた気がしますが、今はそういうコード見ないですよね。

Rails 4だとこんなノリでbefore_actionでredirectして はいおしまい、ってやりますよね。

class UsersController < ApplicationController
  before_action :my_authenticate_admin

  def my_authenticate_admin
    unless current_user.admin?
      redirect_to root_path
    end
  end
end

当然、以後のbefore_actionやactionは実行されないことを期待するわけです。
これで全然問題ないのですが、なぜredirect_toするとそこで止まってアクションが実行されないのか、せっかくなので読んでみましょう。

before_actionを探す

まず、before_actionのコードを探します。

actionpack-4.0.0/lib/abstract_controller/callbacks.rb

     [:before, :after, :around].each do |callback|
        class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
          # Append a before, after or around callback. See _insert_callbacks
          # for details on the allowed parameters.
          def #{callback}_action(*names, &blk)
            _insert_callbacks(names, blk) do |name, options|
              set_callback(:process_action, :#{callback}, name, options)
            end
          end

AbstractController::Callbacksにありました。before_action, after_action, around_actionを定義しています。

AbstractController::Callbacksは、includedでこんな処理もしています。

    included do
      define_callbacks :process_action, :terminator => "response_body", :skip_after_callbacks_if_terminated => true
    end

あと、controllerのactionを実行するべきprocess_actionが、callbackの中に組み込まれていますね。

callbacksを読む

ActiveSupportのcallbacksを呼び出しているので、そちらを読んでみましょう。

activesupport-4.0.0/lib/active_support/callbacks.rb

      def define_callbacks(*callbacks)
        config = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
        callbacks.each do |callback|
          class_attribute "_#{callback}_callbacks"
          send("_#{callback}_callbacks=", CallbackChain.new(callback, config))
        end
      end

actionpackのほうで呼び出していたdefine_callbacksはここにありました。
CallbackChainはArrayにいくつかメソッドが付いただけのものです。”_process_action_callbacks”という配列ができるようです。

set_callbackはこんな感じです。

      def set_callback(name, *filter_list, &block)
        mapped = nil

        __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options|
          mapped ||= filters.map do |filter|
            Callback.new(chain, filter, type, options.dup, self)
          end

          options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)

          target.send("_#{name}_callbacks=", chain)
        end
      end

配列に追加してるだけですね。

CallbackChainはArrayですが、1つcompileという重要そうなメソッドがあります。

      def compile
        method =  ["value = nil", "halted = false"]
        callbacks = "value = !halted && (!block_given? || yield)"
        reverse_each do |callback|
          callbacks = callback.apply(callbacks)
        end
        method << callbacks

        method << "value"
        method.join("\n")
      end

お次はapplyです。

      def apply(code)
        case @kind
        when :before
          <<-RUBY_EVAL
            if !halted && #{@compiled_options}
              # This double assignment is to prevent warnings in 1.9.3 as
              # the `result` variable is not always used except if the
              # terminator code refers to it.
              result = result = #{@filter}
              halted = (#{chain.config[:terminator]})
              if halted
                halted_callback_hook(#{@raw_filter.inspect.inspect})
              end
            end
            #{code}
          RUBY_EVAL

いかにもなif文が出てきました。haltedがtrueになると、それ以降callbackを実行しないようになっています。
chain.config[:terminator]を評価した結果がhaltedになっていますが、これは最初に見たAbstractController::Callbacksで”response_body”にセットされていました。

つまり、response_bodyがnilやfalseでないときに、そこでcallback chainが止まるわけですね。

response_bodyを探す

actionpack-4.0.0/lib/abstract_controller/base.rb

  class Base
    attr_internal :response_body

単なるattributeです。

renderやrespond_toを読んでみると、確かにresponse_bodyに書き込んでいます。
actionpack-4.0.0/lib/abstract_controller/metal/redirecting.rb

    def redirect_to(options = {}, response_status = {}) #:doc:
      raise ActionControllerError.new("Cannot redirect to nil!") unless options
      raise AbstractController::DoubleRenderError if response_body

      self.status        = _extract_redirect_to_status(options, response_status)
      self.location      = _compute_redirect_to_location(options)
      self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(location)}\">redirected</a>.</body></html>"
    end

ということで、before_actionでrenderやredirect_toをすると、response_bodyが空で無くなるため、そこでcallback chainが止まり、以降のbefore_actionやactionが実行されなくなるということが分かりました。

めでたしめでたし。

[Rails 4.0] 巨大なテーブルやserializeを使うときのActiveRecordオーバーヘッドを測定してみた

$
0
0

Railsは遅い!とよく言われますが、高速化ポイントのうち最有力候補の1つがDB/モデル周りです。
N+1問題を修正するだけでも、体感できるレベルの高速化が期待できます。

しかし、RubyKaigiでCookPadの方も言っていたように、RDBMSがSQLを実行する時間だけでなく、ActiveRecordオブジェクトの処理は非常に重いです。
モデル周りの最適化と聞いて、一生懸命SQLをEXPLAINするのも大事ですが、ログに出力される「Prefecture Load (0.5ms)」のような数値にはActiveRecordオブジェクト部分のオーバーヘッドが含まれていないため、全体でどの部分が遅いのかを理解しないと徒労に終わってしまう可能性があります。

よく言われる高速化のポイントとして

  • テーブルにカラムを増やしすぎると重いから、巨大なテーブルは分割すべし
  • serialize(や, ActiveRecord::Store)は遅いから使うな
  • pluckを使うと速い

などがありますが、実際に巨大なテーブルやserializeを使った場合、どの程度遅くなるのか計測してみました。

もちろん、Rails 4で!

条件や環境

DBとモデル

今回使うモデルはこのようなものです。

Model1
nameだけのシンプルなモデル
Model2
256+1個のstringフィールドを持った巨大なモデル
Model3
Model2と同じテーブル構成で、すべてserializeしたもの
Model4
textカラムで長いserializeデータを入れられるもの
create_table :model1s do |t|
  t.string :name
end

create_table :model2s do |t|
  t.string :name
  (0..255).each do |i|
    t.string "name#{i}"
  end
end

create_table :model3s do |t|
  t.string :name
  (0..255).each do |i|
    t.string "name#{i}"
  end
end

create_table :model4s do |t|
  t.string :name
  t.text :data
end
class Model1 < ActiveRecord::Base
end

class Model2 < ActiveRecord::Base
end

class Model3 < ActiveRecord::Base
  (0..255).each do |i|
    serialize "data#{i}"
  end
end

class Model4 < ActiveRecord::Base
  serialize :data
end

測定方法

測定はすべて、rails consoleでBenchmark.measureしたものを掲載しています。
ばらつきを減らすため、3回ずつ測定しました。

環境は以下の通りです。

  • Ruby 2.0.0-p247
  • Ruby on Rails 4.0.0
  • SQLite3
  • Ubuntu 13.04 x86_x64
  • Core i7-4500U, RAM2GB (VM)

結果はBenchmark.measureのそのままなので、以下のようなフォーマットになります。

user system total (real)

rails consoleではログが標準出力に出てくるため、SQLを表示するとそこがオーバーヘッドになる可能性があります。今回は、development.rbでログレベルをinfoにし、SQLがログ出力されないようにしました。

config.log_level = :info

環境の違いについて

rails consoleはdevelopmentモードで起動していますが、同じ測定をproductionモードで実行しても、結果はほぼ同じでした。

DBをMySQLにする場合、InnoDBのmax row sizeに引っかかるので、カラム数を250位に減らしてencodingをlatin1にしてやる必要があります。
MySQLにすると、INSERT系の部分が1割ほど早くなることはありましたが、全体的な傾向は全く同じでした。

測定結果

new

まず、各モデルを1万回ずつnewしてみます。
DBには保存しません。

10000.times { Model1.new }
  0.600000   0.020000   0.620000 (  0.630955)
  0.520000   0.010000   0.530000 (  0.554211)
  0.610000   0.000000   0.610000 (  0.609702)

10000.times { Model2.new }
 21.030000   0.040000  21.070000 ( 21.312515)
 22.290000   0.100000  22.390000 ( 23.066587)
 23.690000   0.030000  23.720000 ( 23.846653)

10000.times { Model3.new }
 30.880000   0.010000  30.890000 ( 31.118914)
 30.920000   0.040000  30.960000 ( 31.395612)
 31.050000   0.120000  31.170000 ( 31.550993)

早速すごい差が出ました。
カラムが1個のModel1に比べ、257個のModel2では40倍近く遅くなっています。
serializeしたModel3は、データを何も入れなくても、さらに1.5倍ほど遅くなりました。

create

次に、各モデルを1000回ずつcreateしてみます。
newに加えて、validationやDBへの書き込みなどが発生します。

1000.times { Model1.create! name: 'HOGE' }
  3.420000   2.280000   5.700000 ( 13.391079)
  3.210000   2.380000   5.590000 ( 12.526155)
  2.970000   2.420000   5.390000 ( 11.630498)

1000.times { Model2.create! name: 'HOGE' }
 10.130000   2.400000  12.530000 ( 20.504972)
  8.270000   2.400000  10.670000 ( 16.984780)
  8.420000   2.520000  10.940000 ( 17.850486)

1000.times { Model3.create! name: 'HOGE' }
 26.470000   2.540000  29.010000 ( 37.193132)
 24.730000   2.520000  27.250000 ( 35.113511)
 24.850000   2.580000  27.430000 ( 34.312424)

おおざっぱに(real – total)がDB待ち時間と考えると、どのパターンでも大差なく8秒ほどがDB待ちになっています。
user時間では、Model2とModel3の差が大きくなっています。serializeカラムは、実際にはそのカラムがnilだとしても、保存時に大きな負荷になることがわかります。

params = { name: 'HOGE' }
(0..255).each { |i| params["data#{i}"] = i }

1000.times { Model2.create! params }
 32.010000   3.020000  35.030000 ( 42.856594)
 30.020000   2.800000  32.820000 ( 40.544360)
 34.010000   2.760000  36.770000 ( 44.144898)

1000.times { Model3.create! params }
139.150000   5.830000 144.980000 (153.915362)
136.440000   5.070000 141.510000 (150.696718)
136.450000   5.300000 141.750000 (151.109004)

今度はすべてのカラムに値を入れてみました。
どちらもデータが少ない場合に比べて遅くなっていますが、特にModel3の遅さが顕著です。serializeしたカラムは、値が入るとさらに大きな負荷がかかることがわかります。

find

次は、DBからデータを1件取得してみます。ID指定なので、DB側の負荷は少ないはずです。

# 事前にテーブルにはデータを1件だけ入れておく
# rake db:migrate:reset
Model1.create! name: 'HOGE'
Model2.create! name: 'HOGE'
Model3.create! name: 'HOGE'

10000.times { Model1.find(1) }
 10.590000   0.460000  11.050000 ( 11.103972)
 10.640000   0.460000  11.100000 ( 11.276034)
 10.470000   0.390000  10.860000 ( 10.997969)

10000.times { Model2.find(1) }
 28.690000   0.520000  29.210000 ( 29.436953)
 27.570000   0.540000  28.110000 ( 28.403855)
 28.990000   0.490000  29.480000 ( 29.817771)

10000.times { Model3.find(1) }
 41.590000   0.530000  42.120000 ( 42.496996)
 41.560000   0.540000  42.100000 ( 42.508387)
 42.140000   0.560000  42.700000 ( 43.247696)

Model1とModel2でまた大きな差が出ました。カラム数が増えればSELECT *で取得する件数も増えるので、単純に遅くなるのは理解できます。

そこで、SELECTするカラムを絞り込んでみましょう。

10000.times { Model1.select(:name).find(1) }
 10.200000   0.340000  10.540000 ( 10.632150)
 10.790000   0.450000  11.240000 ( 11.334411)
 11.070000   0.450000  11.520000 ( 11.797071)

10000.times { Model2.select(:name).find(1) }
 18.090000   0.360000  18.450000 ( 18.537357)
 19.590000   0.550000  20.140000 ( 20.405800)
 19.340000   0.390000  19.730000 ( 19.957142)

10000.times { Model3.select(:name).find(1) }
 21.910000   0.440000  22.350000 ( 22.616035)
 21.920000   0.410000  22.330000 ( 22.525840)
 21.540000   0.440000  21.980000 ( 22.149922)

うわあ微妙…
SELECTするカラムを絞っても、Model1と同じになるというわけではないみたいです。
ただ、serializeカラムを含まない状態でのfindだとModel2とModel3がほぼ同じになるのは興味深いですね。

それでは、速いと評判のpluckを使ってみましょう。

10000.times { Model1.pluck(:name) }
  8.410000   0.360000   8.770000 (  8.800651)
  8.540000   0.430000   8.970000 (  9.081969)
  8.400000   0.470000   8.870000 (  9.099462)

10000.times { Model2.pluck(:name) }
  8.150000   0.390000   8.540000 (  8.639425)
  8.460000   0.430000   8.890000 (  9.159416)
  8.360000   0.390000   8.750000 (  8.922662)

10000.times { Model3.pluck(:name) }
 12.090000   0.460000  12.550000 ( 12.801018)
 12.030000   0.490000  12.520000 ( 12.750606)
 12.070000   0.490000  12.560000 ( 12.808199)

そんな10倍も速くなるというほどではないですが、確かに高速化されています。
Model1とModel2が、同じ速度になりました。Modelオブジェクトをnewせず、SELECTも必要なカラムだけ実行するので、テーブルにいくつカラムがあっても影響ないようです。
ただModel3を見る限り、取得するカラムがserializeされていなくても、そのモデルにserializeカラムがあるだけで遅くなるみたいです。

pluckについて追記

08/13追記
比較対象のfindがnameにアクセスしていなくて不公平だったので、nameを評価したバージョンを追記しておきます。

10000.times { Model1.find(1).name }
 11.090000   0.420000  11.510000 ( 11.583137)
  9.730000   0.390000  10.120000 ( 10.191548)
 10.000000   0.290000  10.290000 ( 10.393030)

10000.times { Model2.find(1).name }
 29.110000   0.450000  29.560000 ( 29.826637)
 28.070000   0.370000  28.440000 ( 28.632295)
 28.380000   0.370000  28.750000 ( 29.052701)

10000.times { Model3.find(1).name }
 41.340000   0.420000  41.760000 ( 42.036065)
 42.110000   0.530000  42.640000 ( 43.228848)
 43.630000   0.530000  44.160000 ( 44.654895)

10000.times { Model1.select(:name).find(1).name }
 11.070000   0.400000  11.470000 ( 11.551049)
 11.620000   0.430000  12.050000 ( 12.187623)
 10.800000   0.410000  11.210000 ( 11.448768)

10000.times { Model2.select(:name).find(1).name }
 18.330000   0.400000  18.730000 ( 18.914217)
 18.140000   0.430000  18.570000 ( 18.680057)
 19.250000   0.440000  19.690000 ( 20.046789)

10000.times { Model3.select(:name).find(1).name }
 21.290000   0.380000  21.670000 ( 22.053869)
 20.440000   0.540000  20.980000 ( 21.202539)
 21.150000   0.470000  21.620000 ( 21.825256)

はい、完全に誤差の範囲内でした。

ついでなので、pluckが真価を発揮する(というか単に正しい使用方法)、大量データから特定カラムを取得する速度を見てみましょう。

# rake db:migrate:reset
# まずは100件のデータで小手調べ
100.times { Model1.create! name: 'HOGE' }

100.times { Model1.all.map(&:name) }
  0.830000   0.010000   0.840000 (  0.857858)
  0.820000   0.000000   0.820000 (  0.837119)
  0.810000   0.010000   0.820000 (  0.829982)

100.times { Model1.pluck(:name) }
  0.170000   0.000000   0.170000 (  0.167697)
  0.230000   0.000000   0.230000 (  0.230604)
  0.250000   0.000000   0.250000 (  0.258802)

# データ1000件で同じことをする
900.times { Model1.create! name: 'HOGE' }

100.times { Model1.all.map(&:name) }
 10.470000   0.020000  10.490000 ( 10.609048)
 10.570000   0.020000  10.590000 ( 10.670045)
 11.100000   0.040000  11.140000 ( 11.354826)

100.times { Model1.pluck(:name) }
  2.080000   0.030000   2.110000 (  2.188038)
  2.150000   0.010000   2.160000 (  2.178590)
  2.160000   0.010000   2.170000 (  2.193926)

# データ1万件で同じことをする
9000.times { Model1.create! name: 'HOGE' }

100.times { Model1.all.map(&:name) }
 90.960000   0.080000  91.040000 ( 91.983647)
 85.910000   0.060000  85.970000 ( 86.698406)
102.590000   0.230000 102.820000 (103.998012)

100.times { Model1.pluck(:name) }
 10.160000   0.100000  10.260000 ( 10.390672)
 14.050000   0.120000  14.170000 ( 14.542783)
 13.250000   0.010000  13.260000 ( 13.390256)

やはりこのケースでは、pluckの優位性が目立ちます。1万件では10倍近くの差が出ました。
先ほどの結果と合わせてみると、Model2やModel3ではより大きな差になることがわかります。

serializeのデータ量による差

最後に、serializeに大きいデータを入れるとどの程度変わるのか見てみましょう。

params = { name: 'HOGE' }

params[:data] = 'a'
1000.times { Model4.create! params }
  4.330000   2.380000   6.710000 ( 13.019374)
  4.620000   2.490000   7.110000 ( 13.991936)
  4.590000   2.410000   7.000000 ( 13.777286)

params[:data] = [1]
1000.times { Model4.create! params }
  4.400000   2.580000   6.980000 ( 14.922659)
  4.400000   2.350000   6.750000 ( 13.414692)
  4.310000   2.380000   6.690000 ( 13.555350)

params[:data] = (1..100).to_a
1000.times { Model4.create! params }
 10.580000   3.070000  13.650000 ( 21.245134)
 10.860000   2.990000  13.850000 ( 21.383432)
 11.020000   3.210000  14.230000 ( 21.944009)

params[:data] = (1..1000).to_a
1000.times { Model4.create! params }
 67.160000   3.040000  70.200000 ( 78.202392)
 67.920000   2.900000  70.820000 ( 78.956099)
 70.520000   3.080000  73.600000 ( 81.448431)

セットするデータが大きくなると、急激に遅くなっています。
長いとはいえ2カラムしかないテーブルで、1件挿入するのに80msもかかるのは軽くショックですね。

次に、findの速度を見てみます。

# rake db:migrate:reset

Model4.create!
10000.times { Model4.find(1) }
  9.890000   0.350000  10.240000 ( 10.310732)
 10.590000   0.370000  10.960000 ( 11.154107)
  9.950000   0.480000  10.430000 ( 10.588264)

Model4.find(1).update_attribute :data, [1]
10000.times { Model4.find(1) }
 10.220000   0.290000  10.510000 ( 10.573952)
 10.220000   0.420000  10.640000 ( 10.917978)
 10.400000   0.420000  10.820000 ( 11.114668)

Model4.find(1).update_attribute :data, (1..100).to_a
10000.times { Model4.find(1) }
 10.090000   0.400000  10.490000 ( 10.532497)
 10.260000   0.440000  10.700000 ( 10.952060)
 10.240000   0.430000  10.670000 ( 10.808926)

Model4.find(1).update_attribute :data, (1..1000).to_a
10000.times { Model4.find(1) }
 10.470000   0.330000  10.800000 ( 10.858970)
 10.400000   0.380000  10.780000 ( 11.011753)
 10.710000   0.460000  11.170000 ( 11.597594)

Model4.find(1).update_attribute :data, (1..10000).to_a
10000.times { Model4.find(1) }
 15.890000   0.430000  16.320000 ( 16.446115)
 15.990000   0.310000  16.300000 ( 16.413631)
 16.080000   0.520000  16.600000 ( 16.799734)

あれ、全然速度が変わりません。deserializeは超高速?

もちろんそんなことはなくて、findしても実際にそのカラムにアクセスするまではdeserializeが行われないのですね。
改めて、ちゃんと評価してみましょう。

# rake db:migrate:reset

Model4.create!
10000.times { Model4.find(1).data }
 10.550000   0.360000  10.910000 ( 11.168614)
 10.280000   0.390000  10.670000 ( 10.782972)
  9.970000   0.400000  10.370000 ( 10.448021)

Model4.find(1).update_attribute :data, [1]
10000.times { Model4.find(1).data }
 13.890000   0.480000  14.370000 ( 14.503605)
 13.630000   0.460000  14.090000 ( 14.238907)
 13.580000   0.390000  13.970000 ( 14.228241)

Model4.find(1).update_attribute :data, (1..100).to_a
10000.times { Model4.find(1).data }
 45.780000   0.490000  46.270000 ( 46.694700)
 45.350000   0.540000  45.890000 ( 46.240731)
 44.940000   0.540000  45.480000 ( 45.923395)

Model4.find(1).update_attribute :data, (1..1000).to_a
10000.times { Model4.find(1).data }
330.520000   0.810000 331.330000 (333.764699)
327.170000   0.670000 327.840000 (330.223176)
328.680000   0.910000 329.590000 (332.435454)

遅い、遅すぎる…
serializeカラムに長いデータが入っている場合、それをたくさんfindするのは現実的ではないようです。

「とりあえず便利そうだからserializeしたけど、WHEREで検索できなくて困った。全件取得してループで回してRuby側で検索しよう」という小学生メソッドでは、遅いどころかGateway Timeoutで死亡してしまいますね。

SQLを直接実行した場合

おまけとして、これらの検証結果と同じようなことを生SQLで実行してみました。

10000.times {
  ActiveRecord::Base.connection.execute(
    'SELECT name FROM model1s WHERE id = 1')
}
  2.930000   0.300000   3.230000 (  4.053164)
  3.060000   0.270000   3.330000 (  3.933746)
  3.070000   0.280000   3.350000 (  3.958062)

1000.times{
  ActiveRecord::Base.connection.execute(
    'INSERT INTO model1s SET name = "Hoge"')
}
  0.340000   0.110000   0.450000 (  3.312933)
  0.370000   0.030000   0.400000 (  2.651639)
  0.350000   0.140000   0.490000 (  3.199039)

比較するまでもありませんでした。
ちなみに、model2s, model3s, model4sのどのテーブルで実行しても速度はほぼ同じです。

まとめ

世間で言われる評判は正しかったです。

テーブルのカラム数が大きくなると、createやfindは非常に遅くなります。特にserializeなんかしていると目も当てられません。Userなど頻繁に使うのに肥大化しがちなテーブルでは注意が必要です。

今回は測定していませんが、scopeやassociationなどほかの要因でもモデルは複雑化していくので、pluckの優秀さが際立ちます。
たくさんのモデルオブジェクトをnewしないようにする注意が必要そうです。

カラムがとても多いテーブルでは、SELECTで絞るのもそれなりに有効です。ただ、257個を1個に絞ったところで2倍程度の速度向上だし、テーブルの肥大化で低下した速度を取り戻すほどではありません。
コーディングの複雑化を考えると、それよりはnewされる回数を減らす工夫をした方が効率が良い気がします。

SQLを生で実行すると、桁が違う速度が得られます。ただ、それを改めてモデルに入れていたら意味がないし、配列のまま扱うと何のためにRailsを使っているのかわからなくなってきます。

結局高速化は、効果のオーダーと手間や複雑さと増大といったコストのバランスを考慮し、費用対効果で判断するしかないですね。

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

$
0
0

最近になって知った Railsの便利なメソッドを紹介したいと思います。

pluck

http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-pluck

User.select(:id).where("age >= ?", 20).map(&:id)

こんなコードが

User.where("age >= ?", 20).pluck(:id)

こうなります

Rails3系では指定できるカラムはひとつだけでしたが、Rails4から複数カラムを指定できるようになりました。

User.where("age >= ?", 20).pluck(:id, :name)
#=> [[1, '太郎'], [2, '花子'], [3, '二郎']]

Rails3系でもmultipluck というgemを使えば同じことができるみたいですね。

reload!

http://api.rubyonrails.org/classes/Rails/ConsoleMethods.html#method-i-reload-21

rails console で動作を確認しながらコードを書くことがあるのですが、
今まではコードを修正する度にexit してrails console を再度実行していましたが、リロード用のコマンドがありました。
reload! を実行するとexit しなくてもRailsをリロードしてくれます。
reload! 実行前に変数に格納していたobjectはリロードされていないので注意が必要です。

irb(main):001:> user = User.first
irb(main):002:> reload!
Reloading...
=> true

irb(main):003:0> user.new_method
NoMethodError: undefined method `method' for ....

irb(main):004:0> user.reload
irb(main):005:0> user.new_method

try

http://api.rubyonrails.org/classes/NilClass.html#method-i-try

viewのコードを書いている場合に多いのですが、nil である可能性があるobject のメソッド を呼び出す場合に

名前: <%= @user.name unless @user.nil? %>

このように 空かどうかの判定を書くことがあると思います。
同じことをtryを使って書くと

名前: <%= @user.try(:name) %>

こちらのほうがスッキリしています。
引数の渡し方がわからなかったのですが、以下のようにすれば良いようです。

User.try(:find, 1)

Rails3 の頃は存在しないメソッドを指定するとNoMethodError になりましたが
Rails4 からはrespond_to? でメソッドの有無をチェックするようになったためより気軽に使えるようになりました。

persisted?

http://api.rubyonrails.org/classes/ActiveModel/Model.html#method-i-persisted-3F

Active Record object がDB に保存済みかどうかを判定するメソッド です。

user = User.first
user.persisted?
#=> true
new_user = User.new
user.persisted?
#=> false

Active Record objectの状態をチェックするためのメソッド は他にもいろいろあるので使いこなせると良いですね。

delegate

http://api.rubyonrails.org/classes/Module.html#method-i-delegate

指定したクラスのメソッドを簡単に呼び出せるようにしてくれます。

class User < ActiveRecord::Base
  #t.string :name
  #t.string :email
 
  has_one :profile
end

class Profile < ActiveRecord::Base
  #t.integer :age
  #t.string :favorites
  #t.string :location

  belongs_to :user
  delegate :name, to: :user
end

profile = Profile.find(1)
# 普通はuser を経由してから呼び出します
profile.user.name #=> "太郎"
profile.user.email #=> "sample@example.com"

# delegate するとprofile から直接呼び出せるようになります
profile.name #=> "太郎"
profile.email #=> NoMethodError: undefined method `email'...

ドキュメントには他にもいろいろなパターンのわかりやすいサンプルコードが掲載されているので
一度目を通しておくと良いと思います。

他の便利メソッド

Railsを使っていて便利だなぁと思うメソッドはActiveSupport のメソッドであることが多いです。
たまにはドキュメントを読んだり
ソースコードを読んだりしてみると新しい発見があるかもしれません。

Viewing all 1384 articles
Browse latest View live