2012年11月19日月曜日

画面オープン直後にProcessing.jsのスクリプトを実行するサンプル

jQueryで画面のオープン直後に処理を実行する場合は、$(document).ready を利用することが一般的なようだが、このイベントに合わせてProcessingのスクリプトを実行しようとしてもうまくいかない。

具体的には、下記のようなソースコードでは、
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<script type="text/javascript" src="jquery.js"></script>
<script src="processing-1.4.1.js" type="text/javascript"></script>
<script type="text/processing" data-processing-target="mycanvas">
void setup()
{
  size(200, 200);
  colorMode(RGB, 100);
  noLoop();
}

void draw()
{
  fill(90);
  ellipse(70, 70, 100, 100);
}

void addEllipse()
{
  fill(60);
  ellipse(90, 90, 70, 70);
}
</script>
<script type="text/javascript">
$(document).ready(function(){
  var p= Processing.getInstanceById("mycanvas");
  p.addEllipse();
})
</script>
</head>
<body>
  <p>
  <canvas id="mycanvas"></canvas>
  </p>
  </body>
</html>
次のようなエラーメッセ―ジが表示されてしまう。


$(document).ready イベントでは、画面上のオブジェクトの読み込みがすべて完了した時点ではなく、画面上のDOM(Document Object Model)の読み込みが完了した時点、つまり、HTMLのソースコードの読み込みが完了した時点となる。

このタイミングでは、Processingの画面描画が完了していないので、Processingの関数を呼ぼうとしても、エラーになってしまうようだ。

$(document).ready の代わりに、$(window).load を使ったらうまくいった。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<script type="text/javascript" src="jquery.js"></script>
<script src="processing-1.4.1.js" type="text/javascript"></script>
<script type="text/processing" data-processing-target="mycanvas">
void setup()
{
  size(200, 200);
  colorMode(RGB, 100);
  noLoop();
}

void draw()
{
  fill(90);
  ellipse(70, 70, 100, 100);
}

void addEllipse()
{
  fill(60);
  ellipse(90, 90, 70, 70);
}
</script>
<script type="text/javascript">
$(window).load(function(){
  var p= Processing.getInstanceById("mycanvas");
  p.addEllipse();
})
</script>
</head>
<body>
  <p>
  <canvas id="mycanvas"></canvas>
  </p>
  </body>
</html>

2012年11月12日月曜日

Railsでリストボックスを使って親子テーブルを同時に更新するサンプル(インタフェース改良版)


前回作成したサンプルで、一応リストボックスとしては機能しているものの、登録件数が多くなると何が選択されていて、何が選択されていないかがわかりにくくなる。

下記のようなインタフェースで、左側のリストボックスが未登録、右側のリストボックスが登録済となるように分割して表示したい。


まずは再改良版のGameクラスとLineupクラスのscaffold。(実行結果は省略。)
rails g scaffold bestgame date:date
rails g scaffold bestlineup bestgame:references player:references
rake db:migrate
Gameクラスの_form.html.erbは、下記のように修正する。
<%= form_for(@bestgame) do |f| %>
  <% if @bestgame.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@bestgame.errors.count, "error") %> prohibited this bestgame from being saved:</h2>

      <ul>
      <% @bestgame.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :date %><br />
    <%= f.date_select :date, :use_month_numbers => true %>
  </div>
  <div class="field">
    <table>
      <tr><td align="center">
        <%= f.label "Bench Lineups" %><br />
        <%= f.select(:non_lineup_ids, @non_lineup_select_data, {}, {:multiple => true, :size => Player.all.size}) %>
      </td><td>
        <input id="btnMoveRight" type="button" value="->" onClick="moveItems('right')" />  
        <br />  
        <input id="btnMoveLeft" type="button" value="<-" onClick="moveItems('left')" />
        </td><td align="center">
        <%= f.label "Starting Lineups" %><br />
        <%= f.select(:player_ids, @lineup_select_data, {}, {:multiple => true, :size => Player.all.size}) %>
      </td></tr>
      </table>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
左側のリストボックスは、Lineupクラスに登録されていないPlayerを表示する。>
右側のリストボックスは、Lineupクラスに登録されているPlayerを表示する。
右側のリストボックスの送信データのみ、model側で扱うようにする。

