factory_bot_rails 6.3.0 から PK に対する sequence はエラーになる #rails

概要

factory_bot_rails 6.3.0 から Active Record の PK カラムに対して sequence を指定するとエラーになるようになったようだ。

指定した場合、 FactoryBot::AttributeDefinitionError が発生する。

github.com

FactoryBot.define do  
  factory :hoge do  
    # エラーになる
    sequence(:id) { |n| n }  
  endend

上記のような場合、↓みたいなエラーメッセージが出る。

FactoryBot::AttributeDefinitionError:
    Attribute generates "id" primary key for Hoge"

    Do not define "id". Instead, rely on the database to generate it.

対応

指定するのをやめればいいのだが、設定で無効化することもできる。

config/application.rb とか config/environments あたりに設定を追加すれば OK。

config.factory_bot.reject_primary_key_attributes = false

この辺は README の Active Record Configuration あたりに書いてある。

今のところ一括で無効化することしか出来ないが、個別に設定するような要望は上がっているようだ。

Rubocop に FactoryBot/IdSequence というのもあるので活用してもいいかもしれない。

感想

要望 Issue を見ていると困るユースケースはありそうだなと思いつつ、 Rails (Active Record) 使うなら取り敢えずサロゲートキーを自動採番で降っておくテーブル設計に統一しとくのがいいのかなと思う。 (それはそれとして、ナチュラルキーに制約は貼るけど)

転職し(てい)ました

表題の通りなんですが、3月いっぱいでコネヒトを退職して、4月からエス・エム・エスという会社で働いています。

試用期間が終わったくらいにブログでも書こうかなと思ってたら、半年以上過ぎてるわ年末が目前だわで、時の早さを自分の怠惰の言い訳にしたくなります。

なぜコネヒトを退職したか

自分のやりたいことと価値観を大切にして、中長期1で働くということを考えた場合に違う環境を選ぶことにしました。

正直複合的な理由がいろいろあるので(残るという選択肢もあった)、興味ある人は直接会ったときにでも聞いてください。

なぜエス・エム・エスに入社したか

きっかけ

エス・エム・エスには2016年から3年半ほど在籍していたことがあり、要は出戻りになります。

ただ転職活動自体は普通にしていて、他社の選考も受けていました。

某媒体で転職活動中なのを @sunaot に見つかり(退職後も多少交流はあった)、飲みにいったのがきっかけです。

以前在籍していたこともあり、 変化や現在地点をリアルに想像できたアドバンテージがあるので、他の人の参考にはならない転職エントリです。。

迷った点

自分は職場を選ぶ際「"何" を "なぜ" 作るのか(何をやろうとしているのか)」というのをとても大事にしています。

そういう意味で、(前回入社してる時点で)その点はそれほど問題ではありませんでした。

