月曜日までに考えておきます

ITネタとゲームネタ中心に興味のあること色々書きます。

Shibuya.rbに参加して発表してきました&ActiveRecordのidの件を補足調査

昨日、Shibuya.rbがあったので参加して発表してきました。

内容は、ActiveRecordで"id"という主キーでないカラムを持っているテーブルにつなぐとおかしな挙動になったという経験をまとめたものです。

スライド公開後、 @mirakui さんからprimary_key=を設定してみては?というリプを頂いたので検証してみました。

# スライドで作ってたクラス
class HyperLegacyTbl < ActiveRecord::Base
  establish_connection(:legacy)
  self.table_name = "hyper_legacy_tbl"
end

# primary_keyを"legacy_id"にしたクラス
class HyperLegacyTbl2 < ActiveRecord::Base
  establish_connection(:legacy)
  self.table_name = "hyper_legacy_tbl"
  self.primary_key = :legacy_id
end

# primary_keyを"id"にしたクラス
class HyperLegacyTbl3 < ActiveRecord::Base
  establish_connection(:legacy)
  self.table_name = "hyper_legacy_tbl"
  self.primary_key = :id
end

1. legacy_idをprimary_keyに設定した場合

[21] pry(main)> h2 = HyperLegacyTbl2.new
=> #<HyperLegacyTbl2 legacy_id: 0, id: nil>
[22] pry(main)> h2.id = 1000
=> 1000
[23] pry(main)> h2
=> #<HyperLegacyTbl2 legacy_id: 1000, id: nil>
[24] pry(main)> h2.save
   (0.2ms)  BEGIN
  SQL (0.2ms)  INSERT INTO `hyper_legacy_tbl` (`legacy_id`) VALUES (1000)
   (0.1ms)  COMMIT
=> true

こうなるのでスライドの内容と同じ挙動になりそう。

2. idをprimary_keyに設定した場合

DB上はプライマリキーじゃないけどRails上でprimary_keyとしてみる。

[36] pry(main)> h3 = HyperLegacyTbl3.new
=> #<HyperLegacyTbl3 legacy_id: 0, id: nil>
[37] pry(main)> h3.id = 2000
=> 2000
[38] pry(main)> h3
=> #<HyperLegacyTbl3 legacy_id: 0, id: 2000>
[39] pry(main)> h3.legacy_id = 1
=> 1
[40] pry(main)> h3
=> #<HyperLegacyTbl3 legacy_id: 1, id: 2000>
[41] pry(main)> h3.save
   (0.2ms)  BEGIN
  SQL (0.3ms)  INSERT INTO `hyper_legacy_tbl` (`id`, `legacy_id`) VALUES (2000, 1)
   (0.1ms)  COMMIT
=> true
[42] pry(main)> h3.id
=> 2000

とりあえず#id#id=はうまく動いてます。DB上の"id"はプライマリキーではないので重複がありえます。このときにRails上で正しく振る舞えるのかCRUD操作してみます。

C

[49] pry(main)> h3_next = HyperLegacyTbl3.new
=> #<HyperLegacyTbl3 legacy_id: 0, id: nil>
[50] pry(main)> h3_next.legacy_id = 2
=> 2
[51] pry(main)> h3_next.id = 2000
=> 2000
[52] pry(main)> h3_next.save
   (0.2ms)  BEGIN
  SQL (0.2ms)  INSERT INTO `hyper_legacy_tbl` (`id`, `legacy_id`) VALUES (2000, 2)
   (0.1ms)  COMMIT
=> true

うまく登録できますね。duplicate entryになったりはしない。

この時点でテーブルはこんな感じ

+-----------+------+
| legacy_id | id   |
+-----------+------+
|         1 | 2000 |
|         2 | 2000 |
+-----------+------+
2 rows in set (0.00 sec)

R

ActiveRecordのデータ取得系メソッドを幾つか試してみます。

  • find
[54] pry(main)> HyperLegacyTbl3.find(2000)
  HyperLegacyTbl3 Load (0.4ms)  SELECT `hyper_legacy_tbl`.* FROM `hyper_legacy_tbl` WHERE `hyper_legacy_tbl`.`id` = 2000 LIMIT 1
=> #<HyperLegacyTbl3 legacy_id: 1, id: 2000>

Railsでprimary_keyに設定した"id"で検索されます。id=2000で検索することになりますが、2件あっても1件しか返さないので結果は不定になるでしょう。find_byと同じですね。

  • find_by_id

同上

[55] pry(main)> HyperLegacyTbl3.find_by_id(2000)
  HyperLegacyTbl3 Load (0.3ms)  SELECT `hyper_legacy_tbl`.* FROM `hyper_legacy_tbl` WHERE `hyper_legacy_tbl`.`id` = 2000 LIMIT 1
=> #<HyperLegacyTbl3 legacy_id: 1, id: 2000>
  • find_by_legacy_id

当然うまく動きます。

[60] pry(main)> HyperLegacyTbl3.find_by_legacy_id(2)
  HyperLegacyTbl3 Load (0.3ms)  SELECT `hyper_legacy_tbl`.* FROM `hyper_legacy_tbl` WHERE `hyper_legacy_tbl`.`legacy_id` = 2 LIMIT 1
