LoginSignup
7
6

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-04-10

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的な親モデルを作って置くなりしてお好きな様にご利用ください。

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

7
6
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6