Upgrade to Pro — share decks privately, control downloads, hide ads and more …

create_tableをしただけなのに〜囚われのuuid編〜

 create_tableをしただけなのに〜囚われのuuid編〜

Roppongi.rb #25( https://roppongirb.connpass.com/event/338053/
Omotesando.rb #104( https://omotesandorb.connpass.com/event/338101/
の共同回をSmartHRで開催した際の会場提供企業LTです。

DaisukeShinoku

December 13, 2024
Tweet

More Decks by DaisukeShinoku

Other Decks in Programming

Transcript

  1. 作業記録アプリケーション 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
  2. 作業記録アプリケーション 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
  3. 作業記録アプリケーション WorkType Work 1:N WorkLog 1 : 1 class WorkLog

    < ApplicationRecord belongs_to :work end class Work < ApplicationRecord has_one :work_log end
  4. 作業記録アプリケーション 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
  5. 開発する新機能 WorkType Work 1:N WorkLog 0..1 : 0..1 WorkLogType 0..1:

    N 作業マスタの名称を「海岸 工事」→「港湾工事」に変 更したい! work_type.update(name: "港湾工事") 作業ログマスタ側は updateではなく name: “海岸工事”とname: “港湾工事” をそれぞれ別のレコードとして持ちたい
  6. マイグレーションファイル作成&実行 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
  7. モデル作成 & リレーション class WorkLog < ApplicationRecord belongs_to :work_log_type, optional:

    true end class WorkLogType < ApplicationRecord has_many :work_logs end
  8. 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
  9. データの確認 irb(main):001> WorkLogType.all WorkLogType Load (0.9ms) SELECT... => [#<WorkLogType:0x00000001230dabf0 id:

    1, name: "港湾工事", created_at: ..., updated_at: ...> ] WorkLogTypeのレコードが作成されている!ヨシッ!
  10. 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
  11. モデル作成 & リレーション class WorkLog < ApplicationRecord belongs_to :work_log_type, optional:

    true end class WorkLogType < ApplicationRecord has_many :work_logs end
  12. マイグレーションファイル作成&実行 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
  13. マイグレーションファイル作成&実行 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
  14. 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
  15. 外部キー制約忘れ 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
  16. 仕組みで解決 全テーブルの主キーが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
  17. まとめ(自戒) • 新機能作成時のcreate_tableのマイグレーションは「期待とワク ワク」を持ちながらも「慎重さ」を忘れない • 主キーbigint & 外部キーはuuid & 関係性がoptional:

    true ◦ このパターンに嵌まって不具合に気付けなかった • 人間はミスする生き物なのでなるべく仕組みで解決する