ユーザーの1クリックで2000レコードINSERTするアクティビティ通知機能を作ったメモ

f:id:technuma:20170511213557p:plain

経緯

ブログサービス ( https://pressblog.me/ ) を運営していて、通知機能を開発することになりました。

通知機能の仕様

  • 「いいね!」を押したらフォロワーに通知
  • コメントしたらフォロワーに通知
  • フォロワーは現在、最大で2000人程度
  • すべてのログイン済みユーザーは、各々の通知ページからフォローしているユーザーのアクティビティ(いいね、コメント内容)を見ることができる
    通知画面↓
    f:id:technuma:20170511203741p:plain

環境

Rails 4.2
MySQL 5.6

大変だったところ

  • フォロワー数に比例してINSERT文を増やす必要があった
    例:2000フォロワーいるユーザーが、「いいね!」を押してコメントをしたら4000レコードINSERTする必要がある
    (eachで回して4000回save走らせたら mac book air のローカル環境で4分くらいかかってつらい)
  • ログイン済みユーザーであれば手軽なアクティビティ(いいね、コメント)でガンガンINSERTが走ってしまう

解決方法

activerecord-importというgemを利用して、BULK INSERTをすることで解決しました
(アドバイスいただいた先輩に圧倒的感謝)
validate が走る仕組みもあるものの、速度を重視するためvalidateなしでBULK INSERTする仕組みで実装しました。

github.com

wikiにexampleが書かれていて分かりやすかったです。
Home · zdennis/activerecord-import Wiki · GitHub

validationの true,false, ○件ずつ実行もできる例もあります。
Examples · zdennis/activerecord-import Wiki · GitHub

mac book airでいろいろ立ち上げながら計測していたのでいろいろスペック不足ではありますが、 ↓のように改善されました。

each で 4000回 save activerecord-import で BULK INSERT
4分程度 4秒

注意点

便利なgemではありますが、使い方を少し間違えると罠にハマるので以下注意です。

  • 内部でSQL文を結合して発行しているため、一度に実行できるSQL文の最大サイズを超えないように気をつける

MySQL :: MySQL 5.6 リファレンスマニュアル :: B.5.2.10 パケットが大きすぎます

  • Railsが提供している after_save, before_save, validate(オプションによる)が走らない

Railsで関連レコードの条件付き集計を行う

f:id:technuma:20170203230844p:plain

ブログサービスを作っているときに、1ユーザーあたりの投稿数(公開済み記事のみ)を取得したいというときに便利なgemがあったのでご紹介します!

環境

$ rails -v
Rails 4.2.5

目的

Railsで関連レコードの条件付き集計をしたい。

DB

user と post が1対多の構成です。
(user:1 post:多)

準備

Rails標準の機能の counter_cache は単純な集計しかできないため counter_culture を使います。

github.com

Gemfileにcounter_cultureを追記してインストール

# Gemfile
gem 'counter_culture'
bundle install

実装

is_draft が false かつ is_deleted が false のときにカウントするようにします。

counter_culture :user, column_name: proc {
  |model| (!model.is_draft && !model.is_deleted) ? 'posts_count' : nil
},
column_names: {
   ["posts.is_draft= ? AND posts.is_deleted= ?", false, false] => 'posts_count',
}

column_name は databaseにCOMMITがあったとき、 column_names は データ初期化するときに呼ばれます。

データ初期化

bin/rails c にて下記コマンドを実行

Post.counter_culture_fix_counts

書いた人

twitter.com

Railsユーザーなのに、まだstrftime使っているの?と思っていたら Rails の DATE_FORMATS でハマった話

TL;DR

  • strftime はクソ
  • DATE_FORMATSは型ごとに設定する必要がある
  • DATETIME型のデータは Time::DATE_FORMATS[:ymd] = ("%Y年%m月%d日")
  • DATE型のデータは Date::DATE_FORMATS[:ymd] = ("%Y年%m月%d日")

前提

以下、今日(2016年10月23日)作成されたユーザーの作成時間(user.created_at、DATETIME型)を整形するという流れで進めます。

strftime を使う場合

日付のデータをフォーマットするときは、 strftime 関数を使います。

user.created_at.strftime("%Y年%m月%d日") 
# => 2016年10月23日

ただ strftime を使ってしまうと都度フォーマットを設定するため、日付フォーマットがバラけてしまいます。 (rails date format などで検索すると上位に表示される方法なので注意しましょう。)

そこで 以下の方法を使います。

time_formats を使う

まず config/initializers/time_formatsに フォーマットを指定します。

# config/initializers/time_formats.rb

Time::DATE_FORMATS[:ymd] = ("%Y年%m月%d日") 

その後、

user.created_at.to_s(:ymd)
# => 2016年10月23日

とするとフォーマットがinitializers/time_formats.rbに集約されていい感じになります。

仮にユーザーの誕生日をフォーマットしたいとします。
誕生日は user.birthday(DATE型) に格納されています。

user.created_at のときに作ったフォーマットを使いまわして整形しようとすると・・・

user.birthday.to_s(:ymd)
# => 2016-10-23

フォーマットされていない・・・?

解決方法

原因は、 initializer の設定ミスでした。

先程設定した Time::DATE_FORMATS[:ymd] = ("%Y年%m月%d日")Time はdatetime型のみ有効です。

そこで、DATE型も有効化されるように追記します。

# config/initializers/time_formats.rb

Time::DATE_FORMATS[:ymd] = ("%Y年%m月%d日") 
Date::DATE_FORMATS[:ymd] = ("%Y年%m月%d日") 

この状態で再度 user.birthday をフォーマットすると

user.birthday.to_s(:ymd)
# => 2016年10月23日

無事できました。

所感

初見だとわかりにくいところだなと感じました。 30分くらいハマっていたのでメモしておきます。

WEBのことならなんでも聞いてください。↓

twitter.com