DjangoのFactoryでFakerを使う時に、LazyAttributeを使わなくていい書き方

この記事は、株式会社リーディングマークのテックブログからの転載になります。株式会社リーディングマークの技術に興味のある方は、ぜひ他のエンジニアの記事も見てみてください。

[転載元の記事はこちら](https://techblog.leadingmark.jp/20240520)
[株式会社リーディングマークのテックブログ](https://techblog.leadingmark.jp/)

# 前書き
こんにちは。株式会社リーディングマークエンジニアの矢代と申します。

RailsのFactoryBotっていいですよね！私は1年半ほどしかRailsの実務経験はありませんが、DjangoのFactory使ってみたり、Djangoの開発をしていると、ああ、Railsの引いてくれていたレールに脳死で乗ってしまっていた部分もあったんだなあと日々、開発者として未熟な自分を痛感する日々であります。

Leadingmarkでの勤務が1年ほどに達してDjangoでもしっかりとテストを書くようになってきまして、今回Fakerの使い方で面白い点があったので記事を書きました。

# 動作環境
- MacBook Pro 13-inch, M1, 2020
- Python 3.12
- Django 4.2
- factory-boy 3.3.0
- Faker 25.2.0

# 今回例で使う3つのモデルとその関係
```mermaid
erDiagram
clients ||--o{ orders : "1人の顧客は複数の注文を持つ"
products ||--o{ orders : "1つの商品は複数の注文を持つ"

  clients {
    bigint id PK
    string code 　"識別コード"
    string name "名前"
    string address "住所"
  }

  products {
    bigint id PK
    string code　"商品コード"
    string name "名前"
    int price "値段"
  }

  orders {
    bigint id PK
    string code　"注文コード"
    bigint client_id "顧客ID"
    bigint product_id "商品ID"
    int quantity "数量"
  }
```

# まずは書いてみた
DjangoにもRailsみたいなFactoryあるやん！となって最初に書いたFactory
```python
from factory.django import DjangoModelFactory
from faker import Faker
fake = Faker('ja-JP')

class ClientFactory(DjangoModelFactory):
    class Meta:
        model = 'myapp.Client'
    
    code = str(fake.random_int(min=0, max=9999)).zfill(4)
    name = fake.romanized_name()
    address = fake.address()
```
## この方法のFakerの落とし穴
実際に呼び出してみるとわかる。特に着目していただきたいのが、ユニークにしたい部分があるのですが、2回作成しようとするとエラーになります
```bash
root@29ac6390e9d2:/code# python project/manage.py shell
Python 3.12.3 (main, Apr 10 2024, 11:26:46) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from myapp.factory import ClientFactory
>>> client_1 = ClientFactory()
>>> client_2 = ClientFactory()
Traceback (most recent call last):

~~~

django.db.utils.IntegrityError: (1062, "Duplicate entry '7878' for key 'myapp_client.PRIMARY'")
```
## なぜ主キーの重複が起きるのか
これはPython特有？なのかわかりませんが、クラス直下にカラム名を記述して値を代入すると、みた感じ作成はできる（client_1 = ClientFactory()は通っているため）のですが、読み込み時に値が決定する方式らしく、読み込み時に「ABC」になったら「ABC」のまま2回目も作成しようとするのである。

一意にしたい顧客コードなのにFactoryの時点でこれはダメである

## 解決策
技術ブログなどを漁ってみたところ、LazyAttribute使えよBro!と書いてあったと思うので使ってみる。
```python
class ClientFactory(DjangoModelFactory):
    class Meta:
        model = 'myapp.Client'
    
    code = factory.LazyAttribute(lambda o: f'{fake.random_int(min=0, max=9999):04}')
    name = factory.LazyAttribute(lambda o: f'{fake.name()}')
    address = factory.LazyAttribute(lambda o: f'{fake.address()}')
```
ちゃんと作成できた
```bash
root@29ac6390e9d2:/code# python project/manage.py shell
Python 3.12.3 (main, Apr 10 2024, 11:26:46) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from myapp.factory import ClientFactory
>>> client_1 = ClientFactory()
>>> client_2 = ClientFactory()
>>> client_1.number
'6994'
>>> client_2.number
'1672'
```

:::note info
後学のために公式ドキュメントを探し当てたので置いておこうと思う
https://factoryboy.readthedocs.io/en/stable/reference.html#lazyattribute
:::

そういえば、0埋めのzfillってこういう書き方（:04）もできたなあと思い出した

## この方法の所感
- 正直、何個も関数に囲まれるのはちょっと…
- 超細かいですがLazyAttribute始まる地点が違くてちょっと読みづらい…？
- え、lambda使わないといけないんすか…

# 代替案として、Pythonのdict(辞書)にあるsetdefaultが便利でした
ここでRailsとは違った自由度の高いソリューションが見つかった(ソリューションって言ってみたかっただけ)
表題にあるとおりの関数でFactoryを作ってみると、なんと、Fakerの呼び出しにLazyAttributeがなくても、都度呼び出して値をセットしてくれるのである

```python
from factory.django import DjangoModelFactory
import factory
from faker import Faker
fake = Faker('ja-JP')

class ClientFactory(DjangoModelFactory):
    class Meta:
        model = 'myapp.Client'
    
    @classmethod
    def _create(cls, model_class, *args, **kwargs):
      kwargs.setdefault('code', f'{fake.random_int(min=0, max=9999):04}')
      kwargs.setdefault('name', f'{fake.name()}')
      kwargs.setdefault('address', f'{fake.address()}')
      
      return super()._create(model_class, *args, **kwargs)
```

実行結果（ほぼ同じにはなりますが）
```bash
root@29ac6390e9d2:/code# python project/manage.py shell
Python 3.12.3 (main, Apr 10 2024, 11:26:46) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from myapp.factory import ClientFactory
>>> client_1 = ClientFactory.create()
>>> client_2 = ClientFactory.create()
>>> client_1.number
'9091'
>>> client_2.number
'3501'
```

setdefaultは、辞書の中にそのキーがなければ、第２引数のものを使うというメソッドで、都度呼び出しをしてくれる。
この方法なら、クラス直下宣言していたカラムたちを持ってきても特に違和感がない。というかぶっちゃけこの書き方は割と好きなだけかもしれません。
これでこのFactoryクラスは、RailsのFactoryBotに大きく近づいたのである

# 最後に
まだまだDjangoは修行中。書くのはノイズかな〜と思ったりした事項もあったので、その点は勉強会などでLTにしようと思います。
しばらくはDjangoのテスト関連の記事を書くと思うので、よろしければ、いいねやシェアで応援していただけると嬉しいです。
最後まで読んでいただきありがとうございました。