手抜き全文検索(LIKE '%foo%')したい時の書き方。

like用のsanitizeするメソッドをいつも忘れてググるので。

scopeは返り値がnilの場合all扱いになるので都合がいい。

class User < ApplicationRecord
  scope :search, -> (keyword) {                                                                                                                                                       
    where('name like ?', "%#{sanitize_sql_like(keyword)}%")                                                                                                                           
  }
end

config/application.rbからは設定がなくなっている。

# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.

上記のようにconfig/initializersに置けとのことなので置く。

config/initializers/i18n.rb:

Rails.application.config.i18n.default_locale = :ja

いつも忘れてググるので。

下記のようにmigrationを作るとcreate_join_tableを使ってくれる。

$ rails g migration create_join_table_user_team user team

下記のような内容でできる。

class CreateJoinTableUserTeam < ActiveRecord::Migration[5.0]
  def change
    create_join_table :users, :teams do |t|
      # t.index [:user_id, :team_id]  
      # t.index [:team_id, :user_id]  
    end
  end
end

indexは必要だったら作れということでしょう。(優しい)

create_join_tableの良いところはテーブル名の順番をきにしなくて良いところです。この例でもuser、teamという順番で指定してるけど実際のテーブルは下記のようにteams_usersになってる。

# \d teams_users
  Table "public.teams_users"
 Column  |  Type   | Modifiers 
---------+---------+-----------
 user_id | integer | not null
 team_id | integer | not null

エラー管理サービス、Airbrakeが有名ですが、怖話ではRollbarを使っています。(確か無料枠の広さで選んだ)

cronで動いているバッチ(rails runner利用)などはrack middlewareを通さないのでエラー拾わず困っていましたが、rollbarではrollbar-rails-runnerコマンドを使えばよいみたいです。(単にevalしてる)

$ RAILS_ENV=production bundle exec rollbar-rails-runner 'puts User.count'

rollbar/rollbar-gem: Exception tracking and logging from Ruby to Rollbar

便利。

デフォルトになってるから。素晴らしい。

Rails 5 support by marcroberts · Pull Request #46 · evrone/quiet_assets

長いことrailsばっかりやってると他人のコードを「Railsのレール(流儀)にどれだけ乗れてるか、どれだけ流行りの書き方してるか」だけで判断しがち。

見慣れない書き方だったり、他の言語っぽい書き方だったりするとボロカスに評価しているのを見かける。

Railsに浸かり過ぎて、Railsっぽくないもの=悪。レールから外れている=ゴミ。そういった判断は楽だしRailsプロジェクトにおいては大抵あってる(Railsを理解するのを面倒臭がっているゴミコード)んだけど、対象のコードが持つ価値(どんなことをどういう方法で解決してるのか)を判断する力が衰える気がする。

Railsのレールや最近の流行りの書き方とは違うけど、一貫性のあるコードだったり、不具合が出づらく変更に強いコードってのはある。

特にRails経験は浅いけど他の言語・フレームワークに習熟してる人のコードにそういうものが多い。

手癖でやってるとそういう筋肉が落ちる。自戒を込めて。

railsでコメント数の実装の悩み - komagataのブログ

コメント欄で色々アドバイスいただきました。ありがとうございます。

そもそも独立した二つの問題を一度に扱っていました。

  1. count数が遅い問題。
  2. 削除時のdependent: destoryが遅い問題。

counter_cacheやredis云々は1の話で僕の直近の問題である2とは関係無いですね。

2についてはdependent: :delete_all使え!で答えかと思います。(user → post → commentのように多段になってる場合はdelete_allではcallbackが動かないので手でやるべき)

1の方が大きなサービスを作っている方々が気になる問題かと思いますが怖話の規模ではconditional_counter_cacheで十分なので問題になったら考えたいと思います。

逆にdependent: destroyって面倒な処理が宣言的に書けてスゴイなとおもいます。この自動感を追い求めてDBのトリガーとかに解決策を見出してしまうと筋悪臭がすごいので素直に手で書きます。

railsでコメント数をどう実装するかで悩んでいます。

https://gyazo.com/c7ac20038c53ad302091f718fd948d15

ニコ動だとこんな感じ。

https://gyazo.com/a124a4bbf487715768730ec4aea9d810

怖話でもこんな感じでコメント数/再生数などを保存しています。

