ラベル activerecord の投稿を表示しています。 すべての投稿を表示
ラベル activerecord の投稿を表示しています。 すべての投稿を表示

2014/01/26

rails+MySQLでdouble型カラムを扱うモンキーパッチ

背景

ruby2.1とrails4を使ってアプリを作っている。DBはMySQLを使っていて、わけあってあるテーブルの列をdouble型にしたいと思って。 カラムの列を変更するmigrationを作ろうと思った時に、そもそもdouble型のカラムを作るにはどうするんだっけ、ってなって、 Webを調べてみて、解がみつかったので試したところ、ちょっと不都合が生じて、それじゃあモンキーパッチを書いてみよう、という話です。

MySQLのdouble型を利用するmigration

まず、MySQLのdouble型の列を使うには、

add_column :table_name, :column, :float, limit: 53
のようなmigrationを書くとできる。(これは、既存のテーブルにカラムを追加するmigration)

型に:floatを指定して、:limitに53を指定する。参考にしたのは、

limit: 53は、MySQLのfloat型に精度を指定して定義する際に、精度が24〜53の場合は倍精度な浮動小数点型として扱われ、すなわちdouble型になるから、という風に理解している。(MySQL :: MySQL 5.6 Reference Manual :: 11.2.3 Floating-Point Types (Approximate Value) - FLOAT, DOUBLE)

問題点

上述のmigrationでカラムを追加すると、確かにdouble型のカラムになる。が、問題点が1つ。bundle exec rake db:migrateした時に、db/schema.rbが更新されるんだけど、このファイル内の当該カラムには、limit: 53が指定されない。結果、bundle exec rake db:test:prepareで、テスト環境のスキーマを更新した時に、当該カラムの型はfloatとして作成されてしまい、期待どおりにデータが扱えなくなってしまう。

ついでにいうと、本来double型を使いたいのに、:float, limit: 53って書くのもの、ちょっと違和感ありますね。

モンキーパッチ

db/schema.rbを手で直すってのも、あれなんで、なんとかならないものか、思って調べ始めたら、次のようなページを発見。

このページのハックを適用すると、
#add_column :table_name, :column, :float, limit: 53
# が↓のように書けるようになる。
add_column :table_name, :column, :double
となり、問題点の2つ目に対する解になりそう。が、db/schema.rbの問題は解決しないなぁ。もうちょっと調べるか、と思ってrailsのソースをみてたら、ActiveRecord::ConnectionAdapters::Columnというクラスのextract_limit(sql_type)というメソッドをみつけた。このメソッドは、どうやらSQLの型をみて、db/schema.rbを吐き出すときに:limitに何を指定するかを判断するものっぽい。各RDBMS用のアダプターでこれらをオーバーライドしているし。

ということで、次のようなモンキーパッチをconfig/initializers/add_double_to_activerecord.rbとして、作っておくことに。

module CustomColumnTypes 
  def double(*args)
    if args.last.is_a? Hash
      args.last[:limit] = 53
      args.last[:null]  = true
    end
    float *args
  end
end
module ColumnWithDouble
  def extract_limit(sql_type)
    case sql_type
    when /double/i
      53
    else
      $1.to_i if sql_type =~ /\((.*)\)/
    end
  end
end
module ActiveRecord
  module ConnectionAdapters
    class Table
      include CustomColumnTypes
    end
    class TableDefinition
      include CustomColumnTypes
    end
    class Column
      prepend ColumnWithDouble
    end
  end
end

これでdb/schema.rbにも無事にlimit: 53が出力されるようになった。

課題

上のパッチでは、ActiveRecord::ConnectionAdapters::Columnに対して振る舞いを変えているんだけど、僕がdoubleをサポートしたいのはMySQLの場合のだけだから、実はうまくない。なので、MySQL用のアダプタをイジる方法に変えるべきだなぁ、と思ってます。

2013/10/26

Rails4からActiveRecordのorderにハッシュを渡せるようなった。

以前、[rails]ActiveRecordのソートという記事を書いた。あれからおよそ1年半の間にRails4がリリースされ、まさにこのソートの機能も改良されたっぽい。

order(*args)

ActiveRecordのソート処理は、ActiveRecord::QueryMethods.orderで実装されている。これまでは、

User.order('name')
=> SELECT "users".* FROM "users" ORDER BY name

User.order('name DESC')
=> SELECT "users".* FROM "users" ORDER BY name DESC

User.order('name DESC, email')
=> SELECT "users".* FROM "users" ORDER BY name DESC, email

User.order(:name)
=> SELECT "users".* FROM "users" ORDER BY "users"."name" ASC

しかなかったのに対し、Rails4から、
User.order(email: :desc)
=> SELECT "users".* FROM "users" ORDER BY "users"."email" DESC

User.order(:name, email: :desc)
=> SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC

のように、ハッシュでソート条件を指定できるようになった。
おかけで、降順もスマートに指定できるね。

2012/12/29

[rails]レガシーなDBにつなぐ際のRakeタスク

Railsアプリを開発する際に、レガシーなDBを扱う場合を考えます。
この場合のアプリケーションはDBのスキーマ情報を定義しないため、RSpecを導入してrake specを実行した場合、 schema.rbが空であるため、db系のrakeタスクが動かずに、エラーとなります。
これを回避するために、db系のrakeタスクをアプリケーションで上書きして、 何もしないタスクに置き換えることができます。