左右のリストボックスのデータの移動は、矢印のボタンを押すと、下記のJavaScriptの関数が呼ばれるようにする。
// リストボックスの項目選択
function moveItems(direction) {
  
  var leftBox = document.getElementById("bestgame_non_lineup_ids");  
  var rightBox = document.getElementById("bestgame_player_ids");   
  var fromBox, toBox;  

  if (direction == "right") {  
    fromBox = leftBox; toBox = rightBox;  
  }   
  else if (direction == "left") {  
    fromBox = rightBox; toBox = leftBox;  
  }  
    
  if ((fromBox != null) && (toBox != null)) {   
    if(fromBox.length < 1) {  
      alert("リストボックスにアイテムがありません!");  
      return false;  
    }  
    if(fromBox.selectedIndex == -1) {  
      alert("移動するアイテムを選択してください!");  
      return false;  
    }  
    while ( fromBox.selectedIndex >= 0 ) {   
      var newOption = new Option();   
      newOption.text = fromBox.options[fromBox.selectedIndex].text;   
      newOption.value = fromBox.options[fromBox.selectedIndex].value;   
      toBox.options[toBox.length] = newOption;  
      fromBox.remove(fromBox.selectedIndex);   
    }   
  }  
  return false;
}

// Submit時のリストボックスの項目自動選択
jQuery(function(){
  $('[id*=bestgame]').submit(function(){
    
    var selectedIDs = new Array();
    $('#bestgame_player_ids option:not(:selected)').each(function() {  
      selectedIDs.push(this.value)
    });
    $('#bestgame_player_ids').val(selectedIDs)
  });
});
moveItemsの関数では、選択されたリストボックスの値と表示テキストを移動先に追加、移動元から削除している。データがない場合とデータ未選択の場合のエラーチェックも行なっている。

$('[id*=bestgame]').submitの関数では、フォームのsubmit時に、右側のリストボックスの値を自動的に全選択するようにしている。

右側のリストボックスにデータを移しただけでは、フォームに値が送信されない。リストボックスは、あくまで選択された値だけがフォームに送信されるからである。

要素名を部分一致にしているのは、railsの場合、newのフォームとeditのフォームで、名前が異なるため。

modeには、下記のメソッドを追加する。
  # Lineupクラスに存在しないPlayerのIDを配列でget/setできる属性を追加
  def non_lineup_ids
    @non_lineup_ids || players.collect{|p| p.id} - bestlineups.collect{|p| p.id}
  end
 
  def non_lineup_ids=(id_array)
    @non_lineup_ids = id_array.collect{|id| id.to_i};
  end
Lineupに存在しないプレイヤーを、playersとlineupsの配列の引き算で算出している。ただし、Controllerからは使用していない。

最後に、Controllerの修正は下記の通りとなる。(new~update部分のみ)
  # GET /bestgames/new
  # GET /bestgames/new.json
  def new
    @bestgame = Bestgame.new
    @non_lineup_select_data = Player.all.collect{|p| [p.name, p.id]}
    @lineup_select_data = ""

    respond_to do |format|
      format.html # new.html.erb
      format.json { render json: @bestgame }
    end
  end

  # GET /bestgames/1/edit
  def edit
    @bestgame = Bestgame.find(params[:id])
    @non_lineup_select_data = Player.all.collect{|p| [p.name, p.id]} - Bestlineup.find_all_by_bestgame_id(params[:id]).collect{|l| [l.player.name, l.player.id]}
    @lineup_select_data = Bestlineup.find_all_by_bestgame_id(params[:id]).collect{|l| [l.player.name, l.player.id]}
  end
新規追加の場合は、登録済のデータが存在しないので、lineup_select_dataの配列を空で初期化している。
修正の場合は、未登録のデータを、playersとlineupsの配列を引き算することで導き出している。modelクラスの関数を使わずに、ここで計算しているのは、modelクラスの関数はidのみ扱っていて、名前を保持していないため。

これで、2つのリストボックスでデータを移動できるインタフェースが完成した。


今回は下記のホームページが参考になった。

リストボックス間でアイテムを移動
http://jsajax.com/Articles/listbox/339

2012年11月8日木曜日

RubyでExcelを操作するサンプル

FXのシステムトレードのブログを運用している。

不労所得を確保セヨ!!シストレ24戦記
http://sysken.blogspot.jp/

FX会社から提供される取引報告のExcelファイルから、HTMLの表組みを手作業で行なっていたのだが、件数が多いと時間がかかる。

