ActiveRecord-importで1クエリでcolumnの値をupdateする方法

Railsでもまとめてupdateしてクエリ発行数を抑えたい!

Railsアプリケーションで、例えば人気投票やカウンター機能で1度に複数のrecordに跨ってincrement…といったケースで 1クエリで実行したい 場合。

既存のレコードのありきの場合はMySQLでは例えば

update votes set counter=counter + 1 where id IN (1, 2, 3)

と言った書き方で出来ますが、Railsだと記述の仕方によってはUPDATEする前にSELECTのクエリが入ったり1件ずつUPDATEしてしまうのでクエリの発行数が増大してしまいます。

また同時に更新処理をする際に、SELECT〜UPDATEが完了するまでの間ロックをしないと正しく値が更新されなくなりますし、ロックをすれば(特に複数レコード同士に跨ると) デッドロックの原因になる のでパフォーマンスを重視するなら1クエリで抑えたいですね!

でもActiveRecord-importの出番なしに

Vote.where(id: [1, 2, 3]).update_all('counter = counter + 1')

と書くことが出来ます。あ…ココまではタイトル関係ないです、すみません。

さらに既存のレコードが無かったら追加したい場合は?

ただ…そこで既存のrecordが 既に存在しているか分からない 前提になるとこれでは通用しなくなりますね。

ここで考えたのが… insert ... on duplicate key update のMysQL構文を持ってくること。
となれば表題の通り ActiveRecord-import Gem の出番になります。

しかしドキュメントを読んでいる限りでは、 on duplicate key update counter = counter + 1 みたく、columnの値を含めた計算式を入れる方法が見当たりません。

最後の望みを託してソースコードを確認すると…
この部分でどうやらそのまま文字列の式を入れるとSQL文に組み込んでくれるように見えます!

ということで、このように書けば1クエリで実行することが出来ました!

records = ids.map do |id|
{ id: id, counter: 1 }
end

Vote.import! records, on_duplicate_key_update: 'counter=counter + 1'

ソースコードのリンクはMySQL用ですが他にPostgresSQLやSQlite用も同様の実装に見えました。

ただし仕様に載ってないということは、将来のバージョンアップでサイレントで実装が変わってしまう可能性はあるので要注意ですね。

Ruby Twitter gemで必要な数だけフォロー、フォロワーのidを取得する方法

Twitterでフォローやフォロワーのidを取得する時の制約

Twitterのフォロー、フォロワーidの一覧取得にはAPIリクエスト 1回で最大5000ユーザー、さらに各API 15分につきリクエストが最大15回までという制限があるため、75000以上のフォロー・フォロワーの全てを取得しようとすると最低でも15分以上かかるという問題が存在します。

後日・あるいは暫く経ってから結果を通知するシステムであれば特に問題はありませんが、できるだけすぐに表示をさせたい場合は予め最大取得数を制限したいですね。(75000ユーザー取得すると数〜数十秒かかりますので、必要性が薄ければ尚更もっと制限したいところ)
では、どのようにすれば必要な数だけ取得できるか次で紹介します。

Ruby Twitter gemで必要な分だけフォロワーを取得する方法

取得する時に、以下のように”friend_ids.take(10)”といった形でtake(取得数)を付けるだけです。
例:

client = Twitter::REST::Client.new do |config|
  config.consumer_key        = "YOUR_CONSUMER_KEY"
  config.consumer_secret     = "YOUR_CONSUMER_SECRET"
  config.access_token        = "YOUR_ACCESS_TOKEN"
  config.access_token_secret = "YOUR_ACCESS_SECRET"
end

#フォローしてる人のidを100人取得
a = client.friend_ids("調べたい人のscreen_nameまたはid").take(100)
#フォロワーのidを100人取得
b = client.follower_ids("調べたい人のscreen_nameまたはid").take(100)

なお、これはuser_timelineなど他のAPIでも応用が効くようです。

Twitterがサーバーダウンした時に出すTwitter APIのレスポンスを列挙してみる。[Ruby Twitter gem]

先日(2016/1/19)に大規模なTwitterのサーバー障害に見舞われました。その時に自分が運営していたサイトのログを確認してどのようなエラーがTwitter側から返ってきたか確認したいと思います。

なお、Ruby Twitter gemを使った上での結果です。

具体的にどのようなエラーが発生したか

Twitter::Error::ServiceUnavailable (Over capacity):
Twitter::Error::InternalServerError (Internal error):
Twitter::Error::RequestTimeout (Net::ReadTimeout):

までは分かるのですが、

Twitter::Error (784: unexpected token at '<!DOCTYPE html>

としまいにはJSONでなくHTMLを返されてしまったようで…鯖落ちした時のエラーのレスポンスの返り方はもう予測不可能ですねw

で、結局どう例外処理すればいいの?

結論としましては、

  1. まずはTwitter関連で発生したエラーのデフォルトの処理を記述する
    (内容はTwitterがサーバーダウンした場合を想定)
  2. TooManyRequests(API制限で利用不可)
    Unauthorized(鍵垢、トークン無効などの理由で読み取り不可)
    Forbidden(140字以上のツイートなどの理由で拒否)
    などユーザー側の操作の理由で発生したエラーの捕捉

をしたほうがいいと思います。

例えばRailsで書くとするとこんな感じでしょうか

class ApplicationController < ActionController::Base
  #rescue_from節は下のほうほど優先順位が高いため、Twitter::Errorの例外は一番上に書きそのサブクラスの例外を下に記述すること。
  rescue_from Twitter::Error, with: :twitter_server_error
  rescue_from Twitter::Error::TooManyRequests, with: :twitter_too_many_requests
  rescue_from Twitter::Error::Unauthorized, with: :twitter_unauthorized
  rescue_from Twitter::Error::Forbidden, with: :twitter_forbidden

  def twitter_server_error
   #Twitterが鯖落ちした時の処理を記述
  end

  #以下同様にしてエラー時の処理を行う各メソッドを記述
end