ただ今回は、技術スタックを以前より重視していました(言語でいうと Go か Java をやりたかった2

サービスにもよりますが、エス・エム・エスは Rubist が多いし、オファーの配属先も Rails を主に使うので正直そこは希望とは異なりました3

ただ最後まで迷いながら自分の考えを突き詰めた結果、エス・エム・エスを選ぶことにしました。

決めた理由

最終的に決め手になったのはオファー面談か何かで言われた「ボトルネックを解消しにいく(ためにやっている)」という言葉でした。

元々ミッションや理念には共感して前回の入社をしているのだけれど、実行の強さみたいなのを感じたというか。

解きたい問題を構造化して、どの変数を重視してどう解釈して(だから)こうやって解こうとしている、をすごく丁寧にやっているなぁと。

それは前回の在職中にも感じていたことだし、外に出てから改めて実感したことでもあります。

そうすると(仮に間違っていたとしても)仮説や施策の精度だったり、再現性が高くなっていくと思っています。

実際に在職していた頃より、ビジョンに対する解像度が上がっているのを話の中で実感して、その先を想像した時にワクワクするものがありました。

技術スタックを重視しようと思っていたけれど、結局最後の決め手がその辺りになったのは自分でも発見だったかもしれません。

その他

もちろん他にも要素はありました。

  • やっぱ一緒に働いたことがあり、かつ価値観まで理解してくれているであろう人に改めて声かけられるのはうれしい(採用はやっぱリファラル強いよね、と自分事ながら痛感した)
  • 選考過程であった人との話が面白かった(今のマネージャーとか)
  • 課題とか不安になる要素を具体的に想像して検討できた(これは出戻りによるチート)
  • etc

今何をやっているか

たくさん事業がある会社なので、その中のまだ小さい数名程度の開発チームで Ruby On Rails 書いてます。

RubyRails を自分が今から触るのがキャリア的にどうなのかなという気持ちは多少あります。

ただ単純に今までやったことがない新しいことをやるという意味では(想像以上に)楽しみながらはやっているし、それでいいかなとも思っています。

マネージャーが Rails に強いので、ただ動くものを作るだけじゃなく、よりよいものを作るために気軽に相談できたりするのにも助けられています。

開発プロセスやチーム運営も気になったりはするけれど、サービスもチームも今の規模やフェーズだと、重視するものが違うよなぁと感じたりはしながら、その辺は少しずつ貢献していければいいなと思っています。


  1. もちろん何が起きるかわからないので、短期になったらその時はその時かなっと思ってる
  2. 技術スタックを理由に選考を辞退した会社もあるので、結果的に言行不一致で申し訳ないという気持ち。。
  3. 実際に、最後まで迷ったオファーをくれた会社は Java がメインだった

Rails の collection_check_boxes をカスタマイズする #rails

概要

検索フォームのような、チェックボックスによる複数条件を指定する UI を作ろうとしたときに、 collection_check_boxes を使うとよさそう。

しかし、collection_check_boxes のオプションだけだと input の属性くらいしかいじれないため、 label の class までカスタマイズするのは難しい。

collection_check_boxes にブロックを渡すとブロックの引数で CheckBoxBuilder を受け取れるので、それを使うことで柔軟に変更できる。

やりたいこと

たとえばブートストラップのようなチェックボックスの場合、下記のような HTML にしたい。

<div class="form-check form-check-inline">
  <input class="form-check-input" type="checkbox" id="inlineCheckbox1" value="option1">
  <label class="form-check-label" for="inlineCheckbox1">1</label>
</div>
<div class="form-check form-check-inline">
  <input class="form-check-input" type="checkbox" id="inlineCheckbox2" value="option2">
  <label class="form-check-label" for="inlineCheckbox2">2</label>
</div>
<div class="form-check form-check-inline">
  <input class="form-check-input" type="checkbox" id="inlineCheckbox3" value="option3" disabled>
  <label class="form-check-label" for="inlineCheckbox3">3 (disabled)</label>
</div>

Checks and radios(チェックとラジオ) · Bootstrap v5.0

個々のチェックボックスに、以下のようなカスタマイズが必要になる。

  • input と label それぞれに個別のスタイルを指定したい
  • div で input と label を囲いたい

collection_check_boxes で実現する

ざっくりやることは次の通り。

  • Controller 側で選択肢となるコレクションを作成し、インスタンス変数に格納しておく
  • collection_check_boxes でコレクションを回す
  • ブロック内でカスタマイズする

例えば「著者による投稿検索」をする場合、 authors コレクションを作成し、author_ids を配列パラメータで渡す。

view の実装は↓のようになる。

 <%= form.collection_check_boxes(:author_ids, @authors, :id, :name, { include_hidden: false }) do |builder|
    tag.div class: 'form-check-inline' do
      checked = params[:author_ids]&.include?(builder.value.to_s)
      
      builder.check_box(checked: checked, class: 'form-check-input') + builder.label(class: 'form-check-label')
    end
  end %>

ブロックの引数で CheckBoxBuilder を受け取れるので、柔軟に変更ができる。

上記の例のように、パラメータなんかと value を突き合わせることで選択状態やスタイルを切り替えることもできる。

(include_hidden は必要に応じて)

rails/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb at v7.0.7.2 · rails/rails · GitHub

参考

Rails でトランザクション完了後に更新内容を取得する #rails

概要

トランザクションとは分離したいが、保存した内容によって事後処理を行いたい場合がある。

changed?hoge_changed? あたりは save 後には false になってしまうため、更新内容を取得できない。

ApplicationRecord.transaction do
  if foo.name_changed?
    # ここでは変更を検知できるけど
  end

  foo.save!
  bar.save!
end

if foo.name_changed?
  # ここでは変更を検知できない
end

対応

hoge_previously_changed? を使うと、変更内容を取得できる。

ApplicationRecord.transaction do
  foo.save!
  bar.save!
end

if foo.name_previously_changed??
  # 変更を検知できる!
end

変更前後の値を比較したり、変更内容を判定したり、まとめて変更を取得できたりもする。

foo.previous_changes         # => {"name" => [nil, "Bill"]}
foo.name_previously_changed? # => true
foo.name_previously_changed?(from: nil, to: "Bill") # => true
foo.name_previous_change     # => [nil, "Bill"]
foo.name_previously_was      # => nil

また ID の変更を判定することで、「新規作成か更新か」を区別するという応用もできる。

# true なら新規作成
foo.id_previously_changed?

詳細はドキュメントを参照。

api.rubyonrails.org

その他

今回のケースでは関係なかったが、 persisted? というのもあった。

どこかで役に立ちそう。

railsdoc.com

参考

Rails + Puma は cluster mode だと RubyMine でデバッグできない #Rails #RubyMine

Rails + Docker な開発環境で RubyMine のリモートデバッグを行う場合、下記を参考にすればできるようになる。

www.jetbrains.com

ところが、 Puma を利用していた際にデバッグモードでエラーが発生し起動できなくなってしまった。

Exception: Connection refused - connect(2) for "172.19.0.1" port 26168
Fatal exception in DebugThread loop: Attempt to unlock a mutex which is not locked

原因

どうやら RubyMine が使用している ruby-debug-ide が、 cluster mode だとうまくできないようである。

github.com

対応

single mode で起動するようにすれば、デバッグできるようになる。

puma の worker を 0 にすれば single mode になる。

環境変数 WEB_CONCURRENCY0 に設定すれば、 worker を 0 にできる。

environment:
  - WEB_CONCURRENCY=${WEB_CONCURRENCY:-0}

docker-compose 経由なら↑な感じ。

=> Booting Puma
=> Rails 7.0.5 application starting in development 
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...

起動ログに single mode と表示されれば OK。

設定ファイルの修正は不要

config/puma.rb あたりでも設定できるが、デフォルトの設定が環境変数を優先するようになっているので、環境変数だけ設定すれば大丈夫。

(設定ファイルの方はデフォルトだとコメントアウトされているが問題ない)

github.com

参考

「詳解Go言語Webアプリケーション開発」を読んだ #golang

仕事では中々 Go を書く機会がないので、手を動かしながら学べる本がないかなと手に取った。

初心者向けの入門書かな、くらいの気持ちだったのだけれど、いい意味で期待を裏切られた。

かなり実践的な内容に踏み込んでいるし、解説も丁寧で参考になることがたくさんあった。

今後自分で Go の Web 開発をするときはこの本をベースにして考える気がする。

気になった点

ハンズオンを写経しながら進めていたが、書籍だけだとどのファイルに書くのかちょっと分かりづらいことがある。

また、ところどころ間違っていると思われるところもある。

ただ GitHub リポジトリでコードと正誤表が公開されているので、詰まったりよく分からないときは参照することで問題なく解決できた。

あとテストもあるのが非常に助かるのだけれど、網羅的に用意されている訳ではないので、省略しているだけなのか、意図的に書いてないのかは気になった。

おまけ

とてもいい本だった。

(実務含め) Go で書く機会を積極的に増やしたいなという気持ちになった。

副業とかないかな。

GitHub Actions で service のコンテナにリポジトリのファイルをマウントしようとするとエラーになる #GitHub #Actions

概要

GitHub Actions では、ジョブのサービスとして MySQL や Redis が使える。

volumes ( jobs.<job_id>.services.<service_id>.volumes ) を指定することで、サービス間やステップ間でデータを共有できる。

ただソースコードは通常 actions/checkout を利用すると思うが、リポジトリで管理しているようなファイルパスをそのままマウントしようとエラーになる。

rm: cannot remove '/home/runner/work/your_repo/path/to/mysql/conf.d': Permission denied

たとえば下記のような Workflow である。

jobs:
  job-name:
    runs-on: ubuntu-latest
    services:  
      mysql:  
        image: mysql
        ports:  
          - 3306:3306  
        volumes:  
          - ${{github.workspace}}/path/to/mysql/conf.d:/etc/mysql/conf.d

steps:  
  - uses: actions/checkout@v3  

原因

Workflow のジョブはサービスが立ち上がった後に動くため、 actions/checkout はマウント後に動く。

そうするとチェックアウトするディレクトリにマウントするディレクトリが存在する(空ではない)とされて、 actions/checkout は一度対象ディレクトリの中を空にしようとするようだ。

その際、サービスを動かすユーザーと actions/checkout を動かすユーザー(権限)異なるため、エラーになってしまう。

解決策

リポジトリをチェックアウトするディレクトリと、マウントする場所を分ければ解決する。

  • jobs.<job_id>.defaults.run の working-directory を設定する
  • チェックアウト場所を working-directory にする
  • 対象ファイルを cp などで移動する
jobs:
  job-name:
    runs-on: ubuntu-latest
  defaults:
    run:  
      working-directory: my_target
    services:  
      mysql:  
        image: mysql
        ports:  
          - 3306:3306
        volumes:  
          - ${{github.workspace}}/path/to/mysql/conf.d:/etc/mysql/conf.d

steps:  
  - uses: actions/checkout@v3
    with:  
      path: my_target
  - run: |  
      cp -r path/to/mysql/conf.d ${{github.workspace}}/path/to/mysql/conf.d

補足

working-directory の default を指定しているのは、 step の追加・変更時にパスをできるだけ意識せずに済むようにするため。

別に workflow 単位でも指定できるし、 step の中で cd してもよい。

例では MySQL の設定ファイルをマウントしているが、反映するためには大抵再起動が必要になるので、実際には data ファイルとかになると思う(テストデータを step 間で使い回すとか?)。

その他参考

docs.github.com