そこで、下記の記事を参考にして、RubyでExcelファイルから値を抜き出してHTMLに変換するプログラムを作成してみた。

VBA より便利で手軽 Excel 操作スクリプト言語「Ruby」へのお誘い (前編)
http://jp.rubyist.net/magazine/?0027-ExcellentRuby

Excelで値を取り出してしまえば、後は通常のRubyのプログラムと同様に値を扱えば良い。

数字をカンマ区切りにするのは、下記の記事を参考にさせていただいた。

Re: 金額カンマ編集について
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/37580

以下、作成したソースコード。
# encoding: CP932
require 'win32ole'

# ストラテジー別の最終取り引き回数(手動で変更)
RIKIWIKITAVYFX_USDJPY_LAST = 173
PMINVESTCAPITAL_EURGBP_LAST = 95
DBSWING_EURUSD_LAST = 28
T_Follow_EURUSD_LAST = 10

# 表組み用HTML
TABLE_HEADER = '<table border="1" bordercolor="#888" cellspacing="0" style="border-collapse: collapse; border-color: rgb(136, 136, 136); border-width: 1px;"><tbody>' + "\n"
TABLE_FOOTER = '</tbody></table>' + "\n"
FIRST_TAG = '<tr><td style="width: 150px;">'
SECOND_TAG = '</td><td style="text-align: right; width: 100px;"><b><span style="color: blue;">'
THIRD_TAG = '</span></b></td><td style="text-align: right; width: 100px;"><b><span style="color: blue;">'
FOURTH_TAG = '</span></b></td></tr>' + "\n"

# ExcelファイルのOpen
app = WIN32OLE.new('Excel.Application')
book = app.Workbooks.Open(app.GetOpenFilename)

# Excelファイルからのデータ抽出
rowIndex = 0
rikiwikitavyfxUSDJPYrowArray = Array.new
pminvestcapitalEURGBProwArray = Array.new
dbswingEURUSDrowArray = Array.new
tfollowEURUSDrowArray = Array.new

for row in book.ActiveSheet.UsedRange.Rows do
  if rowIndex > 0
    colIndex = 0
    colText = ""
    colArray = Array.new
    colArray << ''
    for cell in row.Columns do
      if colIndex == 2 || colIndex == 4 || colIndex.between?(10,12)
        colArray << cell.Value
      end
      colIndex += 1
    end
    if colArray[1] == "RikiTikiTavi FX" && colArray[2] == "USDJPY"
      rikiwikitavyfxUSDJPYrowArray << colArray 
    elsif colArray[1] == "Pminvestcapital" && colArray[2] == "EURGBP"
      pminvestcapitalEURGBProwArray << colArray 
    elsif colArray[1] == "DBSwing" && colArray[2] == "EURUSD"
      dbswingEURUSDrowArray << colArray 
    elsif colArray[1] == "T Follow" && colArray[2] == "EURUSD"
      tfollowEURUSDrowArray << colArray 
    end
  end
  rowIndex += 1 
end

# HTML表組み用関数
def createTable(rowArray, serial, labelSerial)
  
  sumPips = 0
  sumMoney = 0

  table = "<b>" + labelSerial.to_s + ". "
  table += rowArray[0][1] + ":"
  table += rowArray[0][2].slice(0, 3) + "/" + rowArray[0][2].slice(3, 3)
  table += "</b><br />\n"
  table += "<br />\n"
  table += "<br />\n"
  table += "<br />\n"
  table += TABLE_HEADER
  rowArray.reverse!
  for row in rowArray do
    table += FIRST_TAG
    table += serial.to_s + "回目の取り引き"
    if row[4] < 0
      table += SECOND_TAG.sub("blue", "red")
    else
      table += SECOND_TAG
    end
    table += row[4].to_s.reverse.gsub( /(\d{3})(?=\d)/, '\1,' ).reverse + "pips"
    if row[5] < 0
      table += THIRD_TAG.sub("blue", "red")
    else
      table += THIRD_TAG
    end
    table += row[5].round.to_s.reverse.gsub( /(\d{3})(?=\d)/, '\1,' ).reverse + "円"
    table += FOURTH_TAG
    serial += 1
    sumPips += row[4] * 10
    sumMoney += row[5]
  end
  table += FIRST_TAG
  table += "合 計"
  if sumPips < 0
    table += SECOND_TAG.sub("blue", "red")
  else
    table += SECOND_TAG
  end
  table += (sumPips / 10).to_s.reverse.gsub( /(\d{3})(?=\d)/, '\1,' ).reverse + "pips"
  if sumMoney < 0
    table += THIRD_TAG.sub("blue", "red")
  else
    table += THIRD_TAG
  end
  table += sumMoney.round.to_s.reverse.gsub( /(\d{3})(?=\d)/, '\1,' ).reverse + "円"
  table += FOURTH_TAG
  table += TABLE_FOOTER
  table += "<br />\n"
  table += "<br />\n"
  table += "<br />\n"
  return table
