Tag Archives: ActiveRecord

ActiveRecord3.1/mysql2でTRUNCATE

DatabaseCleanerを使うと 複数データベース(各種RDBMSやMongoDB、CouchDBなどなど)に対応した方法でデータを削除できるんだけど、複数のDBにコネクションをはっている場合にはうまく動いてくれないことがあるらしい

でも自分の場合、mysql2でうまく動いてくれればそれでいいし、わざわざDatabaseCleanerを使う必要はないよなぁ…。ということで、ActiveRecord::ConnectionAdapters::Mysql2AdapterにTRUNCATEを発行してくれるメソッドを生やしてみた。実質DatabaseCleanerのコピペ。

module ActiveRecord
  module ConnectionAdapters
    class Mysql2Adapter < AbstractAdapter
      def truncate_tables
        tables_to_truncate.each do |table_name|
          truncate_table(table_name)
        end
      end
      def truncate_table(table_name)
        execute("TRUNCATE TABLE #{quote_table_name(table_name)};")
      end
      private
        def tables_to_truncate
          tables - views - ["schema_migrations"]
        end
        def views
          @views ||= select_values("select table_name from information_schema.views where table_schema = '#{current_database}'") rescue []
        end
    end
  end
end

次のようにtruncate_tablesを呼ぶと、ビューとschema_migrations以外に対してTRUNCATEを発行してくれる。

ActiveRecord::Base.connection.truncate_tables

ActiveRecord3.1/MySQLでBIGINT UNSIGNEDなid

ActiveRecord 3.1でMySQL2アダプタを使っている場合に、idカラム(プライマリキーのカラム)にBIGINT UNSIGNEDを使う方法。

まずCREATE TABLE時のidカラムの型を変更するために、ActiveRecordが読み込まれた直後、rakeタスク等でテーブルの作成が行われる前に次のように上書きする。

ActiveRecord::ConnectionAdapters::Mysql2Adapter::NATIVE_DATABASE_TYPES[:primary_key] = \
  "BIGINT UNSIGNED DEFAULT NULL auto_increment PRIMARY KEY"

あとは各マイグレーションファイルで、外部キー(*_idってやつ)のカラムの型にBIGINT UNSIGNEDを指定する。

t.column :user_id, "BIGINT UNSIGNED"

これでidカラムも*_idカラムもBIGINT UNSIGNEDで作られる。

もし既存のテーブルの型を書き換えたい場合には、

change_column :users, :id, "BIGINT UNSIGNED"

でいけると思う(未確認)。

ただ、BIGINT UNSIGNEDだと0から1846京まで使えるらしいので、DB単体では破綻するような気がする。

参考


ActiveRecordとMongoidは共存できる

第5回 MongoDB.jp 勉強会 in Tokyoに行ってきた! メモとかはまたのちほどアップするとして(そういえばRubyKaigi最終日のメモもアップできていない)、@yuki24 さんの発表で「ActiveRecordとMongoidは共存」できるという話を聞いて、ちょっと試してみた。

その過程で適当に作ったコードをgithubに上げておく。Todoを作成・更新した際にその内容をTodoLogに吐き出すというモノ。

mongoidのインストール

これは普通に。

$ gem install bson_ext mongoid
$ rails g mongoid:config

Mongoidのモデルの作成

mongoidをインストールするとmodelジェネレータがmongoidのモデルファイルを生成してくれる。

$ rails g model todo_log title:string done:boolean 
      invoke  mongoid
      create    app/models/todo_log.rb

ActiveRecordのモデルの作成

modelジェネレータがmongoidに割り振られたので、代わりにactive_record:modelジェネレータを使う。

$ rails g active_record:model todo title:string done:boolean
      create  db/migrate/20110730071022_create_todos.rb
      create  app/models/todo.rb

これはrails gコマンドの出力を見て初めて気づいた。

アプリでやってること

Todoのafter_create、after_updateコールバックにTodoLog.createを追加すれば、TodoLが作成・更新された際にTodoLogが作成される。

最後に

やってみれば当たり前なんだけどちゃんと動く。 mongoid入れただけでmodelがそれ用のジェネレータになっていたのでできないものだと思ってた。 調べてみるってとても大事。

あとスキーマレスは便利。ついさっき、「TodoLogなんだからtodo_id入れなきゃな」と気づいたのでコードをちょっとだけ修正したのだけれど、それだけでtodo_idが保存されるようになった。凄く手軽。 もちろん後から追加したんだから、それ以前のレコードにはtodo_idは保存されていないんだけど。


