How to handle polymorphic associations using exclusive arc
Situation
Currently, modeling polymorphic associations is pretty simple, as demonstrated in GETTING_STARTED.md. However, I have a polymorphic association using the Exclusive Belongs To/Exclusive Arc method (also seen here using Rails), which makes the above solution not work anymore because I have to make two optional associations instead of a single polymorphic one (I've left out the validation logic for clarity):
class Team < ActiveRecord::Base
has_many :posts
end
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :team, optional: true
belongs_to :user, optional: true
end
This leads me to have to awkwardly define the associations in an after(:build) block, relying on a transient and traits that only set the transient to determine which of the two associated models to build/create:
FactoryBot.define do
factory :team
factory :user
factory :post do
transient do
# default to :team
team_or_user { :team }
end
trait :team_owned do
team_or_user { :team }
end
trait :user_owned do
team_or_user { :user }
end
after(:build) do |post, evaluator|
if evaluator.team_or_user == :team
post.team = evaluator.team || evaluator.association(:team, strategy: :build)
elsif evaluator.team_or_user == :user
post.user = evaluator.user || evaluator.association(:user, strategy: :build)
end
end
end
end
As seen above, I call strategy: :build, but that's not ideal because I don't know the chosen strategy (which could lead to issues or missing data in other strategies). I also have to rely on the consumers of this factory to know to pass in team or user without being able to see it in the main factory body (defined like a regular association). I feel like there are other edge cases I'm not considering yet, but I haven't come across them yet. Finally, this is a lot of work to get a "simple" association to work properly.
Question/Discussion
Is this the best solution? Any ideas for better ways to do this that aren't so cumbersome? It makes me wish for an "anti-trait", a way to say "in this invocation, ignore the specified trait" so I could just put the associations in their traits and then put the anti-traits in the opposite traits, making it "obvious":
factory :post do
team_owned
trait :team_owned do
anti_trait :user_owned
team
end
trait :user_owned do
anti_trait :team_owned
user
end
end
Hi Noah, you bring up a great point; however, build strategies traits and transient need to accommodate many combinations, so perhaps some more questions can uncover this a bit more. In the example, the main goal seems to have a default trait that satisfies the database constraints. Could it be possible to achieve the same results utilizing an inline-block definition for the association? I think that adding a default transient value overridden by the specific traits you described in the example could produce an acceptable result/experience? Maybe worth considering that being explicit about this particular type of association (polymorphic associations always get me 😅) could help future readers notice the unique behavior. Not something directly related but something that popped up while thinking about how to use it.
Thanks for the idea! I tried that out and it didn't work as hoped, but I did find a way that works for my use-case: defining a "base" factory and then making a nested factory the default.
FactoryBot.define do
factory :team
factory :user
factory :post_base, class: Post do
factory :post, aliases: [:team_post] do
team
end
factory :user_post do
user
end
end
end
I have the urge to say it's not quite ideal as the class isn't a UserPost but a Post with a defined User, but after wrestling with it, I think that's just nitpicking and demanding consistency where none is necessary or helpful.
Would this be something worth mentioning in the guide? I can open a PR if so. Otherwise, we can close this as solved.