end

# ストラテジー別HTML作成
labelSerial = 1
outputFile = ''

if dbswingEURUSDrowArray.size > 0
  outputFile += createTable(dbswingEURUSDrowArray, DBSWING_EURUSD_LAST, labelSerial) 
  labelSerial += 1
end

if tfollowEURUSDrowArray.size > 0
  outputFile += createTable(tfollowEURUSDrowArray, T_Follow_EURUSD_LAST, labelSerial) 
  labelSerial += 1
end

if pminvestcapitalEURGBProwArray.size > 0
  outputFile += createTable(pminvestcapitalEURGBProwArray, PMINVESTCAPITAL_EURGBP_LAST, labelSerial) 
  labelSerial += 1
end

if rikiwikitavyfxUSDJPYrowArray.size > 0
  outputFile += createTable(rikiwikitavyfxUSDJPYrowArray, RIKIWIKITAVYFX_USDJPY_LAST, labelSerial) 
  labelSerial += 1
end

# HTMLファイルの書き出し
outputHTMLFile = "<html><body>\n" 
outputHTMLFile += outputFile 
outputHTMLFile += "</body></html>\n"

open("シストレ24成績.html", "w") {|f| f.write outputFile}

book.close(false)
app.quit

Railsでリストボックスを使って親子テーブルを同時に更新するサンプル(複数選択対応版)

前回作成したサンプルでは、リストボックスなのに複数選択に対応していない。HTML的に複数選択に対応しても、DBにうまくinsertされない。

しかも、手動でGameクラスにLineupクラスを複数追加すると、下図のようにリストボックスが複数表示されてしまって見栄えが悪い。


下記のサイトで同じ課題に取り組んでいる人がいたので、参考にさせていただいた。


Complex Forms for Many-to-Many Relationships
http://beacon.wharton.upenn.edu/learning/2012/02/complex-forms-for-many-to-many-relationships/

まずは改良版のGameクラスとLineupクラスのscaffold。(実行結果は省略。)
rails g scaffold bettergame date:date
rails g scaffold betterlineup bettergame:references player:references
rake db:migrate
次にbetterゲームクラスのmodelを下記のように修正する。
# encoding: utf-8

class Bettergame < ActiveRecord::Base
  has_many :players, :through => :betterlineups
  has_many :betterlineups
  accepts_nested_attributes_for :betterlineups

  def player_names
    players.collect{|p| p.name}.join(', ')
  end

  # PlayerクラスのIDを配列でget/setできる属性を追加
  def player_ids
    @player_ids || players.collect{|p| p.id}
  end
 
  def player_ids=(id_array)
    @player_ids = id_array.collect{|id| id.to_i};
  end

  after_save :assign_players

  private

  # PlayerクラスのIDの配列から、Betterlineupクラスを追加/編集/削除する。
  def assign_players
    if @player_ids
      new_ids = @player_ids
      old_ids = players.collect{|p| p.id}
      ids_to_delete = old_ids - (old_ids & new_ids)
      ids_to_add = new_ids - (old_ids & new_ids)
      bettergame_id = id
 
      ids_to_delete.each do |player_id|
        Betterlineup.destroy_all(:bettergame_id => bettergame_id, :player_id => player_id)
      end
 
      ids_to_add.each do |player_id|
        Betterlineup.create(:bettergame_id => bettergame_id, :player_id => player_id)
      end
    end
  end
end
リストボックスから複数送信されるPlayerのIDを配列としてまとめて扱うためのメソッドと、送信されたPlayerのIDとLineupクラスで既に保持しているPlayerのIDを比較して、追加/更新/削除を行うメソッドの2つが追加されている。

