model のインスタンスを hash化する

使う機会はあまり無い気もするが、メモしておく*1


結論から書くと、モデルインスタンス#attributes とするだけ。

model_instance = Model.new
attributes_hash = model_instance.attributes  # {"attr1" => "val1", "attr2" => "val2", ...}
p attributes_hash.class  # --> Hash


どんなとこで使ったのかというと、
同じ名前のカラムを持っている2つの異なるモデルがあるとする。
そのカラムを一方からもう一方にコピーしたいときに、どうやるのがスマートかな、という話し。

# name, attr1 を持つモデルA と name, attr1, attr2 を持つモデルB があったとする
model_a = ModelA.first
model_b = ModelB.new

# 1つ1つ入れるのは、カラム数が少なければいいけど、多いと・・・
model_b.name = model_a.name
model_b.attr1 = model_a.attr1

# ループもいまいち・・
ModelA.column_names.each do |attr_name|
  model_b[attr_name] = model_a[attr_name]
end

# attributes なら一発
model_b = ModelB.new(model_a.attributes)

まぁ、そもそもこんなコピーをしないといけないモデル設計が微妙な可能性もあり。。。

ちなみに、上では「いまいち」とかって書いちゃってるけど、
Model.column_names で全カラム名の入った配列が取れるのも最初は知りませんでしたorz...

p ModelA.column_names  # --> ["name", "attr1"]

*1:rails詳しい人的にはあたり前のことかも?

ActiveSalesforce を使ってみたメモ

http://activesfdc.rubyforge.org/ を少し使う機会があったので、そのメモを。
ちなみに、Salesforce の知識はほぼ無い状態で触ってますのであしからず・・・。
なお、rails のバージョンは 2.3.5 です。

Setup と HelloWorld みたいなもの

1. install
公式の通りに

$ gem install activerecord-activesalesforce-adapter

とすると、activerecord-activesalesforce-adapter-2.0.0 がインストールされます*1が、
これだとrails 2.3.5ではパッチ(http://timothynjones.wordpress.com/2008/12/03/patching-activesalesforce-for-rails-222/)を当てないと正しく動きません*2


git(GitHub - oldfartdeveloper/activerecord-activesalesforce-adapter: An ActiveRecord adapter for the Salesforce.com API)には、
gem install で入るものより新しい activerecord-activesalesforce-adapter-2.2.2 があり、
こちらの場合はパッチを当てる必要はありませんでした。
インストール方法は、上記ページでも確認できますが、

$ git clone git://github.com/oldfartdeveloper/activerecord-activesalesforce-adapter.git
$ cd activerecord-activesalesforce-adapter/
$ gem build activerecord-activesalesforce-adapter.gemspec
$ gem install activerecord-activesalesforce-adapter-2.2.2.gem

これでインストールできます。


2. config/database.yml

development:
  adapter:  activesalesforce
  username: <salesforce-username>
  password: <salesforce-password><salesforce-security_token>

password は、「salesforceにログインするときのパスワード」と「セキュリティトークン」を隙間無く並べた文字列です。
また、gitのページでは上記の他に「url:」の項目が説明されていますが、これは記述しなくても動きます。
記述の有無による違いはわかりませんorz


3. モデルを作ります

$ ruby script/generate model Account ―skip-migration

公式でもgitでも書いてない気がしますが、作らないと動きませんでした。
(作らなくても良い方法があるなら教えて下さいorz)


4. 適当なコントローラで Account.all とかやるとデータが取れます

リレーションの無いfind

SalesforceAPIで叩く際には、カラム名が「__c」付きになるっぽいので、そこは気をつける必要があります。

# column_a と column_b で引っかける
Account.find(:first, :conditions => ["column_a__c = ? AND column_b__c = ?", param_a, param_b])

# こっちの書き方でも動きます
Account.find_by_column_a__c_and_column_b__c(param_a, param_b)


で、ここがちょっと微妙なところですが、単に Account.all としても、最大200件しか取ってきません。
でも、limit を大きく設定してあげると200件以上でも取ってきてくれます。
(内部的には、200件ずつループを回して取ってきているようですが。)

# Account に500件のデータが入っていた場合

account = Account.all
p account.length  # --> 200

account = Account.find(:all, :limit => 1000)
p account.length  # --> 500

リレーション(belongs_to, has_many)

belongs_to, has_many は普通に使えます。

しかも、自分でモデルに書かなくても、勝手に作ってくれます。

# log/development.log

Processing AccountsController#index (for ***.***.***.*** at 2010-06-01 13:24:35) [GET]
   Created one-to-one relationship 'master_record' from Account to Account using master_record_id
   Created one-to-one relationship 'parent' from Account to Account using parent_id
   Created one-to-one relationship 'owner' from Account to User using owner_id
   Created one-to-one relationship 'created_by' from Account to User using created_by_id
   Created one-to-one relationship 'last_modified_by' from Account to User using last_modified_by_id
   Created one-to-many relationship 'account_contact_roles' from Account to AccountContactRole using account_id
   Created one-to-many relationship 'histories' from Account to AccountHistory using account_id
   ...

見方としては、

Created one-to-<has_manyならmany / belongs_toならone> relationship '<リレーション名>'
        from <リレーション元モデル名> to <リレーション先モデル名> using <foreign key>

なので、

例えば、上から3つ目の「Created one-to-one relationship 'owner' from Account to User using owner_id」は、

class Account < ActiveRecord::Base
  belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"
end

と同じで、Account.first.owner とかで使えます。


また、一番下の「Created one-to-many relationship 'histories' from Account to AccountHistory using account_id」は、

class Account < ActiveRecord::Base
  has_many :histories, :class_name => "AccountHistory", :foreign_key => "account_id", :dependent => :nullify
end

と同じで、やはり Account.first.histories で使えます。


上のような勝手に作ってくれるリレーションも使えますが、
自分でモデルに書いても正しく動きます。
(foreign_key などにはやはり API名を指定する必要があります)


ただし、SOQLでは、「モデル名__r.カラム名」とかでリレーション先のカラムを条件式に入れたりできるようですが、
これはActiceSalesforceでは使えないようです?*3


例えば、Account と MyCustomObj に 1 対 多 のリレーションが張られているとします。
Account の column1 が 指定した値の MyCustomObj を取得したい場合、
SOQL的にconditions を書くとたぶん下のようになります。
Salesforce全然わかってないので、間違ってたらごめんなさい。。。)

