maco's life

主にエンジニアリングと読書について書いていきます。

Railsで複合PKを使う際に`Undefined method 'to_sym'`がでて辛かったメモ 

Railsidcreated_atを複合PKにしたくてschema.rbに直接下記のような定義を書いていた。

# 例
create_table "tests", id: false, force: :cascade do |t|
  t.integer  "id", limit: 8
  t.datetime "created_at",     null: false
  t.datetime "updated_at",    null: false
end

execute "ALTER TABLE `tests` ADD PRIMARY KEY (`id`, `created_at`);"
execute "ALTER TABLE `tests` MODIFY `id` BIGINT AUTO_INCREMENT;"

直接いじっているのは、いろんな経緯があってのことなのでここでは説明しません。

bundle exec rake db:resetを実行して期待通りに複合PKがはられたテーブルになっていたのでよっしゃいけるぞ!とおもってモデルで

def self.update
  test = Test.find(...)
  test.touch
  test.save!`
end

以上のようなコードを書いて実行してみるとUndefined method 'to_sym'のようなエラーがでて???ってなってた。

いろいろ試行錯誤してみて全く意味がわからなかったのでいろいろ調べてみると、rails(というかActiveRecord)が備えているそのままの機能では複合PKは扱えないそうで、それが原因かーとおもって更に調べると次の記事にたどりついた。

記事の内容通りに複合PKを扱うgemのcomposite_primary_keysを使って、self.primary_keysを定義したところ無事saveすることができた。

ActiveRecordのfind_or_create_byが投げるクエリを検証した

RubyのORMのActiveRecordにfind_or_create_byというメソッドがある。このメソッドはデータがあったらselectした結果が返ってきて、ない場合はinsertをしてその結果を返してくれるという夢のような機能を実現してくれているらしい。 このデータがあったらselect、ない場合はinsertのような処理は扱うときによく考えないとロック周りでDBが詰まってつらい思いすることがある。そこでこのメソッドはどんなクエリを投げるのか検証してみた。

検証方法

  • ActiveRecord::Baseを系統したGamePlayCountモデルに対してrails cで操作を行う。
  • GamePlayCountは対象のgame_idを遊ぶとcountを++していくテーブルだと仮定する。

テーブル構造はこんな感じ。

CREATE TABLE `game_play_counts` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `game_id` int(10) unsigned NOT NULL DEFAULT '0',
  `count` int(10) unsigned NOT NULL DEFAULT '0',
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `game_id` (`game_id`),
)

今回はGamePlayCountに対して下記の操作を行う。

  • find_or_create_by単体で使う
  • find_or_create_byとlockを組み合わせて使う
  • 検証する際は一つのケースに対してtransaction内、外両方のパターンを試してみる

検証結果はrails cでActiveRecord::Base.logger = Logger.new(STDOUT)を実行してログに流れるSQLを順番に抽出したものを貼っています。早速検証スタートだドン!

find_or_create_by単体で使う

検証1

find_or_create_byをそのまま呼んでみます。

GamePlayCount.find_or_create_by(:game_id => 8)
SELECT `game_play_counts`.* FROM `game_play_counts` WHERE `game_play_counts`.`game_id` = 8 LIMIT 1
BEGIN
INSERT INTO `game_play_counts` (`game_id`, ...) VALUES ('8' ...)
COMMIT

一度トランザクションの外でSELECTして、トランザクション内でINSERTする結果となりました。こちらで明示的にトランザクションを指定したわけでないけど、ActiveRecordではINSERTしようとすると勝手にトランザクションがはられるようです。

検証2

明示的にトランザクションを指定して試します。

ActiveRecord::Base.transaction do
  GamePlayCount.find_or_create_by(:game_id => 9)
end
BEGIN
SELECT  `game_play_counts`.* FROM `game_play_counts` WHERE `game_play_counts`.`game_id` = 9 LIMIT 1
INSERT INTO `game_play_counts` (`game_id`, ...) VALUES ('9', ...)
COMMIT

こちらはトランザクション内でSELECT, INSERTを行うクエリが発行されました。この場合、同時にアクセスが来た場合に片方がduplicateになりそうですね。

lockの検証

find_or_create_byとlockの組み合わせを実行する前にlock単体でどういったクエリを投げるか確認します。

検証3

GamePlayCount.lock(:true)
SELECT `game_play_counts`.* FROM `game_play_counts` FOR UPDATE

テーブルに対してFOR UPDATEかけた...トランザクション外だから影響ないけどテーブルロック一歩手間ですね。

検証4

次にトランザクション内でlockを呼んでみます。もうこれテーブルロックでしょ

ActiveRecord::Base.transaction do
  GamePlayCount.lock(:true)
end
BEGIN
COMMIT
SELECT `game_play_counts`.* FROM `game_play_counts` FOR UPDATE

トランザクション内でFOR UPDATEかけると思ったけれど、意外なことに外で実行されました。これはこれで安全設計かもだけどちょっと違和感が...。ちなみにlockの後にfind指定で実行したらきちんとトランザクション内で実行されました。

find_or_create_byとlockを組み合わせて使う

find_or_create_byとlockを呼ぶとどうなるのか。大変気になるので早速検証します。

検証5

まずは明示的にトランザクション囲まないケースから試します。

GamePlayCount.lock(true).find_or_create_by(:game_id => 10)

データがない状態

SELECT `game_play_counts`.* FROM `game_play_counts` WHERE `game_play_counts`.`game_id` = 10 LIMIT 1 FOR UPDATE
BEGIN
INSERT INTO `game_play_counts` (`game_id`, ...) VALUES ('10', ...)
COMMIT

トランザクション外でFOR UPDATEが実行されて、後にトランザクション内でINSERTが走りました。気持ちだけFOR UPDATEつけといたよ感あるけどロックは取れません。

データが既にある状態

SELECT `game_play_counts`.* FROM `game_play_counts` WHERE `game_play_counts`.`game_id` = 10 LIMIT 1 FOR UPDATE

トランザクション外なので同様にロックは取れません。

検証6

明示的にトランザクション囲んだケースを試します。

ActiveRecord::Base.transcation do
  GamePlayCount.lock(true).find_or_create_by(:game_id => 11)
end

データがない状態

BEGIN
SELECT  `game_play_counts`.* FROM `game_play_counts` WHERE `game_play_counts`.`game_id` = 11 LIMIT 1 FOR UPDATE
INSERT INTO `game_play_counts` (`game_id`, ...) VALUES ('11', ...)
COMMIT

トランザクション内でFOR UPDATEしてINSERTもする挙動となりました。最初にFOR UPDATEの空打ちをしているので、ギャップロックが気になります。高い並列度でよばれたら詰まることがありそうですね。

データが既にある状態

BEGIN
SELECT  `game_play_counts`.* FROM `game_play_counts` WHERE `game_play_counts`.`game_id` = 11 LIMIT 1 FOR UPDATE
COMMIT

対象のデータがある場合はトランザクション内でFOR UPDATEをしていて、ロックは取れていました。

まとめ

今回は興味本位でfind_or_create_byを使うとどういったクエリになるか確認しました。find_or_create_byもduplicateのリスクがあるし、lockと組み合わせると初回アクセス時にギャップロックしてしまうしで、使い方によっては良くないことがあるなという感じです。エラー無く処理したいということならichirin2501先生のスライドにあるINSERT ... ON DUPLICATE KEY UPDATEを使うのも良さそうです。INSERT ... ON DUPLICATE KEY UPDATEは呼ばれる度にAUTO INCREMENT値がどんどん増えていってしまうので、BIGINTを使うなりしてOut of rangeになるリスクを避ける必要があるのでご注意ください。

またActiveRecordで気になるメソッドがあったら随時クエリをのぞいていけたならと思います。

何でエンジニアをするのか

今年が終わりそうなので、自分がエンジニアをやる理由を振り返ってみる

  • 新しい技術に触れる瞬間が楽しい
  • コード書いている時間が楽しい
  • 一つのアーキテクトを作り上げるのが楽しい
  • 作り上げたものがユーザに届くのが嬉しい
  • 生じる問題をエンジニアリングで解決するのが楽しい
  • 結果自分のスキルになって成長を感じるのが嬉しい
  • etc..

以上です。

多段SSHする際の.ssh/configの設定

多段SSHの設定いつも忘れるのでブログる。

まず設定の例から書くと

Host fumidai
  HostName hoge
  User macotasu
  Port 10022
Host target-server
  HostName fuga
  User macotasu
  Proxycommand ssh -CW %h:%p fumidai

こんな感じでsshの設定をかいて多段sshにすることができた。

よく理解していなくてハマったのがProxycommandの挙動である。 Proxycommandを使うと、Proxy先のhostからsshするようになる。 なのでtarget-serverへのsshの設定はfumidaiから見た設定を書く必要がある。 この基本的なところを理解していないと、複雑な環境設定がしているところへの 多段SSHで混乱してはまるなーってなった。(実際ハマった)

アニメイトラボに入社しました

2年8ヶ月勤めていた面白法人カヤックを退職して、アニメイトラボで働き始めました。

10月~11月に転職活動をしていて、いくつかの魅力的な企業からオファーを頂いていたのですが、アニメイトラボは設立間もない会社で地盤整えていくフェーズだったのが楽しそうだったり、以前から一緒に働いてみたいと思っていた方が在籍していたりなどのご縁もあって、入社することを決めました。

オフィスは千駄ヶ谷にあって、会社の近くに銭湯があるのが良い感じです。

前職でお世話になった方々と「次が決まったら教えてね」というお話もあったので、ご挨拶を兼ねてブログに書きました。

今後とも皆様よろしくお願いいたします。

DDLとかの検証にDummyデータをさくっと用意するSQL

前にダミーデータサクッとつくりたいわっておもって、便利ストアドプロシージャつくったのでブログにも書いておく。

gist.github.com

SELECTして結果をINSERTするところをコピペしていけば、ストアドプロシージャなんかつかわないでもいいんだろうけど、DRYなコードにしたいじゃん?

そういうことです。

NoPaste作った

丸一日かかってしまったけど、ちゃんと作ってみた

github.com

構成

  • Kossy
  • DBIx::Sunny
  • etc...

DBIx::Sunny初めて使ったけど、シュッと使えて便利だった

感想

今まで一人で一からアプリケーションを書くことは、あまりなかったから良い経験になった

それと共に、自分のPerlを使った実装力がないなーっておもってつぶやいた

地球で二番目っぽいので満足でした 😇