次に、viewの_form.html.erbを下記のように修正する。
<%= form_for(@bettergame) do |f| %>
  <% if @bettergame.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@bettergame.errors.count, "error") %> prohibited this bettergame from being saved:</h2>

      <ul>
      <% @bettergame.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :date %><br />
    <%= f.date_select :date, :use_month_numbers => true %>
  </div>
  <div class="field">
    <%= f.label :lineups %><br />
    <%= f.select(:player_ids, @player_select_data, {}, {:multiple => true, :size => Player.all.size}) %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
セレクトボックスのヘルパーには、先ほど modelクラスに追加した「:player_ids」を指定している。 その次の「@player_select_data」は、リストボックスに表示する値の配列となる。後で Controllerに追加する。 複数選択できるように、「:multiple => true」として、リストボックスの表示サイズは、Player全員とするために、「:size => Player.all.size」としている。

最後に、Controllerの修正は下記の通りとなる。(new~update部分のみ)
  # GET /bettergames/new
  # GET /bettergames/new.json
  def new
    @bettergame = Bettergame.new
    @player_select_data = Player.all.collect{|p| [p.name, p.id]}

    respond_to do |format|
      format.html # new.html.erb
      format.json { render json: @bettergame }
    end
  end

  # GET /bettergames/1/edit
  def edit
    @bettergame = Bettergame.find(params[:id])
    @player_select_data = Player.all.collect{|p| [p.name, p.id]}
  end

  # POST /bettergames
  # POST /bettergames.json
  def create
    @bettergame = Bettergame.new(params[:bettergame])

    respond_to do |format|
      if @bettergame.save
        format.html { redirect_to @bettergame, notice: 'Bettergame was successfully created.' }
        format.json { render json: @bettergame, status: :created, location: @bettergame }
      else
        format.html { render action: "new" }
        format.json { render json: @bettergame.errors, status: :unprocessable_entity }
      end
    end
  end

  # PUT /bettergames/1
  # PUT /bettergames/1.json
  def update
    @bettergame = Bettergame.find(params[:id])

    respond_to do |format|
      if @bettergame.update_attributes(params[:bettergame])
        format.html { redirect_to @bettergame, notice: 'Bettergame was successfully updated.' }
        format.json { head :no_content }
      else
        format.html { render action: "edit" }
        format.json { render json: @bettergame.errors, status: :unprocessable_entity }
      end
    end
  end
修正箇所は、newとeditのメソッド内で、「@player_select_data」の設定(Playerクラスの全データ取得)を行なっているのみ。

これで、複数選択の追加/編集/削除がリストボックス上から行えるようになった。


2012年11月7日水曜日

Railsでリストボックスを使って親子テーブルを同時に更新するサンプル


野球やサッカーなどで、手持ちの選手から試合ごとのメンバーを決める場合のクラス図は上図のようになる。Lineupという中間クラスを通して、GameクラスとPlayerクラスが多対多の関係になっている。

こういう構造の場合に、Gameクラスにデータを追加→Playerクラスにデータを追加→Lineupクラスにデータを追加というように、順々に画面を操作していくのは面倒である。

下図のように、GameクラスとLineupクラスを一画面で同時に追加することはできないだろうか?


まずはscaffold。(実行結果は省略。)
rails g scaffold game date:date
rails g scaffold player name:string
rails g scaffold lineup game:references player:references
rake db:migrate
次に、modelにクラス間のアソシエーションを設定する。
class Game < ActiveRecord::Base
  has_many :players, :through => :lineups
  has_many :lineups
  accepts_nested_attributes_for :lineups
end

class Lineup < ActiveRecord::Base
  belongs_to :game
  belongs_to :player
end
「has_many 子クラス :through 中間クラス」と書くことで、中間クラスを通じて子クラスの属性にアクセスすることができるようなる。

つまり、下記のように中間クラスLineupを意識せずに、子クラスPlayerを扱えるようになる。
irb(main):001:0> Game.find(1).players[0].name
=> "陽岱鋼"
「accepts_nested_attributes_for 中間クラス」と書くことで、子クラスの変更を親クラスで保存できるようになる。
irb(main):002:0> Game.find(1).players << Player.find(2)
irb(main):003:0> Game.find(1).save
irb(main):004:0> Game.find(1).players.last.name
=> "今浪"
view側の変更は下記のようになる。
<%= form_for(@game) do |f| %>
  <% if @game.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@game.errors.count, "error") %> prohibited this game from being saved:</h2>

      <ul>
      <% @game.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :date %><br />
    <%= f.date_select :date, :use_month_numbers => true  %>
  </div>
  <div class="field">
    <%= f.label :lineups %><br />
    <%= f.fields_for :lineups do |lf| %>
      <%= lf.hidden_field :id %>
      <%= lf.select :player_id, Player.all.map {|player| [player.name, player.id]}, {:selected=>lf.object.player_id}, :size => Player.all.size %>
    <% end %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
