How do i fabricate a model with validate presence on has_one relation with foreign key constraint

169 views Asked by At

I seem to run into a some kind of circular relationships that the two solutions in the gem's documentation won't solve for me. See the example below. Is this meant to be done differently?

One would argue that because one object could not really be persisted without the other they ought to just be one model. I think it's better to extract all the logic regarding authentication to it's seperate model in order not to bloat the user. Most of the time credential stuff is only used when creating sessions, whereas the user is used all the time.

create_table "credentials", force: :cascade do |t|
  t.bigint "user_id", null: false
  ...
  t.index ["user_id"], name: "index_credentials_on_user_id"
end

add_foreign_key "credentials", "users"
class Credential < ApplicationRecord
  belongs_to :user, inverse_of: :credential
end

class User < ApplicationRecord
  has_one :credential, inverse_of: :user
  validates :credential, presence: true
end
Fabricator(:user_base, class_name: :user)

Fabricator(:user, from: :user_base) do
  credential
end

Fabricator(:credential) do
  user(fabricator: :user_base)
end
irb(main):001:0> Fabricate(:user)
  TRANSACTION (0.1ms)  BEGIN
  TRANSACTION (0.1ms)  ROLLBACK
Traceback (most recent call last):
        1: from (irb):1:in `<main>'
ActiveRecord::RecordInvalid (Validation failed: Credential can't be blank)
irb(main):002:0> Fabricate(:credential)
Traceback (most recent call last):
        2: from (irb):1:in `<main>'
        1: from (irb):2:in `rescue in <main>'
ActiveRecord::RecordInvalid (Validation failed: Credential can't be blank)
irb(main):003:0> Fabricate.build(:user).save
  TRANSACTION (0.2ms)  BEGIN
  User Create (0.8ms)  INSERT INTO "users" ("email", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["email", "[email protected]"], ["created_at", "2021-05-29 18:19:09.312429"], ["updated_at", "2021-05-29 18:19:09.312429"]]
  Credential Create (0.9ms)  INSERT INTO "credentials" ("user_id", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["user_id", 19], ["created_at", "2021-05-29 18:19:09.319411"], ["updated_at", "2021-05-29 18:19:09.319411"]]
  TRANSACTION (41.2ms)  COMMIT
=> true
2

There are 2 answers

1
Paul Elliott On BEST ANSWER

The way you're solving this would certainly work. The way I normally recommend people solve this is by overriding the inverse relationships. ActiveRecord will do the right thing in this case.

Fabricator(:user) do
  credential { Fabricate.build(:credential, user: nil) }
end

Fabricator(:credential) do
  user { Fabricate.build(:user, credential: nil) }
end
1
pinzonjulian On

The model that has the foreign key, in this case Credential, is the one that is required to have a user_id value to be persisted. This means that there needs to be a user (either in memory or in the database) before creating a credential. This is the reason why using build works for you.

If the user exists in memory, rails will be smart enough to create that one first before creating the credential. It seems to me that when you use build with Fabricate it’s initializing a user and a credential so when the user is saved, it saves the credential with the newly created user.

Note that the docs use this syntax for belongs_to, not has_one. It seems that you may need to refer to the callbacks section of the documentation to fix this issue.