Railsで生のSQLにテーブル名やカラム名を動的に埋め込みたい場合はquoteメソッドを使おう

Railsでアプリケーション開発をしていると、深淵なる理由で生のSQLにテーブル名やカラム名を動的に埋め込みたいケースが出てくる。

その場合安全だと分かっていてもサニタイズしないでSQLに文字列を埋め込むのは気が引ける。

そういう場合は、 ActiveRecord::Base.connection.quote_table_name('table_name')ActiveRecord::Base.connection.quote_column_name('column_name') が使える。

他にもメソッドがあり、 http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Quoting.html をみるとよい。

#quote_table_name#quote_column_nameをかますとクォートで囲ってくれる。

もし文字列のなかにクォートがあれば以下のようにエスケープしてくれる。

pry(main)> ActiveRecord::Base.connection.quote_table_name('users')
=> "`users`"
pry(main)> ActiveRecord::Base.connection.quote_table_name('users`')
=> "`users```"

Railsで未実行のmigrationを調べたい時はrake db:migrate:statusを使うとよい

本番環境でrake db:migrateを実行する場合、はたしてどのmigrationが実行されるのか確認したくなる場合がある。

その場合、rake db:migrate:statusを実行すると未実行のmigrationが表示されて便利だ。

たとえば、以下のような場合downとなっているのが未実行のmigrationである。

% rake db:migrate:status

database: my_db

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20160704042124  Create users
   up     20160705054353  Create blogs
  down    20160722061711  Create categories

以前はdb/migration/以下のファイルと、dbのschema_migrationsテーブルの中身を比較していたが、こんな便利なコマンドがあったなんてな。。

RSpec Mocksで既存メソッドを上書く

RSpecで既存のとあるメソッドが呼びだされたら特定の値を返したい場合、

allow(model).to receive(:foo).and_return(14)   

みたいにant_returnすればよい。これは有名。

しかし深淵なる理由でメソッドを上書きたい場合がでてくる。テスト中に特定の値を返すのではなくオブジェクトの状態やメソッドの引数の値によって返す値を変えたいといった場合だ。

そういった場合は、 https://relishapp.com/rspec/rspec-mocks/v/3-5/docs/configuring-responses/block-implementation にあるように、

allow(model).to receive(:foo) do |arg|
 # テスト用の実装 
end 

のようにすると既存のメソッドを上書ける。

Railsでtrimするたった一つの冴えたやり方

trimしたいという欲求はアプリケーションを作る上で必ず出てくる。

そしてすぐにString#stripではダメだと気付く。全角スペースが削除されないからだ。 で、独自の正規表現を書いたりするんだけど、ActiveSupport内に便利な定数があるのでそれを使うと楽。

str.gsub(ActiveSupport::Multibyte::Unicode::LEADERS_PAT, '').gsub(ActiveSupport::Multibyte::Unicode::TRAILERS_PAT, '')

LEADERS_PATとか、TRAILERS_PATって何?って思った人は、 http://api.rubyonrails.org/classes/ActiveSupport/Multibyte/Unicode.html をみるか、ソースを見るといい。

要は、以下の文字がtrimされる。

      # All the unicode whitespace
      WHITESPACE = [
        (0x0009..0x000D).to_a, # White_Space # Cc   [5] <control-0009>..<control-000D>
        0x0020,                # White_Space # Zs       SPACE
        0x0085,                # White_Space # Cc       <control-0085>
        0x00A0,                # White_Space # Zs       NO-BREAK SPACE
        0x1680,                # White_Space # Zs       OGHAM SPACE MARK
        (0x2000..0x200A).to_a, # White_Space # Zs  [11] EN QUAD..HAIR SPACE
        0x2028,                # White_Space # Zl       LINE SEPARATOR
        0x2029,                # White_Space # Zp       PARAGRAPH SEPARATOR
        0x202F,                # White_Space # Zs       NARROW NO-BREAK SPACE
        0x205F,                # White_Space # Zs       MEDIUM MATHEMATICAL SPACE
        0x3000,                # White_Space # Zs       IDEOGRAPHIC SPACE
      ].flatten.freeze

Railsで複数DBのmigration

大規模なRailsアプリケーションだったり、旧来のシステムをRailsに置き換えたりしてると、どうしても複数DBを扱いたくなってくる。

その場合は例えば、以下のようなlib/tasks/database_foo.rakeを作成します。