MyCustomObj.find(:all, :conditions => ["MyCustomObj__r.column1 = ?", column1_param])

でも、これは動きません。


ライブラリの中身を少し見ましたが、別テーブルへのエイリアスは削除しているっぽいです。

# /usr/local/lib/ruby/gems/1.8/gems/activerecord-activesalesforce-adapter-2.2.2/lib/active_record/connection_adapters/activesalesforce_adapter.rb の 308, 309行目

# strip away any table alias
column_name.sub!(/\w+\./, '')

この column_name に上でいうと「MyCustomObj__r.column1」が入ってくるので、
column_name はただの「column1」になり、MyCustomObj には column1 というカラムはないので、
そんなカラム無いよということでエラーになってしまいます。

Column not found for #{column_name}!

これが上手いこと動くととても使いやすい気がするんですがね。。。

insert

これも普通に動きます。
newして値をセットして、saveするだけです。
(値をセットする際に、各カラム名API名にしないといけない点だけ注意が必要です)

account = Account.new
account.column1__c = value1
account.column2__c = value2
...

account.save!


とりあえず、実際に使ってみたのはこれくらいですが、
「update」とか「delete」も定義されてるので、同様に使えるのではないかと思います。


*1:2010/05/31 時点

*2:2.2.2のパッチとなっていますが、2.3.5でも動きました

*3:使い方があるなら教えて欲しいですorz

html5 の File API を使って、ローカルのテキストファイルを読み込んでみる

とっても簡単です(Firefox 3.6 で動作を確認)。

html

fileのアップロードフォームにonchangeを仕込んでおきます。

あとは、読み込んだファイルの内容を表示するtextareaを用意。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>

<input type="file" onchange="read(this)" />

<hr />
<textarea id="text" cols="80" rows="20" wrap="off"></textarea>

</body>
</html>

javascript

