django-import-export

インポート・エクスポートできるだけじゃない

〜カスタマイズと使いこなし〜

Takanori Suzuki

BPStyle 180 / 2026 Jan 8

はなすこと kiriri

  • なんのために? hate

  • django-import-exportの基本 work

  • django-import-exportの使いこなし work-moeru

なんのために? hate

明治図書のマナビリア案件で必要 naruhodo

教材データとは benkyou

  • 以下のようなデータがDBに入っている

  • 学習教材の目次構成

  • 問題文

  • 解答形式

  • 正解

教材データのコピーが必要 hueta

  • 作成中の教材は間違いがある

  • レビューして修正

  • レビューOKな教材のみが本番にある

  • 別環境で作成した教材データを
    本番環境にコピーする運用 yoshi

既存の教材データコピー tako

  • 原稿作成環境(Edit)から本番環境(Production)にExcelファイルで教材データをコピー

        architecture-beta
    service stg(logos:aws-ecs)[Edit]
    service excel(vscode-icons:file-type-excel)[Excel]
    service prd(logos:aws-ecs)[Production]

    stg:R --> L:excel
    excel:R --> L:prd
    

運用できていたわけですが… good

マナビリアCBTが爆誕!! [1] paaan

マナビリアCBT

新しい教材データコピーが必要 ha

  • 教材データの構成は似ているが微妙に違う

  • Excelで実装すると工数が…テストも…

django-import-exportで省力化に挑戦 totugeki

django-import-exportの基本 work

django-import-exportとは hate

インストールと有効化 [2]

pip install django-import-export
settings.pyで有効化
INSTALLED_APPS = (
    ...
    'import_export',
)

リソースクラスを作成 [3]

  • リソースクラスで対象となるモデルを指定

app/admin.py
from import_export import resources
from core.models import Book

class BookResource(resources.ModelResource):
    class Meta:
        model = Book

Django Adminに適用 [4]

  • 親クラスを変更

  • リソースクラスを指定

app/admin.py
from django.contrib import admin
from .models import Book
from import_export.admin import ImportExportModelAdmin

@admin.register(Book)
class BookAdmin(ImportExportModelAdmin):
    resource_classes = [BookResource]

Django Admin画面で
インポートエクスポートできる! yatta

対象フォーマットは以下

  • csv, xlsx, tsv, json, yaml, html

しかし、このままでは教材データの
コピーには使えないwork-yabai

django-import-exportの
使いこなし work-moeru

対象フィールドの指定

  • 作成日時、更新日時等は不要

        erDiagram
    Work["Work(教材)"] {
        int id PK
        string code "コード"
        int textbook_id FK "教科書ID"
        int subject_id FK "教科ID"
        string name "教材名"
        datetime created_at "作成日時"
        datetime updated_at "更新日時"
        int updated_by FK "更新者"
        str updated_by_name "更新者名"
    }
    

対象フィールドの指定

  • リソースで対象フィールドを指定 [5]

class BookResource(resources.ModelResource):
    class Meta:
        model = Book
        # 対象フィールドを指定
        fields = ("id", "name", "price")
class BookResource(resources.ModelResource):
    class Meta:
        model = Book
        # 対象外フィールドを指定
        exclude = ("created_at", "updated_at")

対象フィールドの指定

  • 今回はexcludeを選択

  • フィールドが増えて自動で対応

class WorkResource(resources.ModelResource):
    class Meta:
        model = Work
        exclude = ("id", "created_at", "updated_at",
                   "updated_by")

主キーの変更

  • デフォルトではidを使って追加、更新する

  • 環境が異なるためidがずれる

        erDiagram
    Work["Work(教材)"] {
        int id PK
        string code "コード"
        int textbook_id FK "教科書ID"
        int subject_id FK "教科ID"
        string name "教材名"
        datetime created_at "作成日時"
        datetime updated_at "更新日時"
        int updated_by FK "更新者"
        str updated_by_name "更新者名"
    }
    

主キーの変更

  • データを一意に識別するフィールドを指定 [6]

  • codeというユニークな文字列を使用

class WorkResource(resources.ModelResource):
    class Meta:
        model = Work
        import_id_fields = ("code",)  # codeで一意に識別

大量データ対応 [7]

  • 教材の問題は大量全件インポート

  • 同じ値の場合は処理を飛ばす

  • 飛ばしたデータは確認画面で表示しない

class WorkResource(resources.ModelResource):
    class Meta:
        model = Work
        skip_unchanged = True  # 処理を飛ばす
        report_skipped = False  # 表示しない

