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的な親モデルを作って置くなりしてお好きな様にご利用ください。
(もっといい方法知ってるぜ!という方、ぜひ教えて下さい><)