Slide 1

Slide 1 text

create_tableをしただけなのに 〜囚われのuuid編〜 2024.12.12 Thu. Roppongi.rb#25 & Omotesando.rb#104 @SmartHR東京オフィス shinoku(新奥 大介) SmartHR プロダクトエンジニア

Slide 2

Slide 2 text

Rails開発をしている皆さん 、 create_table はお好きですか?

Slide 3

Slide 3 text

作業記録アプリケーション WorkType Work 1:N WorkLog 1 : 1

Slide 4

Slide 4 text

作業記録アプリケーション WorkType Work 1:N WorkLog 1 or 0 :1 or 0 class Work < ApplicationRecord belongs_to :work_type end class WorkType < ApplicationRecord has_many :works, dependent: :destroy end

Slide 5

Slide 5 text

作業記録アプリケーション WorkType Work 1:N WorkLog 1 or 0 :1 or 0 def create @work = Work.new(work_params) @work.save! end def work_params params.require(:work).permit(:name, :work_type_id) end

Slide 6

Slide 6 text

作業記録アプリケーション WorkType Work 1:N WorkLog 1 : 1 class WorkLog < ApplicationRecord belongs_to :work end class Work < ApplicationRecord has_one :work_log end

Slide 7

Slide 7 text

作業記録アプリケーション WorkType Work 1:N WorkLog 1 : 1 class Work < ApplicationRecord def complete! ApplicationRecord.transaction do WorkLog.create!(completed_at: Time.zone.now, work: self) update!(completed: true) end end end

Slide 8

Slide 8 text

開発する新機能 WorkType Work 1:N WorkLog 1 : 1 WorkLogType 0..1 : N

Slide 9

Slide 9 text

開発する新機能 WorkType Work 1:N WorkLog 0..1 : 0..1 WorkLogType 0..1: N 作業マスタの名称を「海岸 工事」→「港湾工事」に変 更したい! work_type.update(name: "港湾工事") 作業ログマスタ側は updateではなく name: “海岸工事”とname: “港湾工事” をそれぞれ別のレコードとして持ちたい

Slide 10

Slide 10 text

それでは開発していきます

Slide 11

Slide 11 text

マイグレーションファイル作成&実行 class CreateWorkLogTypes < ActiveRecord::Migration[7.1] def change create_table :work_log_types do |t| t.string :name, null: false t.timestamps end add_reference :work_logs, :work_log_type, type: :uuid, null: true end end

Slide 12

Slide 12 text

モデル作成 & リレーション class WorkLog < ApplicationRecord belongs_to :work_log_type, optional: true end class WorkLogType < ApplicationRecord has_many :work_logs end

Slide 13

Slide 13 text

Work#complete!(修正前) class Work < ApplicationRecord def complete! ApplicationRecord.transaction do WorkLog.create!(completed_at: Time.zone.now, work: self) update!(completed: true) end end end

Slide 14

Slide 14 text

Work#complete!(修正後) class Work < ApplicationRecord def complete!(work_type) ApplicationRecord.transaction do work_log_type = WorkLogType.find_or_create_by(name: work_type.name) work_log = WorkLog.build(completed_at: Time.zone.now, work_log_type:, work: self) work_log.save! update!(completed: true) end end end

Slide 15

Slide 15 text

API実行 curl -X PATCH http://localhost:3000/works/56781d31-442b-48fe-a 9fe-a80e63e486d5

Slide 16

Slide 16 text

