Search code examples
ruby-on-railsrails-activerecord

Rails Active Record, reciprocal dependency hell


I have two models with a 1 to 1 reciprocal dependency.

User (1..1) <-> (1..1) Address

I have set up it like this:

class User
  has_one :address
  validates :address, presence: true

  # automatic create address object for new Users.
  before_validation :create_address, on: :create

  def create_address
    if !address.present?
      build_address
    end
  end
end

class Address
  belongs_to :user
  validates :user, presence: true
end

I also have these factories (FactoryBot)

factory :user do
end


factory :address do
  association :user
end

But this generates infinite problems.

When I try to create an Address using FactoryBot.create(:address) it creates 2 Addresses for the same User.

The only solution I found is either to remove the automatic creation of Address in the User class, or remove the automatic creation of the User in the Address factory.

In any case, the factories will be incomplete and will fail if I don't expecitly add the missing dependency.

I can also remove the validations but this sounds even uglier to me.

I have tried many combinations. But it is impossible to save one of the instances without the other being already saved, and none of them can be saved without the other :/

What would be your approach to this?


Solution

  • You can use inline association definition and use instance method that refers to the model being created:

    factory :user
    
    factory :address do
      user { association :user, address: instance }
    end
    
    >> FactoryBot.create(:address)
      TRANSACTION (0.1ms)  begin transaction
      User Create (0.5ms)  INSERT INTO "users" ...
      Address Create (0.1ms)  INSERT INTO "addresses" ...
      TRANSACTION (0.1ms)  commit transaction
    => #<Address:0x00007fcb5a00ce1 ...>
    

    https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#interconnected-associations


    class Address
      belongs_to :user
      # ^ belongs to is already required. extra validation is redundant.
      validates :user, presence: true
    end
    
    >> Address.validators
    =>
    # validation from belongs to  vvv
    [#<ActiveRecord::Validations::PresenceValidator:0x00007fb9d6853878 @attributes=[:user], @options={:message=>:required}>,
     #<ActiveRecord::Validations::PresenceValidator:0x00007fb9d687b238 @attributes=[:user], @options={}>]
    # your presence validation    ^^^