namespace :foo do
  desc 'Configure the variables that rails need in order to look up for the db
    configuration in a different folder'
  task :set_custom_db_config_paths do
    ENV['SCHEMA']                                     = 'db_foo/schema.rb'
    ENV['DB_STRUCTURE']                               = 'db_foo/structure.sql'
    Rails.application.config.paths['db']              = ['db_foo']
    Rails.application.config.paths['db/migrate']      = ['db_foo/migrate']
    Rails.application.config.paths['db/seeds.rb']     = ['db_foo/seeds.rb']
    Rails.application.config.paths['config/database'] = ['config/database_foo.yml']
    ActiveRecord::Migrator.migrations_paths           = 'db_foo/migrate'

    # db_fooディレクトリを作りたくない場合は、以下のようにするとよい。
    # ENV['SCHEMA']                                     = 'db/schema_foo.rb'
    # ENV['DB_STRUCTURE']                               = 'db/structure_foo.sql'
    # Rails.application.config.paths['db']              = ['db']
    # Rails.application.config.paths['db/migrate']      = ['db/migrate_foo']
    # Rails.application.config.paths['db/seeds.rb']     = ['db/seeds_foo.rb']
    # Rails.application.config.paths['config/database'] = ['config/database_foo.yml']
    # ActiveRecord::Migrator.migrations_paths           = 'db/migrate_foo'
  end

  namespace :db do
    task drop: :set_custom_db_config_paths do
      Rake::Task['db:drop'].invoke
    end

    task create: :set_custom_db_config_paths do
      Rake::Task['db:create'].invoke
    end

    task migrate: :set_custom_db_config_paths do
      Rake::Task['db:migrate'].invoke
    end

    namespace :migrate do
      task up: :set_custom_db_config_paths do
        Rake::Task['db:migrate:up'].invoke
      end
      task down: :set_custom_db_config_paths do
        Rake::Task['db:migrate:down'].invoke
      end
    end

    task rollback: :set_custom_db_config_paths do
      Rake::Task['db:rollback'].invoke
    end

    task reset: :set_custom_db_config_paths do
      Rake::Task['db:drop'].invoke
      Rake::Task['db:create'].invoke
      Rake::Task['db:migrate'].invoke
    end

    namespace :schema do
      task dump: :set_custom_db_config_paths do
        Rake::Task['db:schema:dump'].invoke
      end
      task load: :set_custom_db_config_paths do
        Rake::Task['db:schema:load'].invoke
      end
    end

    namespace :structure do
      task dump: :set_custom_db_config_paths do
        Rake::Task['db:structure:dump'].invoke
      end
      task load: :set_custom_db_config_paths do
        Rake::Task['db:structure:load'].invoke
      end
    end

    task version: :set_custom_db_config_paths do
      Rake::Task['db:version'].invoke
    end

    task seed: :set_custom_db_config_paths do
      Rake::Task['db:seed'].invoke
    end

    namespace :test do
      task prepare: :set_custom_db_config_paths do
        Rake::Task['db:drop'].invoke
        Rake::Task['db:create'].invoke
        Rake::Task['db:migrate'].invoke
      end
    end
  end
end

すると、fooというDBに対して

  • rake foo:db:create
  • rake foo:db:drop
  • rake foo:db:migrate
  • rake foo:db:migrate:up
  • rake foo:db:migrate:down
  • rake foo:db:schema:dump
  • rake foo:db:schema:load

等々が実行できるようになります。

mail gemで、Content-Type: multipart/mixedとしたいけどならないときの対処法

mail gem使って、multipartメールでかつファイルが添付されたメールを送信したら、 Content-Type: multipart/mixedではなくContent-Type: multipart/alternativeになってしまい、 Thunderbirdでは添付ファイルが認識されず、MacメーラーではHTML本文が表示されない自体となってしまった。 別におかしなことはしていなくて、 https://github.com/mikel/mail のREADME通り、

@mail = Mail.new do
  to      'nicolas@test.lindsaar.net.au'
  from    'Mikel Lindsaar <mikel@test.lindsaar.net.au>'
  subject 'First multipart email sent with Mail'

  text_part do
    body 'Here is the attachment you wanted'
  end

  html_part do
    content_type 'text/html; charset=UTF-8'
    body '<h1>Funky Title</h1><p>Here is the attachment you wanted</p>'
  end

  add_file '/path/to/myfile.pdf'
end 

のようにしてたのだけど、なぜかダメだった。 で、issues漁っていたら、 https://github.com/mikel/mail/issues/590 が見つかって、そこで紹介されている https://gist.github.com/steve500002/bfc4b027d93c0c04f126 のようにしたら、Content-Type: multipart/mixed で送信されるようになった。

mail=Mail.new do |mail|
  to      'reipient@example.com'
  from    'Fred Flintstone <fred@flintstones.com>'
  subject 'Multipart email with attachment'
end

# Remember to end the plain text with a few blank lines. iOS devices often display the content of the attachments so you want to force them to go below the text

# Create the text and html as separate mail parts
text_body = Mail::Part.new do
    body "This is the message\n\n"
    content_type 'text/plain; '
end
html_body  = Mail::Part.new do
    body "<h1>This is HTML</h1>"
    content_type 'text/html; charset=UTF-8'
end

# Create a mail part to hold the html and plain text
bodypart = Mail::Part.new
bodypart.text_part = text_body
bodypart.html_part = html_body

# Add the html and plain text to the email
mail.add_part bodypart

# Add the attachment(s)
attachment = "../path/to/doc.pdf"
mail.attachments["#{File.basename(attachment)}"] = File.read(attachment)

いったん、 bodypart = Mail::Part.new として、 その後、 mail.add_part bodypartするのがポイント。

mail gemで、「Non US-ASCII detected and no charset defined. Defaulting to UTF-8, set your own if this is incorrect.」と出た場合

mail gemを使っていて

Non US-ASCII detected and no charset defined. Defaulting to UTF-8, 
set your own if this is incorrect.

の様な警告が出る場合は、

mail.text_part do
  body 'あああ'
  content_type 'text/plain; charset=UTF-8'
end
mail.html_part do
  body 'あああ'
  content_type 'text/html; charset=UTF-8'
end

とするか、もしくは

mail.text_part =  Mail::Part.new(body: 'あああ', charset: 'UTF-8')
mail.html_part =  Mail::Part.new(body: 'あああ', charset: 'UTF-8')

とすると、警告が出なくなる。

http://docs.komagata.org/4879 のようなやり方がネットで載っているが、mail gemのバージョンアップによってそれでは警告は消えなくなったもよう。