function read (ele) {
    if (!ele.files.length) return;

    var file = ele.files[0];
    if (!/^text\//.test(file.type)) return;  // text/plain, text/html, ...

    var fr = new FileReader();
    fr.onload = function () {
        document.getElementById('text').value = fr.result;  // 読み込み結果をtextareaに
    };
    fr.readAsText(file);  // ファイルをテキストとして読み込む
}

readAsTextの2つ目の引数で文字コードを指定できます*1が、
指定しなくても上手いこと推定してくれるっぽいです。
UTF-8, SJIS, EUC は普通に読み込めました。)


file フォームに multiple をセットしておけば、
複数ファイルの中身をマージした結果とかも簡単に出せちゃいそうですね

*1:fr.readAsText(file, "UTF-8"); とかで

html5の File API を使って、アップロード無しで画像プレビュー


画像をプレビューするために、サーバへアップロードする必要がなくなります*1

html

fileのアップロードフォームにonchangeを仕込んでおきます。
あとは、プレビュー表示用の要素だけ準備。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>

<form>
  <input type="file" name="file" onchange="preview(this)" />
</form>

<hr />
<b>preview:</b><br />
<div id="preview_field"></div>

</body>
</html>

javascript

function preview(ele) {
    if (!ele.files.length) return;  // ファイル未選択
    
    var file = ele.files[0];
    if (!/^image\/(png|jpeg|gif)$/.test(file.type)) return;  // typeプロパティでMIMEタイプを参照

    var img = document.createElement('img');
    var fr = new FileReader();
    fr.onload = function() {
        img.src = fr.result;  // 読み込んだ画像データをsrcにセット
        document.getElementById('preview_field').appendChild(img);
    }
    fr.readAsDataURL(file);  // 画像読み込み

    // 画像名・MIMEタイプ・ファイルサイズ
    document.getElementById('preview_field').innerHTML =
        'file name: ' + file.name + '<br />' +
        'file type: ' + file.type + '<br />' +
        'file size: ' + file.size + '<br />';
}

スバラシイ。

*1:Firefox 3.6で動作を確認

html5で複数ファイルアップロード&保存

とても簡単。

html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>

<form action="save.cgi" method="POST" enctype="multipart/form-data">
  <input type="file" name="files" multiple />
  <input type="submit" value="submit" />
</form>

</body>
</html>

input type="file" で multiple属性を追加するだけ。

save.cgi (perl)

#!/usr/bin/perl

use strict;
use warnings;
use CGI;
use File::Copy;

my $SAVE_DIR = '/save/dir';  # 保存するディレクトリのパス

my $q = CGI->new;
my @files = $q->param('files');
for my $filename (@files) {
    copy($q->tmpFileName($filename), "$SAVE_DIR/$filename") or die $!;
}

exit;

今までは、複数ファイルが選択できるアップローダーにはFlashが必要だったから、これは非常に便利。

オブジェクトのプロパティの存在確認方法メモ

1.単純に obj.property で確認すると・・・

var obj = {};
if (obj.hoge) alert('not exist');

obj.hoge = false;
if (obj.hoge) alert('exist false');

obj.hoge = null;
if (obj.hoge) alert('exist null');

obj.hoge = undefined;
if (obj.hoge) alert('exist undefined');

obj.hoge = "";
if (obj.hoge) alert('exist ""');

obj.hoge = 0;
if (obj.hoge) alert('exist 0');

obj.hoge = NaN;
if (obj.hoge) alert('exist NaN');

1番上のif以外は、hogeプロパティが存在しているので、
alert が表示されて欲しいが、1つも出ない

2.undefined かどうかで判定

基本的に、プロパティが存在しない場合には undefined が返ってくるので、

var obj = {};
alert(typeof obj.hoge);  // "undefined"

「undefined でない」イコール「プロパティが存在している」とするのであれば、

var obj = {};
if (obj.hoge !== undefined) alert('not exist');

obj.hoge = false;
if (obj.hoge !== undefined) alert('exist false');

obj.hoge = null;
if (obj.hoge !== undefined) alert('exist null');

obj.hoge = undefined;
if (obj.hoge !== undefined) alert('exist undefined');

obj.hoge = "";
if (obj.hoge !== undefined) alert('exist ""');

