Rails4 ActiveRecordでatomicなSQLでincrementするハック

  • 8
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

Rails4 ActiveRecordでatomicにincrementしたい

ボタンが押された回数をカウントしたり、特定ページのPVをカウントしたりと、大量にカウントしたい場合、とか、こういうatomicなSQLで効率よくカウントしたいですよね。

こんなSQL
UPDATE `pages` SET `viewed_number` = COALESCE(`viewed_number`, 0) + 1, `updated_at` = now() WHERE `pages`.`id` = 1

普通にincrement!を使うとカウントが漏れる

RailsというかActiveRecordをみると、「increment!」っていう如何にもなネーミングのメソッドがありまして、これでいいだろうと思っていたのですが、、、、呼び出し時のSQLをよく見てみると

increment!によるSQL
UPDATE `pages` SET `viewed_number` = 13, `updated_at` = '2015-04-10 07:36:24' WHERE `pages`.`id` = 1

だめじゃん。。。
アップデートされる数字がmodel内の数字に1を足しただけなので、以下のようなケースでカウント漏れが、、、

こんなケース
page   = Page.find(1)            #viewed_number 1
page_2 = Page.find(1)            #viewed_number 1
page.  increment! :viewed_number #viewed_number 2
page_2.increment! :viewed_number #viewed_number 2

おしいんだけどなー。。

作りました

そこで、atomicなincrementをするメソッドを用意しました。

initializer/add_increment_atomic
module ActiveRecord
  class Base
    def increment_atomic!(attribute, by = 1, touch = true)
      operator = by < 0 ? '-' : '+'
      quoted_column = self.class.connection.quote_column_name(attribute)
      updates = ["#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{by.abs}"]
      updates << "#{self.class.connection.quote_column_name(:updated_at)} = now()" if touch
      self.class.unscoped.where(self.class.primary_key => id).update_all updates.join(', ')
      reload
    end
  end
end

最初はincrement!メソッドを上書きしちゃおうかと思ったのですが、影響範囲が読めないので別名にしておきました。

initializerに置くなり、上記のdef部分だけApplicationModel的な親モデルを作って置くなりしてお好きな様にご利用ください。

(もっといい方法知ってるぜ!という方、ぜひ教えて下さい><)