Djangoとyamdlにより、fixtureを使わずにYAMLとモデルを紐付ける

これは JSL(日本システム技研) Advent Calendar 2020 - Qiita の12/21分の記事です。

ちょっとしたDjangoアプリを作る中で、

  • モデルのデータソースはYAMLにしたい
    • YAMLはマスタ的存在
    • モデル間のリレーションは存在しない
  • YAMLを書き換えた場合、Djangoアプリを再起動するだけで、データが反映されるようにしたい
    • migrateやfixtureのようなコマンドを使いたくない
  • YAMLに対して、QuerySetの抽出系メソッドを使いたい

ということがありました。

DBの代わりに、YAMLをモデルと紐付けられないかを調べましたが、標準ではそれらしいものがありませんでした。

そこでライブラリがないかを探したところ、 yamdl がありました。
andrewgodwin/yamdl: ORM-queryable YAML fixtures for Django

2017/8以降にコミットがないものの、試してみたところ希望通りの動作だったため、メモを残します。

 
目次

 

環境

 

モデルのデータソースとするYAML

りんごとその種子親・花粉親がまとまっているYAMLがあり、ここからデータをQuerySet経由でデータを抽出したいとします。

- name: 'シナノゴールド'
  seed: 'ゴールデンデリシャス'
  pollen: '千秋'

- name: 'フジ'
  seed: '国光'
  pollen: 'デリシャス'

- name: 'シナノゴールド'
  seed: 'ゴールデンデリシャス'
  pollen: '千秋'

- name: '秋映'
  seed: '千秋'
  pollen: 'ツガル'

- name: '王林'
  seed: 'ゴールデンデリシャス'
  pollen: '印度'

 

Djangoアプリの作成

Djangoプロジェクトの作成

いつも通り作成します。

$ django-admin startproject config .
$ python manage.py startapp myapp

 

モデル

上記のYAMLファイルに対応した項目を持つモデルを作成します。

yamdlのREADMEに従い、モデルの中に __yamdl__ = True と指定することで、YAMLをデータソースとしたモデルであると明示します。

また、いつロードされたのかを把握するため、 created_at で登録日時を保持しておきます。

from django.db import models
from django.utils import timezone


class Apple(models.Model):
    name = models.CharField('名前', max_length=20)
    seed = models.CharField('種子親', max_length=20)
    pollen = models.CharField('花粉親', max_length=20)

    created_at = models.DateTimeField(default=timezone.now)

    # yamdl用の設定を追加
    __yamdl__ = True

 

settings.py

いくつか追加します。

INSTALLED_APPS
INSTALLED_APPS = [
    # 自分のDjangoアプリ
    'myapp.apps.MyappConfig',
    ...
    # yamdl用
    'yamdl',
]

 

DATABASES

今回はSQLiteファイル自体を生成しないようにするため、defaultで設定してあるSQLiteをインメモリへと変更します。

また、yamdl用のエントリも追加します。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': ':memory:',
    },
    'yamdl': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': 'file:yamdl-db?mode=memory&cache=shared',
    }
}

 

YAMDL_DIRECTORIES

データソースとなるYAMLファイルの置き場を指定します。

今回はBASE_DIRの下に content ディレクトリを指定し、その中にYAMLファイルを置くことにします。

YAMDL_DIRECTORIES = [
    BASE_DIR / 'content',
]

 

DATABASE_ROUTERS

こちらはREADMEそのままです。

DATABASE_ROUTERS = [
    "yamdl.router.YamdlRouter",
]

 

View

今回は、全件取得してJSONを返すようにします。

日本語をそのまま表示するために ensure_ascii を使います。

また、モデルの項目 created_at がDateTime型なので、シリアライズ可能にするために cls も指定しておきます。
https://docs.djangoproject.com/ja/3.1/topics/serialization/#serialization-formats-json

from django.core.serializers.json import DjangoJSONEncoder
from django.http import HttpResponse
from django.views import View
from myapp.models import Apple

class AppleView(View):
    def get(self, request, *args, **kwargs):
        apples = Apple.objects.all().values()

        data_json = json.dumps(list(apples), ensure_ascii=False, cls=DjangoJSONEncoder)
        return HttpResponse(data_json, content_type='application/json')

 

urls.py

よくある形です。

config/urls.py

urlpatterns = [
    path('', include('myapp.urls')),
]

 
myapp/urls.py

urlpatterns = [
    path('', AppleView.as_view()),
]

 

YAMLファイル

settings.pyの YAMDL_DIRECTORIES で指定した通り、 manage.py と同じ階層に content ディレクトリを作ります。

