ransacker_translatorを作った

数日前になりますが、ransacker_translatorというgemを作りました。
ransackで半角全角カナ、英数字、アルファベットの大文字小文字関係なく検索するためgemです。

https://github.com/patorash/ransacker_translator

ransackはransackerメソッドで検索条件を作ることができるので、それを使って上記の条件を作ります。
使い方は上記のリンクに書いてあるのでそれを見てもらえたらわかるかと思います。

この処理を作っていて、「これ絶対に同じようなことやってる人多いだろうなぁ…」と思ったので、とりあえずgemにしておきました。それにしてもgemは簡単に作って公開できるので本当に便利です。


Railsで既存のテーブルにてcouter_cacheを使うように修正する方法

Railsにはcounter_cacheというリレーション先のカウント数をキャッシュする仕組みがあります。これを使うと、リレーション先を参照してカウントせずに済むのでデータベースへのアクセス数が減って速くなります。いいですね〜。

で、これを使いたいと思うようになったので、入れてみました。当初、マイグレーション後の初期化方法を知らなかったので、とりあえずupdate_columnを使って更新していました。

class AddCommentsCountToPost < ActiveRecord::Migration
  def change
    add_column :posts, :comments_count, :integer, default: 0
    Post.find_each do |post|
      post.update_column :comments_count, post.comments.count
    end
  end
end

これでrake db:migrateした後、couter_cacheを使うように設定していました。

class Comment < ActiveRecord::Base
  belongs_to :post, counter_cache: true
end

その後、機能を作り終えてからレビュー用の環境にデプロイしたところ、なんと失敗!?

comments_count is marked as readonly

どうもcounter_cacheの設定を行うと読み込み専用の属性になるようで、update_columnでは更新できないと…。うーむ、設定する順番が悪かったのか…。となるとどうやって初期値を設定するのだろうか?と思ってぐぐったらやっぱりありました。

http://apidock.com/rails/ActiveRecord/Base/reset_counters/class

Post.reset_counters(id, :comments)

マイグレーションファイルを修正し直します。rake db:rollbackを先にやることをお忘れなく。

class AddCommentsCountToPost < ActiveRecord::Migration
  def up
    add_column :posts, :comments_count, :integer, default: 0
    Post.all.pluck(:id).each {|id| Post.reset_counters(id, :comments)}
  end

  def down
    remove_column :posts, :comments_count, :integer, default: 0
  end
end

そしてrake db:migrate。
これでうまく動くようになりました。


CanCan使用時にDeviseでタイムアウトしたときのflashを活かす

単純に自分のコードの書き方が悪かっただけなのですが…。
Deviseでタイムアウトした後にCanCanでアクセス権限チェックが働いて、アクセス権限がないというメッセージになっていました。タイムアウトならタイムアウトという通知じゃないとちょっとメッセージ的にキツいなと思っていたところでした(仕様上は問題ないんだけど)。

結局、CanCanの権限チェックでエラーになるので、そこで既にタイムアウトを検知していたらそのメッセージを使うようにすれば解決します。

まず、Deviseを使うmodel(Userとか)

class User < ActiveRecord::Base
  devise :timeoutable # 他のモジュールは適当に…
end

次にCanCanの設定ですが、CanCanのabilityは適当に書いてください。

次は、ApplicationControllerです。

class ApplicationController < ActionController::Base
  check_authorization unless: :devise_controller?

  # 該当箇所だけ抜粋
  rescue_from CanCan::AccessDenied do |exception|
    if current_user
      # 適当なpathを指定
      redirect_to mypage_index_path, alert: exception.message
    else
      if flash[:timedout]
        flash.keep(:alert)
        redirect_to new_user_session_path
      else
        flash.discard
        redirect_to root_url
      end
    end
  endend

flash[:timedout]がtrueだと、タイムアウトしているので、メッセージをflash.keep(:alert)で持続させてからリダイレクトさせます。それ以外の場合もありますが、今回は省略してエラーメッセージを消してトップページにリダイレクトしています。

ずっと気になっていたけれど優先順位を下げていた問題が解決したので、ほっと一安心。


HerokuのpostgresqlのためにカラムのCOLLATEをCにする

日本語圏の皆さん、こんにちは。

Herokuを使っていると、ローカル環境ではソートがうまくいくのに、本番環境ではうまくいかない…。そんなことが起きると思います。はい、それはpostgresqlのCOLLATEがen_US.UTF-8だからです!Cにしましょう、Cに。

しかし、後で気付いて変更しようにも結構面倒なので、migrationでやってしまいましょう。
新しいテーブルを追加した後でも、簡単に呼び出せるように、rake taskにしてみました。

作ったファイルは、

  • lib/collation.rb
  • lib/tasks/db.rake
  • マイグレーションファイル(rails g migration ChangeCollation)

の3つです。

実際に処理をするCollationクラスを作ります。