一定以上の数になると素朴な処理では速度的に無理が来ます。

怖話での実装方法

怖話でのカウント数の実装の歴史。

  1. 何もせずDBに1レコードずつ保存時代。
  2. 一覧ページなどが重いのでrailsのcounter cahcheを使う。
  3. コメントや閲覧はpolymorphic関連なのでcounter cacheが対応してない。conditional_counter_cacheを使う。
  4. 削除に時間がかかり過ぎる。 ← イマココ

一覧の表示はconditional_counter_cacheで大丈夫なのですが、削除時の処理がかかりすぎてタイムアップする問題がでてきました。

削除時の問題

ここではわかりやすく閲覧数ではなくコメント数で説明します。

コメント数が1万件ある話を削除すると下記のような処理が走ります。

  1. storyを削除する。
  2. dependent: :destroyで依存するコメント1が自動的に削除される。
  3. コメントが削除されたのでstoryのcomments_countが-1でUPDATEされる。
  4. dependent: :destroyで依存するコメント2が自動的に削除される。
  5. コメントが削除されたのでstoryのcomments_countが-1でUPDATEされる。
  6. 以下1万回繰り返し

1件につき100msだとしても1000sかかるので無理がある。

これまでの考え

  • memcacheやredisを使うのは手間なので無理が来るまで避けたい。(開発環境の構築の手間が増える)
  • ランキングで集計するので時間の情報は欲しい。
  • 一番楽な方法で実装しよう。

今の考え

  • RDBは無理がある。memcacheやredis、他のストレージもやむなし。
  • rails的に一般的な実装はなんだろう?
  • ありがちな問題なので一般的な対処方法を構築したい。

皆さんこういうのどう実装されてますか?こんな風にやってるよという方がいらっしゃったら @komagata などにメッセージいただけるとありがたいです。 :bow:

更新:railsでコメント数の実装の悩み〜解決編〜 - komagataのブログ

怖話でネストしたリソースのクラス名がぶつかってしまうので、どうつけるかで悩んでいます。

ぶつかるページ

  1. story 1個づつに紐づくcomments(怖い話1のコメント一覧)
  2. なんらかのstoryに紐づくcomments(怖い話のコメント一覧)

story(怖い話)以外にもcomic(ホラー漫画)とかurban_legend(都市伝説)とかあり、それぞれにもcommentがつく。

# config/routes.rb
Rails.application.routes.draw do
  namespace :stories do
    resources :comments, only: :index
  end

  resources :stories, only: :index do
    resources :comments, only: :index, controller: 'stories/comments'
  end
end
% rake routes        
        Prefix Verb URI Pattern                        Controller#Action
stories_comments GET  /stories/comments          stories/comments#index
 story_comments GET  /stories/:post_id/comments stories/comments#index
         posts GET  /stories                   stories#index

URLの意味的にどちらも上記のようにしたいが、commentsコントローラーが被ってしまう。

これを避けてnamespace無しのcommentsコントローラーを使うと今度はcomicのcommentなどとかぶってしまう。

それを避けるにはAllCommentsとかCommentsByStoryなどといったダサい名前しか思いつかない。まいったなあ。

RailsGuidesにちゃんと書いてありますが、ちょっとハマりました。

こういうのはダメ。

Rails.application.routes.draw do
  constraints subdomain: :api do
    scope module: :api do
      namespace :v1, format: :json do
        resources :posts
      end
    end
  end
end

ちゃんと文字列にする。

Rails.application.routes.draw do
  constraints subdomain: 'api' do
    scope module: 'api' do
      namespace 'v1', format: 'json' do
        resources :posts
      end
    end
  end
end

3.9 リクエスト内容に応じて制限を加える

リクエストベースの制限は、Requestオブジェクトに対してあるメソッドを呼び出すことで実行されます。メソッド呼び出し時にハッシュキーと同じ名前をメソッドに渡し、返された値をハッシュ値と比較します。従って、制限された値は、対応するRequestオブジェクトメソッドが返す型と一致する必要があります。たとえば、constraints: { subdomain: 'api' }という制限はapiサブドメインに期待どおりマッチしますが、constraints: { subdomain: :api }のようにシンボルを使用した場合はapiサブドメインに一致しません。request.subdomainが返す'api'は文字列型であるためです。

ドメインやPATHなど、文字列っぽいものは文字列で指定すべきと考えておけば良さそうです。