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で複数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のバージョンアップによってそれでは警告は消えなくなったもよう。
perfectqueueを動かしてみる
Fluentdの作者の古橋さんが作って、Treasure Dataで使われているというメッセージキュー、 perfectqueue。 動かしてみようと思ったら全然ネットに資料がなかったので頑張って動かしてみました。 大きな特徴としてはMySQL上でキューイングシステムを構築するところでしょうか。
インストール
$ git clone https://github.com/treasure-data/perfectqueue.git $ cd perfectqueue $ bundle install
設定
$ mysql -h localhost -u root -e 'create database perfectqueue;' $ mkdir config $ cat config/perfectqueue.yml #以下のような内容でファイル作成 development: type: rdb_compat url: mysql2://root:@localhost:3306/perfectqueue table: queues $ bundle exec perfectqueue init # テーブルを初期構築
実装
$ cat app/workers/dispatch/test_handler.rb class TestHandler < PerfectQueue::Application::Base # implement run method def run # do something ... puts "#### acquired task: #{task.inspect}" # call task.finish!, task.retry! or task.release! task.finish! end end
$ cat app/workers/dispatch/dispatch.rb $:.unshift File.dirname(__FILE__) require 'test_handler.rb' class Dispatch < PerfectQueue::Application::Dispatch # describe routing route "type1" => TestHandler route /^regexp-.*$/ => :TestHandler # String or Regexp => Class or Symbol end
キューにメッセージを詰めてみる
$ bundle exec perfectqueue submit k1 type1 '{"uid":1}' -u user_1
$ bundle exec perfectqueue list
key type user status created_at timeout data
k1 user_task user_1 waiting 2016-03-10 22:15:12 +0900 2016-03-10 22:15:12 +0900 {"uid"=>1}
perfectqueueを起動させる
$ bundle exec perfectqueue run -I. -rapp/workers/dispatch/dispatch.rb Dispatch
I, [2016-03-10T22:21:11.842687 #3933] INFO -- : PerfectQueue 0.8.45
I, [2016-03-10T22:21:11.852560 #3933] INFO -- : Worker process started. pid=3934
I, [2016-03-10T22:21:11.972423 #3934] INFO -- : acquired task task=k1 id=1
#### acquired task: #<PerfectQueue::AcquiredTask @key="k1" @attributes={:status=>:running, :created_at=>1457616062, :data=>{"uid"=>1}, :type=>"type1", :user=>"user_1", :timeout=>1457616062, :max_running=>nil, :message=>nil, :node=>nil}>
I, [2016-03-10T22:21:11.973519 #3934] INFO -- : finished task=k1
I, [2016-03-10T22:21:11.974812 #3934] INFO -- : completed processing task=k2 id=1:
$ bundle exec perfectqueue list
key type user status created_at timeout data
k1 type1 finished 1984-07-02 20:39:31 +0900 {"uid"=>1}
起動中にキューを追加
$ bundle exec perfectqueue submit k5 type1 '{"uid":5}' -u user_5
I, [2016-03-10T22:26:43.535768 #4234] INFO -- : acquired task task=k5 id=1
#### acquired task: #<PerfectQueue::AcquiredTask @key="k5" @attributes={:status=>:running, :created_at=>1457616402, :data=>{"uid"=>5}, :type=>"type1", :user=>"user_5", :timeout=>1457616402, :max_running=>nil, :message=>nil, :node=>nil}>
I, [2016-03-10T22:26:43.536079 #4234] INFO -- : finished task=k5
I, [2016-03-10T22:26:43.544347 #4234] INFO -- : completed processing task=k5 id=1:
ちゃんと処理されてますね。