外部キーの維持

  • 教材の目次構造は外部キーで実現

        erDiagram
    direction LR
    Work ||--|{ Unit : has
    Unit ||--|{ Question : has
    Work["Work(教材)"] {
        int id PK
        string code
        string name
    }
    Unit["Unit(ユニット)"] {
        int id PK
        int work_id FK "教材ID"
        string code
        string title
    }
    Question["Qustion(問題)"] {
        int id PK
        int unit_id FK "ユニットID"
        string code
        string text
    }
    

外部キーの維持

  • idは環境ごとに異なる

  • 任意のフィールド(code)を外部キー代わりに使用 [8]

class UnitResource(resources.ModelResource):
    class Meta:
        model = Unit

    work = fields.Field(
        column_name="work",
        attribute="work",
        widget=ForeignKeyWidget(Work, field="code"))

外部キーの維持(複数カラム

  • 教材は任意の教科書に紐付く

        erDiagram
    direction LR
    Subject ||--|{ Textbook : has
    Textbook ||--|{ Work : has
    Subject["Subject(教科)"] {
        int id PK
        string name "国語等"
    }
    Textbook["Textbook(教科書)"] {
        int id PK
        int subject_id FK "教科ID"
        int edition "発行年"
        int year "学年"
		int publisher "出版社"
    }
    Work["Work(教材)"] {
        int id PK
		int textbook_id FK "教科書ID"
        string name
    }
    

外部キーの維持(複数カラム

  • 教科書は複数カラムで一意になる

        erDiagram
    direction LR
    Textbook["Textbook(教科書)"] {
        int id PK
        int subject_id FK "教科ID"
        int edition "発行年"
        int year "学年"
		int publisher "出版社"
    }
    

外部キーの維持(複数カラム

  • 外部キーの維持にnatural keyを使用[9]

  • 複数の情報がJSONにシリアライズされる

class TextbookManager(models.Manager):
    def get_by_natural_key(self, edition, subject_name, year, publisher):
	    """natural keyを使用してデータを取得するメソッド"""
        return self.get(
            edition=edition,
            subject__name=subject_name,
            year=year,
            publisher=publisher,
        )

外部キーの維持(複数カラム

  • モデルにnatural_keyメソッド追加

  • objectsTextbookManager()を指定

class Textbook(BaseModel):
    """教科書モデル"""
    objects = TextbookManager()

    def natural_key(self):
        return (
            self.edition, self.subject.name,
            self.year, self.publisher,
        )

外部キーの維持(複数カラム

  • ForeignKeyWidgetでnatural keyを使用

class WorkResource(resources.ModelResource):
    textbook = fields.Field(
        column_name="textbook",
        attribute="textbook",
        widget=ForeignKeyWidget(Textbook,
            use_natural_foreign_keys=True)
    )

任意のデータをエクスポート対象に

  • 全件エクスポートだとレビュー中の教材データも本番に入る

  • レビュー完了したデータのみをエクスポート対象にしたい

任意のデータをエクスポート対象に

  • can_exrpotがTrueのデータをエクスポート

  • 上位がFalseならエクスポートしない

        erDiagram
    direction LR
    Work ||--|{ Unit : has
    Unit ||--|{ Question : has
    Work["Work(教材)"] {
        int id PK
        string code
        bool can_export "フラグ"
    }
    Unit["Unit(ユニット)"] {
        int id PK
        int work_id FK "教材ID"
        string code
        bool can_export "フラグ"
    }
    Question["Question(問題)"] {
        int id PK
        int unit_id FK "ユニットID"
        string code
        string text "問題文"
    }
    

任意のデータをエクスポート対象に

  • get_export_queryset()で絞り込み [10]

class WorkAdmin(ImportExportModelAdmin):
    def get_export_queryset(self, request):
        return Work.objects.filter(can_export=True)

任意のデータをエクスポート対象に

class UnitAdmin(ImportExportModelAdmin):
    def get_export_queryset(self, request):
        # UnitとWorkの両方のcan_exportがTrue
        return Unit.objects.select_related("work").filter(
            can_export=True,
            work__can_export=True,
        )

class QuestionAdmin(ImportExportModelAdmin):
    def get_export_queryset(self, request):
        return Question.objects.select_related("unit__work").filter(
            unit__can_export=True,
            unit__work__can_export=True
        )

インポート時のデータ削除

  • 原稿作成環境で削除されたデータを本番環境でも自動で削除したい

  • →モデルに削除フラグを追加

class Question(BaseModel):
    """問題のモデルクラス"""
    ...
    is_delete = models.BooleanField("削除フラグ", default=False)
    ...

インポート時のデータ削除

  • リソースにfor_delete()メソッドを追加 [11]

class QuestionResource(resources.ModelResource):
    """Question(問題)のインポート・エクスポート用の設定"""

    def for_delete(self, row, instance):
        """is_deleteがTrueのデータを削除する"""
        return row["is_delete"] == "1"

    ...

まとめ juutai

  • 単純な処理以外にいろいろできる

  • 対象フィールドの指定

  • 主キーの変更

  • 大量データ対応

  • 外部キーの維持(複数カラムも)

  • 任意のデータをエクスポート対象に

  • インポート時のデータ削除

他にもいろいろできるので
インポート・エクスポート
必要なときは思い出してkyapi

django-import-export.readthedocs.io