📘

ActiveRecordを単独で使うとbelongs_toが参照先の存在を検証しない

2025/01/19に公開

tl;dr

ActiveRecordを単体で(Railsなしで)使うときは、belongs_toはデフォルトでoptional: true相当の挙動をします。

Rails同様にoptional: falseをデフォルトにするにはActiveRecord::Base.belongs_to_required_by_default = trueの設定が必要です。

Railsにおけるbelongs_to

RailsでActiveRecordを使う際、belongs_toは参照先のレコードが存在することを検証します。[Railsガイド]

例えば次のようなテーブルが存在するとします。

ActiveRecord::Schema.define do
  create_table :shops do |t|
  end

  create_table :items do |t|
    t.references :shop, null: false, foreign_key: true
  end
end

class Shop < ActiveRecord::Base
end

class Item < ActiveRecord::Base
  belongs_to :shop
end

このとき、Item#shopnilであるならばItemはinvalidになります。

p Item.new.valid? #=> false

この検証をやめるには、belongs_tooptional: trueというオプションを設定します。

ActiveRecord単体におけるbelongs_to

ところが、ActiveRecordをRailsなしで使うと、この検証は行われません。

require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "activerecord"
  gem "sqlite3"
end

require "active_record"
require "sqlite3"

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")

ActiveRecord::Schema.define do
  create_table :shops do |t|
  end

  create_table :items do |t|
    t.references :shop, null: false, foreign_key: true
  end
end

class Shop < ActiveRecord::Base
end

class Item < ActiveRecord::Base
  belongs_to :shop
end

p Item.new.valid? #=> true

上のコードのように、Item#shopnilであってもItemはvalidになります。

原因と対策

これは、belongs_tooptional: falseがデフォルトとなっているのは、ActiveRecordのデフォルトではなくRailsが設定している動作のためです。

https://github.com/rails/rails/blob/v8.0.1/railties/lib/rails/application/configuration.rb#L123

従って、ActiveRecordを単体で使う際にoptional: falseをデフォルトとするには、ActiveRecord::Base.belongs_to_required_by_default = trueを書いておく必要があります。

require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "activerecord"
  gem "sqlite3"
end

require "active_record"
require "sqlite3"

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")

ActiveRecord::Schema.define do
  create_table :shops do |t|
  end

  create_table :items do |t|
    t.references :shop, null: false, foreign_key: true
  end
end

###################
# ↓これを追加
###################
ActiveRecord::Base.belongs_to_required_by_default = true

class Shop < ActiveRecord::Base
end

class Item < ActiveRecord::Base
  belongs_to :shop
end

p Item.new.valid? #=> false

Discussion