アプリケーション定義Rakeタスクファイルとして、

{Rails.root}/lib/tasks/db.rake
を作成し、次のように実装します。

module Rake::TaskManager
  def remove_task(task_name)
    @tasks.delete(task_name.to_s)
  end
end

Rake.application.remove_task "db:create"
Rake.application.remove_task "db:drop"
Rake.application.remove_task "db:fixtures:load"
Rake.application.remove_task "db:migrate"
Rake.application.remove_task "db:migrate:status"
Rake.application.remove_task "db:rollback"
Rake.application.remove_task "db:schema:dump"
Rake.application.remove_task "db:schema:load"
Rake.application.remove_task "db:seed"
Rake.application.remove_task "db:setup"
Rake.application.remove_task "db:structure:dump"
Rake.application.remove_task "db:test:prepare"

namespace :db do
  desc "do nothing."
  task :create => :environment do
    puts "This task does nothing."
  end

  : 以下、全てのタスクを空に。
end

rake -Tを実行すると、db系タスクが上書きできていることが確認できます。 これで、レガシーDBを扱うアプリケーションでも、DBを変更する危険もなく、 RSpecもスムーズに実行することができます。

2012/07/12

[rails] rspecでDBのロールバックをテストする。

Railsアプリで、ActiveRecord::Base.transactionを使って、 トランザクションを明示的に張り、異常がある場合にロールバックする、 という実装は大いに有りますよね。

僕はRSpecを使ってテストしています。
RSpecを使って、DBがロールバックされることを確認する方法を書いておきます。

例えば、コントローラに次のような処理を書いたとします。
モデルの変更後、メールを送信する、という処理で、ここでは、メールの送信に失敗した場合は、 変更をロールバックする、という仕様とします。(その仕様は果たして妥当か?はここでは考えない。)


def save_and_send_mail(model)
  begin
    ActiveRecord::Base.tansaction do
      model.save!
      SomeMailer.something.deliver
    end
  rescue => e
    logger.error(e.message)
    logger.error(e.backtrace)
  end
end

「メールの送信に失敗したら、DBをロールバックする」という振る舞いを確認するために、 実行前後でデータを引っ張り出して比較するというのも出来なくはなさそうですが、
ちょっと面倒に思えます。

なので、「ロールバックする」メソッドが呼ばれることを確認することにました。
この「ロールバックする」メソッドとは、

  • ActiveRecord::Base.connection.rollback_db_transaction
にあたるので、

describe 'some method' do

  before do
    # 前処理
  end

  it "should rollback transaction if fail to send mail." do
    # 送信を失敗させる。
    SomeMailer.stub(:something).and_raise StandardError
    # ロールバック処理が呼ばれること
    ActiveRecord::Base.connection.should_receive(:rollback_db_transaction).once
    # 実行
    @controller.save_and_send_mail(model)
  end

end

というexampleが書けます。
これで、RSpecを使って、簡単にロールバックのテストができます。

2012/05/29

[rails]ActiveRecordのソート

ActiveRecordには、ソートに関するメソッドが3つあります。

  • order
  • reorder
  • reverse_order

order

指定した文字列、もしくはシンボルでソートします。


Sample.order(:created_at)                 #=> ORDER BY created_at
Sample.order("created_at")                #=> ORDER BY created_at
Sample.order("created_at ASC, name DESC") #=> ORDER BY created_at ASC, name DESC

reorder

デフォルトスコープで指定したソート順を上書きし、reorderで指定したソート条件を適用します。


class Post < ActiveRecord::Base
  ..
  ..
  has_many :comments, :order => 'posted_at DESC'
end
 
Post.find(10).comments.reorder('name')

reverse_order

指定したソートの昇順・降順を入れ替えてソートします。 (以前こっちに書きましたが・・)


Sample.order(:created_at).reverse_order #=> ORDER BY created_at DESC

別段これは!ということは書いていませんが、reorderの存在を最近知ったので、 ちょっとまとめてみました。

そういえば、第4版が出ていましたね。

2013/11/01 追記。
Rails4におけるソートの新機能について、Rails4からActiveRecordのorderにハッシュを渡せるようなった。に書きました。

2012/04/24

[rails]establish_connectionで接続先DBを変更する。

開発中の一連のシステムは、Rails3によるアプリを複数立ち上げ、 全て同じデータベースを参照している。
データベースは、その中の1つが管理(schemaや、migration)し、 他のアプリは、そのデータベースに接続する。

データベースを管理しないアプリ側では以下のコードを実装し、 接続先を変更する。
{Rails.root}/lib/custom_connection.rb

  module CustomConnection

    def establish_connection_to_other
      case Rails.env
      when "development"
        establish_connection :other_development
      when "test"
        establish_connection :other_test
      when "staging"
        establish_connection :other_staging
      when "production"
        establish_connection :other_production
      end
    end

  end
  ActiveRecord::Base.send(:extend, CustomConnection)

{Rails.root}/config/initializers/config.rb

  require 'custom_connection'
  ActiveRecord::Base.establish_connection_to_other

config/database.ymlには、 other_development, other_test, other_staging, other_productionの 定義が必要になる。