Django の deconstruct と deconstructible について

## 概要

- Djangoは 1.7 から migration 機能を内蔵するようになった。
- **makemigrations** で自動で差分を抽出るために、Modelのカスタムフィールドやカスタムバリデーターに細工をする必要がある
- Fieldでは **deconstruct** メソッドの実装が必要
- Validator では **deconstructible** デコレータの付与と、 **\_\_eq\_\_**  メソッドの実装が必要
- 簡単に対応内容をまとめる。

## 遭遇したエラー

- カスタムバリデーターを作成し **makemigrations** を実行したら、以下のようなエラーに遭遇した

```bash:
There are some values Django cannot serialize into migration files.
For more, see https://docs.djangoproject.com/en/1.9/topics/migrations/#migration-serializing
```

- これを解決しようとした時に、**deconstruct** の話に辿りついた

## カスタムバリデーター

- クラスベースのカスタムバリデーター は 1.6 以前までは下記のように書いていた

```python
# validators.py ----

from django.core.exceptions import ValidationError

class UploadSizeValidator:

    def __init__(self, size=None):
        if size:
            self.size = size

    def __call__(self, value):
        if value.file.size > self.size:
            raise ValidationError('Upload Size Error')

# models.py ----

class Question(models.Model):

    question_image = models.ImageField(
        upload_to='question_image',
        validators=[UploadSizeValidator(size=200)],
        default=None
    )
```

- だが、1.7 からは Validatorを下記のように実装しないと上記のエラーが表示される。

```python
from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible

@deconstructible # <- decoratorを付加
class UploadSizeValidator:

    def __init__(self, size=None):
        if size:
            self.size = size

    def __call__(self, value):
        if value.file.size > self.size:
            raise ValidationError('Upload Size Error')

    def __eq__(self, other): # <- __eq__ メソッドを実装
        return (
            isinstance(other, self.__class__) and (self.size == other.size)
        )
```

- これで初めて **makemigrations** が通る。
- **\_\_eq\_\_**  はなんのため必要なのかというと、migrationファイルを生成する時の差分比較に利用する。
- 具体的には以下のようなことが起きる。

```
1. UploadSizeValidator(size=100) をFieldに設定した状態で migration ファイルを作ったとする
2. sizeを 200 に変更して makemigrations を実行する
3. __eq__ メソッド で 変更前(other.size=100)と変更後(self.size=200) の size を比較
4. 一致しないので、AlterField の migration ファイルが生成される
```

- 実際に生成された migration の内容はこんな感じ

```python
operations = [
    migrations.AlterField(
        model_name='question',
        name='question_image',
        field=models.ImageField(
          default=None, upload_to='question_image',
          validators=[polls.validators.UploadSizeValidator(size=200)]
        ),
    ),
```

- こうすることで、バリデーター の中身も migrate したり, backwards で戻したりできる。
- ちなみに関数ベースのバリデーターは差分比較できるものを持たないので、今までの書き方と一緒。
- validatorsに指定した時に、AlterField が生成されるのみ

```python
def validate_even(value):
    if value % 2 != 0:
        raise ValidationError(
            _('%(value)s is not an even number'),
            params={'value': value},
        )
```

## カスタムフィールド

- カスタムフィールドを作る時は、**deconstruct** メソッドの実装が必要になる。
- **deconstruct** は 上記の **\_\_eq\_\_**  と似たような役割を持つ
- つまり Fieldに変更があったかどうかを比較するための情報を返す
- 情報は フィールド名(name), フィールドのパス(path), Field の constructorへの 引数(args)、キーワード引数(kwargs)

```python
class HandField(models.Field):

    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 104
        super(HandField, self).__init__(*args, **kwargs)

    def deconstruct(self):  # <- deconstruct
        name, path, args, kwargs = super(HandField, self).deconstruct()
        del kwargs["max_length"]
        return name, path, args, kwargs

# refs https://docs.djangoproject.com/ja/1.9/howto/custom-model-fields/#field-deconstruction
```

## まとめ

- カスタムバリデータと、カスタムフィールドを作る特は細工が必要
- カスタムストレージクラスを作る時も **deconstructible** が必要

## 参考

- https://docs.djangoproject.com/ja/1.9/howto/custom-model-fields/#field-deconstruction
- https://docs.djangoproject.com/ja/1.9/topics/migrations/#custom-deconstruct-method
- https://docs.djangoproject.com/ja/1.9/howto/custom-file-storage/