class Collation
  def initialize
    @connection = ActiveRecord::Base.connection
  end

  def change_all
    migration_base do |table, column|
      # 配列型に対応
      column_type = column.array ? "#{column.sql_type}[]" : column.sql_type
      @connection.execute "ALTER TABLE #{table} ALTER COLUMN #{column.name} TYPE #{column_type} COLLATE \"C\""
    end
  end

  def rollback_all
    migration_base do |table, column|
      # 配列型に対応
      column_type = column.array ? "#{column.sql_type}[]" : column.sql_type
      @connection.execute "ALTER TABLE #{table} ALTER COLUMN #{column.name} TYPE #{column_type}"
    end
  end

  private
  def migration_base
    @connection.tables.each do |table|
      begin
        model = Module.const_get(table.classify)
      rescue
        next
      end
      # 1からマイグレーションすると、schema cacheのせいで定義が古いまま。
      # 削除したり、リネームしたカラムを扱おうとして落ちるので、リセットする。
      model.connection.schema_cache.clear!
      model.reset_column_information
      model.columns.select {|column| column.type == :string || column.type == :text }.each do |column|
        yield(table, column)
      end
    end
  end
end

テーブル名一覧を取得し、そのテーブル名からモデルを取得します。gemによって作られたモデルによってはModule.const_getできないので、rescueで拾って次のモデルへ。テストの時に準備でmigrateしていると、モデルの定義が古いままで現在のカラム情報が取れなかったので困っていたのですが、schema_cache.clear!とreset_column_informationで現在の定義が取得できます。その後、テキストのカラムだけ取得して、yieldに投げます。

change_allとrollback_allでやってることは、COLLATEを設定しているかどうかの違いだけです。落とし穴だったのが配列型で、column.sql_typeだけでは型だけで配列になっていなかったのですが、column.arrayに配列型かどうかのbooleanを持っていたので、これで判定して配列型にしています。

rake taskで呼び出せるようにしておきます。

namespace :db do
  namespace :collation do
    task :change => :environment do
      Collation.new.change_all
    end
    task :rollback => :environment do
      Collation.new.rollback_all
    end
  end
end

最後に、migrationファイルを作ります。

class ChangeCollation < ActiveRecord::Migration
  def up
    Rake::Task['db:collation:change'].execute
  end

  def down
    Rake::Task['db:collation:rollback'].execute
  end
end

これで、もし後でテーブルを追加した場合でも、また次のマイグレーションの最後でこのrake taskを呼び出せば、大丈夫なはずです。


gem middleman-typescriptをリリースしました。

今日はOkayama.rbだったので久々にRubyっぽいことをやりました。
というか昨日の夜から作業してたやつの続きをしただけですが。

先日、有志でやってる読書会の教材としてどの本にしようか?という話をしていたのですが、それはUnity系にしようということに決まったのですが、個人的にはTypeScript気になるなぁと思っていたのです。

しかし、Railsに慣れてる人がTypeScriptをやろうと思うと結構めんどい。

RailsでTypeScriptをやるのは茨の道らしいというのはぐぐったらわかっていたのですが、最近typescript-railsというgemが更新されだしたらしいではないですか。といっても、Railsで新しいプロジェクトを作るんならいいけれど、簡単にTypeScriptを試したい人にとってはちょっと敷居が高いかなと思ったので、middlemanでTypeScriptを使えるようにしよう!と思った訳です。

typescript-railsが、typescript-nodeというgemを使っていたので、これを使うようにして、middlemanの拡張を作り始めました。middlemanのソースを読んだのですが、なかなかうまくいかず、最終的にはmiddleman-livereloadのソースを読んで、必要なところだけ抽出してmiddleman-typescriptを作りました。テストは(まだ)書いていません。バージョン0.0.1だから許して…。

github: okayamarb/middleman-typescript

使い方

まず、middlemanプロジェクトのGemfileに追記してください。

gem 'middleman-typescript'

そしてbundle installします。

そして、config.rbに以下を書きます。

configure :development do
  ignore "source/typescripts/*" 
  activate :livereload
  activate :typescript # デフォルトはtypescripts。オプションで typescript_dir: 'ts' で変更可能
end

そして、sourceディレクトリ以下にデフォルトならtypescriptsディレクトリを作ります。ここにtypespcriptファイル(*.ts)を置きます。あとはmiddleman sでサーバが起動している状態でtsファイルを作ると、js_dirに*.tsファイルから*.jsファイルがコンパイルされます。

ignore “source/typescripts/*”

としておくことで、middlemanのLiveReloadからtypescriptディレクトリを除外します。middleman-typescriptがsource/typescripts/*を監視しているので、LiveReloadで見る事はありません。というのも、middleman-typescriptがtsファイルからjsファイルを作ったタイミングでmiddleman-livereloadによってリロードされますから、除外しておかないとリロードしまくっておかしくなるのです。

ただ、jsファイルを実際に作るので、middlemanがデフォルトでサポートしているCoffeeScriptのようにはなりません。

これで自分的にもTypeScriptを勉強する環境が整ったはず!