UNIONを使いたいときはVIEWを作った方が手っ取り早い


ActiveRecordを使ってUNIONを実装しようとしていたのですが、どんどん複雑になっていきます。
それなら、VIEWを作った方が手っ取り早いです。

migrationファイルなど、SQLを直接記述するようなこともありますが、環境を意識しながら実装していくのであれば問題ないかと。

なお、Data Baseが
* MySQL
* PostgreSQL (native and pure ruby)
* SQL Server
であれば、rails_sql_viewsというgemもあります。
ですが、ここでは他のData Baseでも対応できるように、ベタなサンプルで実装しています。

実装環境

OS
CentOS 6.5
Ruby
2.1.5
Rails
4.2.0
DB
MySQL

サンプル用のテーブル作成

今回、サンプル用に作成するテーブルです。
わざわざ二つのテーブルに分ける必要は無いのですが、サンプル用ということで勘弁してください。

ユーザテーブルのmigrationファイルの作成
bundle exec rails g model user name:string email:string

ゲストテーブルのmigrationファイルの作成
bundle exec rails g model guest name:string email:string

VIEWの作成

サンプルのテーブルをUNION ALLして、一つのVIEWとします。
generateでは作成できないので、手動で実装します。
今回は、usersguestsを統合して、view_peopleというVIEWを作成します。
(ファイル名のxxxxxxxxxxxxxxの部分は適宜、年月日時分秒を指定してください。)

xxxxxxxxxxxxxx_create_view_people.rb
class CreateViewPeople < ActiveRecord::Migration
  DB_NAME = "view_people"   # VIEWの名称

  def up
    execute create_view_sql
  end

  def down
    execute drop_view_sql
  end

  def create_view_sql
    db_adapter = ActiveRecord::Base.connection_config[:adapter]

    case db_adapter
    when 'mysql2' then
      create_mysql_view_sql
    else
      raise Exception, "Not Support Data Base [#{db_adapter}]"
    end

  end

  def drop_view_sql
    "DROP VIEW #{DB_NAME}"
  end

  def create_mysql_view_sql
    "
      create or replace view #{DB_NAME}
      as
        select
          'user'  as tbl_type,
          name              ,
          email             ,
          created_at        ,
          updated_at
        from
          users
        union all
        select
          'guest' as tbl_type,
          name              ,
          email             ,
          created_at        ,
          updated_at
        from
          guests
      ;
    "
  end

end

さて、単純にdef upCREATE VIEWを書いても良いのですが、直接SQLを書くことを考慮して、Data Base別に実装できるようにしています。

ActiveRecord::Base.connection_config[:adapter]は、config/database.ymladapterを取得します。
これで実装相手のData Baseを判断して、それに合ったCREATE VIEWを生成します。

def downは、Data Base毎に書き方が変わることはないかと思い、そのまま書いています。
もし、違う場合があれば、def up同様、切り分けてください。

最後に、rake db:migrationをして、エラーが出なければOKです。

MODELの作成

上記でVIEWはできましたが、VIEWを参照するために、モデルを作成します。

このMODELは普通のテーブルとなんら変わりません。

app/models/view_person.rb
class ViewPerson < ActiveRecord::Base
end

これだけです。
(必要に応じて、メソッド等は追加してください。)

ここで注意が必要です。

こんなことをやってみました。

obj = ViewPerson.new
obj.tbl_type='guest'
obj.name='hoge'
obj.email='[email protected]'
obj.save

インスタンスから値の代入までは問題なく実行できるのですが、saveは失敗しました。
MySQLからERRORが戻ってきます。
つまり、UNIONを使用したVIEWでは保存処理はできません。

ですが、MySQLからERRORが戻ってきたということは、保存できるようなVIEWであれば、saveもできそうです。(未検証)

VIEWを使わなかったら

ActiveRecordでUNIONは以下のように書きます。

UNION

ViewPerson.from("#{User.select("'user' as tbl_type, name, email").union_all(Guest.select("'guest' as tbl_type, name, email")).to_sql} view_people")

もしくは

ViewPerson.from("#{Arel::Nodes::Union.new(User.select("'user' as tbl_type, name, email").ast, Guest.select("'guest' as tbl_type, name, email").ast).to_sql} view_people")

UNION ALL

ViewPerson.from("#{Arel::Nodes::UnionAll.new(User.select("'user' as tbl_type, name, email").ast, Guest.select("'guest' as tbl_type, name, email").ast).to_sql} view_people")

となります。