utamaro’s blog

誰かの役に立つ情報を発信するブログ

Djangoのプロジェクト構造について

Djangoのプロジェクト構造について紹介します。

一般的な(ドキュメントに書いてあるような)構造とは少し異なります。

まずは全体の構造を紹介します。

./
├── Procfile  <== heroku用
├── README.md
├── app
│   ├── __init__.py
│   └── card
│       ├── __init__.py
│       ├── admin.py
│       ├── apps.py
│       ├── migrations
│       │   ├── 0001_initial.py
│       │   └── __init__.py
│       ├── models.py
│       ├── query.py
│       ├── serializer.py
│       ├── tests.py
│       └── views.py
├── manage.py
├── package-lock.json
├── requirements.txt
├── runtime.txt  <== heroku用
├── DjangoForHeroku
│   ├── __init__.py
│   ├── assets
│   │   └── static
│   │       ├── css
│   │       │   ├── card.css
│   │       │   ├── reset.css
│   │       │   └── top.css
│   │       └── js
│   │           └── card.js
│   ├── django.log
│   ├── settings
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── local.py
│   │   └── product.py
│   ├── templates
│   │   └── top.html
│   ├── urls.py
│   └── wsgi.py
├── staticfiles
│   └── >> 省略
└── utils
    ├── CommonResponse.py
    └── __init__.py

Djangoのアプリケーションでは、↓のように、1階層目にアプリケーションが配置されるのが一般的です。

project:
    ├─ application:
    │    ├─ view.py
    │    └─ model.py
    ├─ application:
    │    └─ view.py
    └─ project:
    │    ├─ urls.py
    │    └─ settings.py
    └─ manage.py

ですが↑のような構造にした場合、applicationの数が多くなると横に広くなってしまうので、見通しが悪くなってしまいます。

そこで、applicationを一つにまとめるappというパッケージを作成して管理することにしました。 ↓の部分です。

├── app
│   ├── __init__.py
│   └── card
│       ├── __init__.py
│       ├── admin.py
│       ├── apps.py
│       ├── migrations
│       │   ├── 0001_initial.py
│       │   └── __init__.py
│       ├── models.py
│       ├── query.py
│       ├── serializer.py
│       ├── tests.py
│       └── views.py

ただし、djangoで用意されているstartappを実行するのが少し面倒です。

↓のように一度appに移動してからコマンドを実行する必要があります。

cd app
../manage.py startapp card

※ 移動せずに実行すると↓のように怒られます。

CommandError: 'app/hoge' is not a valid app name. Please make sure the name is a valid identifier.

また、settings/base.pyには以下のように書いています。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
]

INSTALLED_APPS += list(map(lambda x: 'app.' + x, [
    'card',
]))

いちいちapp.Aとかapp.Bと書くのが美しくないなと思って、lambdaを使ってます。

これはこれでイケてない気がしますが。。

おまけ

お気づきの方がいらっしゃるかと思いますが、startappで作成されるファイル以外も2つ追加しています。 - query.py - serializer.py

それぞれのファイルの役割を説明します。

query.pyについて

読んで時の如く、クエリをまとめたファイルです。

ファイルの中身は↓のようなことを書いています。

def select_card(card_id):
    template = """
    select
        id
    from
        card
    where
        id = {{card_id}}
    """
    data = {
        'card_id': card_id,
    }
    return template, data

↑の例だとORMを書いたほうが簡単なのですが、joinやwhereが複雑だった場合にはクエリを書いたほうが良いと考えています。

また、createやinsert、deleteなどもORMを使ったほうが簡単と感じました。

ちなみに、クエリの生成ですが、jinjasqlを使っています。

jinjasqlを使った場合は、↓のように書きます。

j = JinjaSql()
template, data = select_card(card_id)
query, bind_params = j.prepare_query(template, data)
cards = Card.objects.raw(query, bind_params)
return list(cards)

複雑なクエリをORMを使って書いて、生成されたクエリを確認して、思ってたクエリと違ってて、悩んで、ググって、ヒットしなくて、詰まるよりは↑のほうが簡単です(実体験)

serializer.py

このファイルはdjangorestframeworkように作りました。

中はこのようになっています。(例)

class DataUpdateSerializer(serializers.Serializer):
    id = serializers.IntegerField(required=True)
    uuid = serializers.UUIDField(required=True)
    title = serializers.CharField(max_length=125, required=True, allow_blank=False)
    content = serializers.CharField(allow_blank=True)

少しトリッキーな書き方を思いついたので、ついでに紹介します。

リアライザを使ってリクエストパラメータのバリデーションをしようとしたときがありました。

デフォルト値をハードコーディングすると、後々の修正が大変になるので、enumで管理したいと考えました。 (条件とデフォルト値を同時にもつフィールドが欲しかった)

というわけで、↓のようなシリアライザを作りました。

class DataListSerializer(serializers.Serializer):
    class SortEnum(Enum):
        SORT_DIRECTION = (r"^(asc|desc)?$", 'asc')

        def __new__(cls, _pattern, _default):
            obj = object.__new__(cls)
            obj.pattern = re.compile(_pattern)
            obj.default = _default
            return obj

    sort_direction = serializers.RegexField(
        regex=SortEnum.SORT_DIRECTION.pattern, 
        default=SortEnum.SORT_DIRECTION.default
    )

他のシリアライザでも使う場合、SortEnumを外に出しても良いと思います。

ただし、複数のシリアライザで共有している場合、修正時の影響が大きくなるので気をつけなければなりません。

まとめ

Djangoの構造は修正しすぎるとうまく動かなかったり、問題が起きたときにググっても解決方法が出てこなくなったりします。

今回紹介したレベルでの修正が開発しやすいのではないでしょうか。