=> #<HyperLegacyTbl3 legacy_id: 2, id: 2000>
  • where

これも問題なし。

[61] pry(main)> HyperLegacyTbl3.where(legacy_id: 2)
  HyperLegacyTbl3 Load (0.3ms)  SELECT `hyper_legacy_tbl`.* FROM `hyper_legacy_tbl` WHERE `hyper_legacy_tbl`.`legacy_id` = 2
=> [#<HyperLegacyTbl3 legacy_id: 2, id: 2000>]
[62] pry(main)> HyperLegacyTbl3.where(id: 2000)
  HyperLegacyTbl3 Load (0.3ms)  SELECT `hyper_legacy_tbl`.* FROM `hyper_legacy_tbl` WHERE `hyper_legacy_tbl`.`id` = 2000
=> [#<HyperLegacyTbl3 legacy_id: 1, id: 2000>,
 #<HyperLegacyTbl3 legacy_id: 2, id: 2000>]

U

更新処理。

[63] pry(main)> h3_1 = HyperLegacyTbl3.where(legacy_id: 1).first
  HyperLegacyTbl3 Load (0.3ms)  SELECT `hyper_legacy_tbl`.* FROM `hyper_legacy_tbl` WHERE `hyper_legacy_tbl`.`legacy_id` = 1 ORDER BY `hyper_legacy_tbl`.`id` ASC LIMIT 1
=> #<HyperLegacyTbl3 legacy_id: 1, id: 2000>
[64] pry(main)> h3_1.update_attributes(id: 3000)
   (0.2ms)  BEGIN
  SQL (0.4ms)  UPDATE `hyper_legacy_tbl` SET `id` = 3000 WHERE `hyper_legacy_tbl`.`id` = 2000
   (0.1ms)  COMMIT
=> true

げえっ

mysql> select * from hyper_legacy_tbl;
+-----------+------+
| legacy_id | id   |
+-----------+------+
|         1 | 3000 |
|         2 | 3000 |
+-----------+------+

レコードの一番目(legacy_id=1)のインスタンスがupdate_attributesするとidをキーに更新しに行くのでlegacy_id=2の方も更新されるので注意! update_attributes, saveともに同じ結果でした。

D

更新が上だったのでなんとなく想像つきますが

[86] pry(main)> h3 = HyperLegacyTbl3.where(legacy_id: 1).first
  HyperLegacyTbl3 Load (0.3ms)  SELECT `hyper_legacy_tbl`.* FROM `hyper_legacy_tbl` WHERE `hyper_legacy_tbl`.`legacy_id` = 1 ORDER BY `hyper_legacy_tbl`.`id` ASC LIMIT 1
=> #<HyperLegacyTbl3 legacy_id: 1, id: 3000>
[87] pry(main)> h3.destroy
   (0.2ms)  BEGIN
  SQL (0.3ms)  DELETE FROM `hyper_legacy_tbl` WHERE `hyper_legacy_tbl`.`id` = 3000
   (0.1ms)  COMMIT
=> #<HyperLegacyTbl3 legacy_id: 1, id: 3000>
mysql> select * from hyper_legacy_tbl;
Empty set (0.00 sec)

▂▅▇█▓▒░('ω')░▒▓█▇▅▂うわあああああああ

まとめ

残念ながら#primary_key=をDB上の主キーに当てても#id, #id=が指すのは"legacy_id"のままで"id"の方は向いてくれない。 primary_keyとして"id"を割り当てると#id, #id=が使えるようになるものの、update,deleteの挙動が本来のActiveRecordインスタンスの挙動と変わってくるのが厄介ですね。

追記:

この検証を行ったのは activerecord (4.0.1) です。 会社でこの問題で頭抱えた時はRails3.2.1x 系だったので3系でも当てはまると思います。

皆さんの発表

Railsにプルリク送ってみた話 by @sue445 さん

  • First step of Rails Contribute‎ #shibuyarb
  • Rails4でlockの機能をサンプルコード通りに実装したらdepricateと言われたのでサンプルコードを修正した
  • 英語で苦労したけどそんな怖い人達じゃなくて無事にマージされた。

プロセス監視ツール eye by @tyabe さん

vimでpull requestを見れるプラグインの話 by @joker1007 さん

* ドキュメントツールを作った話 by @ryooopan さん

  • GitHubからコードを取ってきて、コードを部分選択してメモを貼るWebサービスを作った話。すごい。
  • このサイトCumiki

Twilio by by @joohounsong さん

  • Twilioの中の人によるデモ

chromeのgitlab extension作った話 by @sue445 さんふたたび

  • 社内でgitlab作っているのでそれを作った話とかJSのテスティングフレームワーク jasmineの活用とか

Refinements by @joker1007 さんふたたび

  • Refinements Ruby2.0とRuby2.1での変化など
  • 2.1からmodule scopeになるらしくて、活用できる箇所が増えそう。

最近の悩みが解決

  • GitHub wikiを手軽にPDF化したいなぁと思ってて、それphantome.jsでできるのでは?という話が出て実際できそう。
  • phantomjsのrasterize.jsがサイズ指定してpdf化できるので、これのスクリプト書いて実行すれば一気にGitHub wikiをpdf化、みたいなことがやれそうです。

Screen Capture · ariya/phantomjs Wiki