「fields_for」の内側に子クラスのform要素を記述する。
これで1つのview内に複数のmodelを扱えるようになる。

「:selected=>lf.object.player_id」は、こう記述しておくと、表示しているPlayerクラスの一覧から、Lineupクラスに登録されているものが選択された状態になる。

次に、controller側の変更は下記のようになる。
  # GET /games/new
  # GET /games/new.json
  def new
    @game = Game.new
    @game.lineups.build

    respond_to do |format|
      format.html # new.html.erb
      format.json { render json: @game }
    end
  end
「@game.lineups.build」の行を追加したのみ。
ここでbuildをしておかないと、view側のfields_forで指定している:lineupsが空になってしまうために、下図のようにリストボックスが表示されなくなってしまう。


model、view、controllerの修正後は、下図のようにGameクラスの画面から、子クラスPlayerの追加、修正ができるようになる。


しかしながら、現状では、せっかくリストボックスを使っているのに複数選択に対応していない。
実質的にコンボボックスと同じ機能になってしまっている。

複数選択の対応は次回の課題としたい。

以下は、今回参考にさせていただいたブログ。

has_manyな関連先をまとめてINSERTする
http://aerial.st/archive/2011/06/11/insert-has-many-relations/

has_manyな関連で、1アクションで複数のモデルを同時に保存するには?
http://d.hatena.ne.jp/zariganitosh/20080104/1199437301

[Rails]accepts_nested_attributes_forでネストした別モデルのフォーム[Ruby]
http://shasou.blogspot.jp/2011/05/railsacceptsnestedattributesforruby.html

Nested AttributesとNested Model Formsを使って親子オブジェクトを一括で登録/変更するには
http://www.everyleaf.com/tech/ror_tips/nested-attributesnested-model-forms/



2012年11月2日金曜日

Railsで正規化されていないIDから関連テーブルの情報を取得するサンプル


上図の例のように、正規化せずに一つのテーブル内に複数の外部キーを持った場合に、外部のテーブルの情報を参照(Food.name)するにはどうすれば良いだろうか?

なにはともあれscaffold。(実行結果は省略。)
rails g scaffold food name:string
rails g scaffold person name:string first_favfood_id:integer second_favfood_id:integer third_favfood_id:integer
rake db:migrate
FoodとPersonに適当にデータを入力しておく。



通常は、下記のように参照先のテーブルをblongs_toで指定することになるのだが、
class Person < ActiveRecord::Base
  belongs_to :food
end
こうすると外部キーのIDが、food_idに限定されてしまう。

下記のようにbelongs_toのオプションを指定すると、food_idの外部キーを個別に指定できるようだ。
class Person < ActiveRecord::Base
  belongs_to :first_favfood, :class_name => 'Food', :foreign_key => 'first_favfood_id'
  belongs_to :second_favfood, :class_name => 'Food', :foreign_key => 'second_favfood_id'
  belongs_to :third_favfood, :class_name => 'Food', :foreign_key => 'third_favfood_id'
end
その後、viewのindex.html.erbを下記のように変更すると、Food.nameが参照できるようになった。
<h1>Listing people</h1>

<table>
  <tr>
    <th>Name</th>
    <th>First favfood</th>
    <th>Second favfood</th>
    <th>Third favfood</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @people.each do |person| %>
  <tr>
    <td><%= person.name %></td>
    <td><%= person.first_favfood.name %></td>
    <td><%= person.second_favfood.name %></td>
    <td><%= person.third_favfood.name %></td>
    <td><%= link_to 'Show', person %></td>
    <td><%= link_to 'Edit', edit_person_path(person) %></td>
    <td><%= link_to 'Destroy', person, confirm: 'Are you sure?', method: :delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New Person', new_person_path %>

下記のブログが参考になった。

【Ruby on Rails】1つのテーブルと2フィールドでリレーションを持たせる方法(作成者と最終更新者の情報を持たせる)
http://kagayoshito.com/posts/show/102