Factory Boyでユニーク値をもつモデルに逆参照したら重複した話

テストを記述しているときに参照先のユニークIDが重複して生成されるというエラーが起きてハマった。 モデルは以下

class User(models.Model):
  …
  some_id = models.CharField(… unique=True)
  …

class Post(models.Model):
  some_id = models.ForeignKey(User, related_name=“posts”, to_field=“some_id”, on_delete=models.CASCADE)
  …
 

class PostFactory(DjangoModelFactory):
  class Meta:
    model = Post
  
  some_id = SubFactory(SubFactory)

class UserFactory(DjangoModelFactory):
  class Meta:
    model = User

  some_id = str(uuid.uuid4())
  …

最初は上記のようなコードで動くかと思いshellを動かしてみたら2回目のビルドに落ちることに気づいた。エラー内容としては、userモデルのsome_idがuniqueなのに重複しているというエラーだ。

原因としてはテスト(一度のプログラムの実行)では、異なるUserインスタンスを生成してくれているわけでは無く、全く同じ内容のインスタンスが生成されているらしい。

つまり、一度目は、当然新規に発行されたUserなので重複することはないが、二度目は一度目と同じデータになるので、Unique制限に引っかかっていたということだ。

このエラーはFactoryBoyあるあるらしく、公式を見て回っていたら普通に回避方法が見つかった。

class UserFactory(DjangoModelFactory):
  class Meta:
    model = User
    fields = “__all__”
    django_get_or_create = (“some_id”,)

django_get_or_createに重複させたくないカラムを指定することで、作成かgetで適切に動作を処理してくれるようになる。

この場合、一度目の実行では新たにUserを作成し、二度目は作成しない。

後日談: Userモデルをページネーション以下で利用しようとたらget_or_createのおかげでUserを必要数作成することができなかった。 その為、FactoryboyのSequenceを使って、都度別々のuuidを作るように対応したけど、使い道によってファクトリー分けてもよさそう。

class UserFactory(DjangoModelFactory):
    class Meta:
        model = User

    uid = Sequence(lambda n: f"{str(uuid.uuid4())}-{n}")