obj.hoge = 0;
if (obj.hoge !== undefined) alert('exist 0');

obj.hoge = NaN;
if (obj.hoge !== undefined) alert('exist NaN');

これで、false, null, "", 0, NaN を値として取るプロパティも判定可能
(typeof obj.hoge !== "undefined" でもOK)
ただ、当然ながら undefined を値として取るプロパティは判定不可能

3.undefined を値として取るプロパティも判定

var obj = {};
if ("hoge" in obj) alert('not exist');

obj.hoge = false;
if ("hoge" in obj) alert('exist false');

obj.hoge = null;
if ("hoge" in obj) alert('exist null');

obj.hoge = undefined;
if ("hoge" in obj) alert('exist undefined');

obj.hoge = "";
if ("hoge" in obj) alert('exist ""');

obj.hoge = 0;
if ("hoge" in obj) alert('exist 0');

obj.hoge = NaN;
if ("hoge" in obj) alert('exist NaN');

とすると、false, null, undefined, "", 0, NaN の存在確認が可能

4.おまけ

2の例で「!==」ではなく「!=」とすると、

var obj = {};
if (obj.hoge != undefined) alert('not exist');

obj.hoge = false;
if (obj.hoge != undefined) alert('exist false');

obj.hoge = null;
if (obj.hoge != undefined) alert('exist null');

obj.hoge = undefined;
if (obj.hoge != undefined) alert('exist undefined');

obj.hoge = "";
if (obj.hoge != undefined) alert('exist ""');

obj.hoge = 0;
if (obj.hoge != undefined) alert('exist 0');

obj.hoge = NaN;
if (obj.hoge != undefined) alert('exist NaN');

null が外れて、false, "", 0, NaN のみになる

public配下の任意の静的ファイルへのURLを作成するメモ

ローカルでは、http://localhost:3000/ で開発をしているが、
本番サーバで動かすときは、http://domain/prefix/ で動かしたい場合がある。


viewでリンクなどのURLを直書きしていると、相対パスなら大丈夫かもしれないが、
絶対パスは prefix があるせいで、本番サーバに持って行ったときに意図する動作(URL)にならない。

# viewファイル(直書きの例)
<a href="/controller/action">link</a>

<script type="text/javascript" src="/javascripts/prototype.js"></script>


上の例のように、aタグリンクであったり、
public配下であっても javascripts,stylesheets,images 下のファイルであれば、
railsが用意してくれている link_to, url_forや、
javascript_include_tag, stylesheet_link_tag, image_tag などを使うことで、
prefixの有無に関わらずrailsがよろしくURLを作ってくれる。

# viewファイル
<%= link_to "link", :controller => "controller", :action => "action" %>

<%= javascript_include_tag "prototype" %>


でも、それ以外の静的ファイルのURLに関して、どうするのがいいかちょっと困ったのでメモする。

# viewファイル
# 例えば、public/swf/example.swf とか
<embed src="/swf/examle.swf">

結論としては、当たり前のことかもしれないけど、
root_path を取得して、public配下のパスをくっつけるという方法しか思いつかなかった。
で、root_pathの取得の仕方は、 config/routes.rb で map.root を設定しているなら、
そのまま root_path で参照できる。

# viewファイル
<embed src="<%= "#{root_path}swf/examle.swf" %>">

これで、root_path には、ローカルなら '/' が、
本番サーバなら '/prefix/' が入るので、どちらの環境でも正しく動作する。


config/routes.rb で map.root を設定していない場合は、
次のような helper を作ると、同じことができる。

# helperファイル
def my_root_path
  return "#{ActionController::Base.relative_url_root}/"
end

# viewファイル
<embed src="<%= "#{my_root_path}swf/examle.swf" %>">


どうせなら、

# helperファイル
def url_for_public(path)
  return "#{ActionController::Base.relative_url_root}/#{path}"
end

# viewファイル
<embed src="<%= url_for_public "swf/examle.swf" %>">

ここまでやった方がきれいかもしれない。


(※) ちなみに、railsのversionは 2.3.5 です。
古いversionだと、ActionController::Base.relative_url_root ではなく、
request.relative_url_root で参照するようです。