その下に、 <Djangoアプリ名>.<モデル名>ディレクトリを作成します。今回は myapp.Apple となります。

さらにその下に、データソースであるYAMLファイル (今回はapple.yaml) を置きます。

全体としてはこんな感じになります。

$ tree -I env
.
...
├── content
│   └── myapp.Apple
│       └── apple.yaml
├── manage.py
...

 

マイグレーションファイルの作成

今回はマイグレーションファイルのみ作成し、マイグレーションは行いません。

$ python manage.py makemigrations

 

ローカルでの動作確認

runserver後に localhost:8000 を確認すると、YAMLの内容がJSONレスポンスとして表示されました。

[
    {
        "id": 1,
        "name": "シナノゴールド",
        "seed": "ゴールデンデリシャス",
        "pollen": "千秋",
        "created_at": "2020-12-17T13:08:27.095Z"
    },
    {
        "id": 2,
        "name": "フジ",
        "seed": "国光",
        "pollen": "デリシャス",
        "created_at": "2020-12-17T13:08:27.098Z"
    },
    {
        "id": 3,
        "name": "シナノゴールド",
        "seed": "ゴールデンデリシャス",
        "pollen": "千秋",
        "created_at": "2020-12-17T13:08:27.098Z"
    },
    {
        "id": 4,
        "name": "秋映",
        "seed": "千秋",
        "pollen": "ツガル",
        "created_at": "2020-12-17T13:08:27.099Z"
    },
    {
        "id": 5,
        "name": "王林",
        "seed": "ゴールデンデリシャス",
        "pollen": "印度",
        "created_at": "2020-12-17T13:08:27.099Z"
    }
]

 

Herokuでの動作確認

ローカルで動作したものがHerokuでも動作するようであれば、読込専用データを表示するためだけにHeroku Postgresを使わなくても良さそうです。

そのため、Herokuでも動作確認をしてみました。

 
Herokuアプリとしてのセットアップは以下を参考に行いました。
DjangoアプリをHerokuにデプロイする方法 - Qiita

主な内容は以下の通りです。

  • ファイルの追加
    • .gitignore
    • requirements.txt
    • Procfile
    • runtime.txt
  • 設定変更
    • settings.pyで ALLOWED_HOSTS = ['*']
  • Herokuの設定変更
    • 今回はcollectstaticを使わないため、 heroku config:set DISABLE_COLLECTSTATIC=1 を実行

 

デプロイ後、時間をおいて確認してみたところ、 created_at 以外は同じデータが表示されました。想定通りでした。

初回

[
    {
        "id": 1,
        "name": "シナノゴールド",
        "seed": "ゴールデンデリシャス",
        "pollen": "千秋",
        "created_at": "2020-12-17T14:53:44.772Z"
    },
    {
        "id": 2,
        "name": "フジ",
        "seed": "国光",
        "pollen": "デリシャス",
        "created_at": "2020-12-17T14:53:44.812Z"
    },
    {
        "id": 3,
        "name": "シナノゴールド",
        "seed": "ゴールデンデリシャス",
        "pollen": "千秋",
        "created_at": "2020-12-17T14:53:44.812Z"
    },
    {
        "id": 4,
        "name": "秋映",
        "seed": "千秋",
        "pollen": "ツガル",
        "created_at": "2020-12-17T14:53:44.813Z"
    },
    {
        "id": 5,
        "name": "王林",
        "seed": "ゴールデンデリシャス",
        "pollen": "印度",
        "created_at": "2020-12-17T14:53:44.814Z"
    }
]

 

時間をおいた後

[
    {
        "id": 1,
        "name": "シナノゴールド",
        "seed": "ゴールデンデリシャス",
        "pollen": "千秋",
        "created_at": "2020-12-21T23:16:06.099Z"
    },
    {
        "id": 2,
        "name": "フジ",
        "seed": "国光",
        "pollen": "デリシャス",
        "created_at": "2020-12-21T23:16:06.123Z"
    },
    {
        "id": 3,
        "name": "シナノゴールド",
        "seed": "ゴールデンデリシャス",
        "pollen": "千秋",
        "created_at": "2020-12-21T23:16:06.124Z"
    },
    {
        "id": 4,
        "name": "秋映",
        "seed": "千秋",
        "pollen": "ツガル",
        "created_at": "2020-12-21T23:16:06.125Z"
    },
    {
        "id": 5,
        "name": "王林",
        "seed": "ゴールデンデリシャス",
        "pollen": "印度",
        "created_at": "2020-12-21T23:16:06.125Z"
    }
]

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi-sandbox/django_yamdl-sample