Setting primary key when building model doesn't set associated primary key
ryanmk54 opened this issue · comments
Description
We have two tables where the primary key is generated using the same sequence. I need to set the primary key value when building/creating a model from FactoryBot, because we have an id hard-coded in the code we are testing.
If I set the primary key when calling create(:factory_name, primary_key: 5), the association is still created, but it isn't tied to the parent object, because the child object was created with the default values, not the parent's primary key.
Reproduction Steps
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "factory_bot", "~> 6.0"
gem "activerecord"
gem "sqlite3"
end
require "active_record"
require "factory_bot"
require "minitest/autorun"
require "logger"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :vendors, primary_key: :vendor_no
create_table :vendor_items, primary_key: :vendor_no
end
class Vendor < ActiveRecord::Base
self.primary_key = :vendor_no
belongs_to :vendor_item, foreign_key: :vendor_no, primary_key: :vendor_no
end
class VendorItem < ActiveRecord::Base
self.primary_key = :vendor_no
end
FactoryBot.define do
factory :vendor do
vendor_item
# Possible work-around
# after(:build) do |vendor|
# vendor.vendor_item = build(:vendor_item, vendor_no: vendor.vendor_no)
# end
end
factory :vendor_item do
end
end
class FactoryBotTest < Minitest::Test
def test_factory_bot_stuff
vendor = FactoryBot.create(:vendor, vendor_no: 5)
assert_instance_of(VendorItem, vendor.vendor_item)
end
end
# Run the tests with `ruby <filename>`
Expected behavior
I thought that the association would automatically be created with the correct foreign key
Actual behavior
As shown by the sql statements in the debug log, it was created using default values.
I thought setting a sequence, but a sequence generally won't be necessary for this factory,
and I wouldn't want the sequence to collide when I need a factory to create a model with
a specific id.
It is possible to work-around this by adding an after(:build) callback, in which case, the
association doesn't need to be defined inside the factory.
System configuration
factory_bot version: 6.2.1
rails version: 6.0.4.8
ruby version: 3.1.1p18
With the factory you presented, this call:
vendor = FactoryBot.create(:vendor, vendor_no: 5)
ultimately runs something like:
vendor = Vendor.new
vendor.vendor_no = 5
vendor_item = VendorItem.new
vendor.vendor_item = vendor_item
vendor.save!
The assignment of a new VendorItem
with no primary key will wipe out the foreign key on the vendor
.
Solving with an after(:build)
callback seems reasonable. You might also be able to use the association
method inside the block:
factory :vendor do
vendor_item { association :vendor_item, vendor_no: vendor_no }
end
Which would make it behave more like:
vendor = Vendor.new
vendor.vendor_no = 5
vendor_item = VendorItem.new
vendor_item.vendor_no = vendor.vendor_no
vendor.vendor_item = vendor_item
vendor.save!
Thank you for your response! The FactoryBot.create explanation and the association method example were very helpful. I looked at the docs for belongs_to to understand why the assignment of a new VendorItem with no primary key would wipe out the foreign key on the vendor.
belongs_to
association=(associate)
Assigns the associate object, extracts the primary key, and sets it as the foreign key...
Post#author=(author) (similar to post.author_id = author.id)
has_one
association=(associate)
Assigns the associate object, extracts the primary key, sets it as the foreign key, and saves the associate object...
Account#beneficiary=(beneficiary) (similar to beneficiary.account_id = account.id; beneficiary.save)
I realized that changing the vendor_item association from belongs_to to has_one would fix the test. I changed my Vendor class from
class Vendor < ActiveRecord::Base
self.primary_key = :vendor_no
belongs_to :vendor_item, foreign_key: :vendor_no, primary_key: :vendor_no
end
to
class Vendor < ActiveRecord::Base
self.primary_key = :vendor_no
has_one :vendor_item, foreign_key: :vendor_no, primary_key: :vendor_no, required: true
end