データの確認 irb(main):001> WorkLogType.all WorkLogType Load (0.9ms) SELECT... => [# ] WorkLogTypeのレコードが作成されている!ヨシッ!

Slide 17

Slide 17 text

データの確認 紐付くWorkLogも・・・あれ・・・取れない・・・? irb(main):002> WorkLogType.first.work_logs => []

Slide 18

Slide 18 text

データの確認 よくみたらWorkLogのwork_log_type_idがnilになっています。 これは何かがマズそうです・・・ irb(main):003> WorkLog.order(created_at: :desc).last => #

Slide 19

Slide 19 text

Work#complete!(修正後) class Work < ApplicationRecord def complete!(work_type) ApplicationRecord.transaction do work_log_type = WorkLogType.find_or_create_by(name: work_type.name) work_log = WorkLog.build(completed_at: Time.zone.now, work_log_type:, work: self) work_log.save! update!(completed: true) end end end

Slide 20

Slide 20 text

モデル作成 & リレーション class WorkLog < ApplicationRecord belongs_to :work_log_type, optional: true end class WorkLogType < ApplicationRecord has_many :work_logs end

Slide 21

Slide 21 text

マイグレーションファイル作成&実行 class CreateWorkLogTypes < ActiveRecord::Migration[7.1] def change create_table :work_log_types do |t| t.string :name, null: false t.timestamps end add_reference :work_logs, :work_log_type, type: :uuid, null: true end end

Slide 22

Slide 22 text

マイグレーションファイル作成&実行 class CreateWorkLogTypes < ActiveRecord::Migration[7.1] def change create_table :work_log_types do |t| t.string :name, null: false t.timestamps end add_reference :work_logs, :work_log_type, type: :uuid, null: true end end

Slide 23

Slide 23 text

Schemaを確認する # 主キーにuuidをできているテーブル create_table "work_types", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "name", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end # idがデフォルトのまま・・・ create_table "work_log_types", force: :cascade do |t| t.string "name", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end # にも関わらず関連先の外部キーはuuidにしてしまっている create_table "work_logs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.uuid "work_log_type_id" end

Slide 24

Slide 24 text

外部キー制約忘れ PG::DatatypeMismatch: ERROR: foreign key constraint "fk_rails_d1282d5b36" cannot be implemented DETAIL: Key columns "work_log_type_id" and "id" are of incompatible types: uuid and bigint. マイグレーション時に気付けそうなものだが・・・ 外部キー制約も付け忘れていて検知できなかった・・・ def change add_reference :work_logs, :work_log_type, type: :uuid, null: true, foreign_key: true end

Slide 25

Slide 25 text

後日談 ・migrationのみで1つのPRとしてmainブランチにマージされた後 だったため、新規migrationファイルを作成するしかなかった ・2024XXXX_create_work_log_types.rb ・2024XXXX_drop_work_log_types.rb ・2024XXXX_recreate_work_log_types.rb ・溢れ出る「マイグレーションをミスりました」感 ・チームメンバーが「仕組みで解決しよう!」と言ってくれた

Slide 26

Slide 26 text

後日談 ・migrationのみで1つのPRとしてmainブランチにマージされた後 だったため、新規migrationファイルを作成するしかなかった ・2024XXXX_create_work_log_types.rb ・2024XXXX_drop_work_log_types.rb ・2024XXXX_recreate_work_log_types.rb ・溢れ出る「マイグレーションをミスりました」感 ・チームメンバーが「仕組みで解決しよう!」と言ってくれた 最高!!

Slide 27

Slide 27 text

仕組みで解決 マイグレーションファイル生成時にuuid型で作成するよう設定 config.generators do |g| g.orm :active_record, primary_key_type: :uuid end config/application.rb

Slide 28

Slide 28 text

仕組みで解決 全テーブルの主キーがuuidであることを担保するspecを用意 →CI実行時に気付けるようになりました! describe "primary key" do it "すべてのtableのprimary keyがuuidになっており、defaultが設定されていること" do aggregate_failures do all_tables.each do |tbl| p_key_name = AR::Base.connection.primary_key(tbl) p_key_column = AR::Base.connection.columns(tbl).find { |c| c.name == p_key_name } expect(p_key_column.sql_type).to eq("uuid"), "Table: #{tbl}" expect(p_key_column.default_function).to eq("gen_random_uuid()"), "Table: #{tbl}" end end end end

Slide 29

Slide 29 text

まとめ(自戒) ● 新機能作成時のcreate_tableのマイグレーションは「期待とワク ワク」を持ちながらも「慎重さ」を忘れない ● 主キーbigint & 外部キーはuuid & 関係性がoptional: true ○ このパターンに嵌まって不具合に気付けなかった ● 人間はミスする生き物なのでなるべく仕組みで解決する

Slide 30

Slide 30 text

https://hello-world.smarthr.co.jp