見出し画像

Django_ContentTypeモデルとdumpdata, flush, loaddata, 自然キー #364

Djangoの少し深い機能についてメモしておきます。

ContentTypeモデルとは

django.contrib.contenttypes.models.pyに定義されているContentTypeモデルは、アプリケーション内の各モデルに関連するメタデータを格納します。具体的には、アプリケーション名とモデル名の情報が含まれます。

テーブル名はdjango_content_typeで、マイグレーション時やプログラムでモデルを動的に生成した場合に自動的に追加されていきます。


簡単な例を使ってContentTypeモデルのデータの持ち方とアプリケーションのモデルとの関係を説明します。

例えば、以下のようなBlogアプリケーションがあるとします。

# blog/models.py
from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

このアプリケーションにはAuthorPostという2つのモデルがあり、Djangoはこれらのモデルに対応するContentTypeオブジェクトを自動的に作成します。これらのContentTypeオブジェクトは以下のようなデータ構造を持っています。

django_content_typeテーブルの例

このテーブルのデータを活用するシーンは以下があげられます。

・権限管理の際に特定のモデルに対するアクセス権限を持つユーザーを制御
・ジェネリックリレーションで異なるモデルに対する参照を一つのフィールドで実現

ただ、このテーブルは主にDjangoの内部処理で利用されるものであるため、開発者が意識することは少ないです。

ContentTypeモデルが関わるシーン

このモデルを意識せざるを得ないシーンとして、アプリケーションのテストやデータ移行で、fixtureファイルに特定のデータ状態を保存する場合などがあります。

つまり、「ある環境でモデル内のデータを一括でexportして、それを別環境にimportする」といった場合です。データを用意する担当者のローカル環境から、テストコードを書く担当者のローカル環境にデータを移行する場合などが想定されます。

この時、Djangoのコマンド的には以下の順序で実行します。
以下のdumpdataのコマンドはこのままだとimport時にエラーを起こしますが、一旦記載します。

# データを用意する側がexportする
python manage.py dumpdata -o path/to/your/fixtures/directory target_model_1 target_model_2

ちなみに上記でtarget_modelの部分を省略した場合は全てのテーブルがdumpdataされます。

続いて、

# importする側のデータを削除する
python manage.py flush --no-input

--no-inputはユーザーからの入力(確認プロンプト)を求めずに実行を進めるためのオプションです。自動化された環境であれば付けておくと便利です。

最後に、

# importする側でデータをimportする
python manage.py loaddata path/to/target_file/from/fixtures/directory

loaddataはデフォルトでfixturesディレクトリを参照するので、そこから対象ファイルのパスを記載します。fixturesディレクトリ以外に対象ファイルがある場合はそのパスを記述すればOKです。

ただし先述した通りこのままだと上手くいかず、loaddata時に、idの競合によるIntegrityErrorが起きます。

エラーの原因と対処方法

原因

このエラーは「loadしようとしているデータと同じID(主キー)のデータが既に存在するからloadできない」というものです。対象となっているいずれかのテーブルでその事象が起きていることになります。

しかし、手順的には予め「python manage.py flush」で全データを削除したはずです。なぜ「同じIDのデータ存在する」となるのでしょう。

その原因がContentTypeモデルです。

ContentTypeモデルは、マイグレーション時やプログラムでモデルを動的に生成した場合に自動的に追加されていきます。そして「python manage.py flush」で全テーブルがリセットされた時も、このdjango_content_typeテーブルには、Djangoが自動的にデータを挿入するようになっています。

つまり空だと思われたテーブルにデータが存在しており、ID(主キー)が被っているからloadできない、ということですね。

対処方法

ContentTypeのID(主キー)は自動採番で、migartionの順序なども関わってくるらしく、load元とload先を主キーで整合するのはとても難しいです。

そのため、通常は「自然キー」を用いる方法を取ります。

自然キーとは主キーの代わりになれるもので、IDのような数字ではなく、指定したカラムの値で構成されたタプルです。例えばContentTypeであれば以下のタプルが自然キーです。

# ContentTypeの自然キー(アプリ名, モデル名)
(app_label, model)

このように自然キーが定義されているテーブルであれば、自然キーを主キーや外部キー(ForeignKeyやManyToMany)として使うことが可能です。

具体的にはdumpdataをする際に、自然キーを使うオプションを加えます。

# データを用意する側がexportする
python manage.py dumpdata --natural-primary --natural-foreign -o path/to/your/fixtures/directory target_model_1 target_model_2

--natural-primary: このオプションを指定すると、シリアル化時にモデルが自然キーを持っている場合、自然キーが主キーとして使用されます。

--natural-foreign: このオプションを指定すると、シリアル化時に外部キー(ForeignKey)や多対多フィールド(ManyToManyField)が自然キーを持つ関連モデルを参照している場合、自然キーを使用して関連モデルを参照します。

今回のようにContentTypeモデルだけを考えればいい場合は、`--natural-primary`だけでも問題ないと思います。

ちなみに自然キーを使うことでIDの競合を回避しているだけなので、元の主キーに紐づいているデータが消えるわけではないです。つまり主キーと自然キーの両方で、同じデータが共存している状況になります。

ただContenTypeは基本的にDjangoの内部処理用のテーブルなので、ID違いで同じデータが共存していてもあまり問題にはなりません。

自然キーを定義するには

各モデルクラスの中で簡単に定義できます。
例えばAuthorモデルで定義する場合は以下です。

# blog/models.py
from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

    def natural_key(self):
        return (self.name,)

natural_key()でフィールドを指定してreturnすれば、それが自然キーを構成するタプルになります。このタプルが一意となるように注意は必要です。


参考:ContentTypeクラスのソースコード

最後にソースコードを貼っておきます。最下部にnatural_key()が定義されていることを確認できます。

[django.contrib.contenttypes.models.py]

class ContentType(models.Model):
    app_label = models.CharField(max_length=100)
    model = models.CharField(_("python model class name"), max_length=100)
    objects = ContentTypeManager()

    class Meta:
        verbose_name = _("content type")
        verbose_name_plural = _("content types")
        db_table = "django_content_type"
        unique_together = [["app_label", "model"]]

    def __str__(self):
        return self.app_labeled_name

    @property
    def name(self):
        model = self.model_class()
        if not model:
            return self.model
        return str(model._meta.verbose_name)

    @property
    def app_labeled_name(self):
        model = self.model_class()
        if not model:
            return self.model
        return "%s | %s" % (model._meta.app_label, model._meta.verbose_name)

    def model_class(self):
        """Return the model class for this type of content."""
        try:
            return apps.get_model(self.app_label, self.model)
        except LookupError:
            return None

    def get_object_for_this_type(self, **kwargs):
        """
        Return an object of this type for the keyword arguments given.
        Basically, this is a proxy around this object_type's get_object() model
        method. The ObjectNotExist exception, if thrown, will not be caught,
        so code that calls this method should catch it.
        """
        return self.model_class()._base_manager.using(self._state.db).get(**kwargs)

    def get_all_objects_for_this_type(self, **kwargs):
        """
        Return all objects of this type for the keyword arguments given.
        """
        return self.model_class()._base_manager.using(self._state.db).filter(**kwargs)

    def natural_key(self):
        return (self.app_label, self.model)



この記事が気に入ったらサポートをしてみませんか?