has_manyな関連先をまとめてINSERTする

fields_foraccepts_nested_attributes_forを使って、has_manyな関連先をまとめてINSERTする方法。ソースはgithubに上げておいた。ちなみにRails 3.0.8。

Post has_many Tags through Taggingsというモデルがあったとする。

Post has_many Tags through Taggings.

とりあえずscaffoldはこんな感じ。Taggingだけは画面が要らないのでmodelだけ。

$ rails g scaffold posts title:string text:text
$ rails g scaffold tags name:string
$ rails g model tagging post:references tag:references
$ rake db:migrate

コードの修正で重要なのは次の2点。

app/models/post.rb に accepts_nested_attributes_forを設定する。

class Post < ActiveRecord::Base
  has_many :taggings
  has_many :tags, :through => :taggings
  accepts_nested_attributes_for :taggings
end

これで次のようなコードが実行された時にTaggingもまとめて作ってくれるようになる。

Post.create(
  :title => "タイトル", :text => "本文",
  :taggings_attributes => [
    { :tag_id => 1 }, { :tag_id => 2 }
  ]
)

app/views/posts/_form.html に fields_for を使ってリレーション先についてのフォームを作る。第一引数がtaggings_attributes[]になっているのがポイント。これはaccepts_nested_attributes_forに合わせて設定する。

  <div class="field">
    Tags<br />
    <% @taggings.each do |tagging| %>
      <%= f.fields_for "taggings_attributes[]", tagging do |tf| %>
        <%= tf.select :tag_id, Tag.all.map { |x| [x.name, x.id] } %>
      <% end %>
    <% end %>
  </div>

なお、 @taggings はapp/controllers/posts_controller.rbで事前に用意しておく。 @postからリレーションで辿らないのは、新規にPostを作るフォームでは@post.taggingsが必ず空なのでeachが回らないから。

  # GET /posts/new
  # GET /posts/new.xml
  def new
    @post = Post.new
    @taggings = Array.new(3) { Tagging.new }

    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @post }
    end
  end

  # GET /posts/1/edit
  def edit
    @post = Post.find(params[:id])
    @taggings = @post.taggings
  end

あとはサーバを起動して/tagsからタグを登録し、/postsから記事を投稿すればok。 一応どんな感じの画面になるのかスクリーンショットを載せておこう。

作り込むなら、

  • accepts_nested_attributes_for のreject_ifオプションでtag_idがblankなら無視する
  • dependentにdestroyを設定する
  • Taggingでpost_idとtag_idのペアが一致するものが複数できないように制限をかける

などの点が気になるけど、今回は置いておく。

accepts_nested_attributes_for自体はhas_oneでも使えて、それに合わせてfields_forの引数も単数形にすれば良い。詳しくはリファレンスを参照。


named_scopeの自動生成+お手軽検索

@ukstudioさんの「scopeでお手軽検索」 をRails2.3向けに少し修正1しつつ、カラムごとに一致検索をするようなnamed_scopeを自動生成するようにフック部分にしかけをした。

例えばnameカラムとageカラムを持ったUserモデルについて、

class User < ActiveRecord::Base
  extend Searchable
end

とすると、次のような感じでカラムの条件で簡単に検索が可能になる。

User.name("jsmith").first #=> User named jsmith
User.search(:name => "jsmith", :age => 20..30)

追記

これだけだとwhereの導入された3.0では記法的なあまりメリットはないかもしれない。2.x系なら:conditionsをキーとしたハッシュが不要になるというメリットはあるけど。

やっぱり独自のnamed_scopeを作ってこそ便利になる気がする。


  1. Rails2.3だとscopedに続けてnamed_scopeのメソッドを呼び出すとエラーが出るらしく、injectの引数にはselfのみを渡すようにした。 


named_scopeの引数による条件分岐

named_scopeに引数を渡した場合、その型によって設定するSQLを動的に変更したいことがある。 こういうときにはlambdaに渡しているのブロック中で条件分岐すれば良い。


named_scope :member, lambda { |m|
  case m
  when Member
    { :conditions => { :member_id => m.id } }
  when Integer
    { :conditions => { :member_id => m } }
  else
    {}
  end
}

要するに、lambdaに渡したブロックの処理の結果、findに渡すHashパラメータが返ればそれでok、という感じなんじゃないかと。