SE(たぶん)の雑感記

一応SEやっている筆者の思ったことを書き連ねます。会計学もやってたので、両方を生かした記事を書きたいと考えています。 でもテーマが定まってない感がすごい。

Djangoで複数アプリ構築時、事前に考慮したほうが良い部分

この記事は、Qiita Advent Calendar 20189日目の記事になります。16日目とはなりますが、投稿がなかったため、代わりに投稿しました。

qiita.com


Djangoを使って、Webアプリを開発しております。

当初から同時に二つのアプリケーション(同じ場所で使うが、機能がだいぶ異なるので分かれている)を構築することとなっており、一つ作った後にもう一つ、というときにいろいろ躓いたため、どこで困ったか書いてみます。

Djangoは、本番環境で動かす際どうするかについて書いてある記事がそれほど多くない印象です。その一助になれば幸いです。

主にファイル管理関連です。

バージョン等

名称 バージョン
Python 3.7.0
Django 2.1.1

サンプル

f:id:hiroronn:20181216142735p:plain
サンプルの構成

プロジェクトとしてdjango_complex_sampleというものを作り、その中にapp1app2というアプリケーションを作っています。

事前準備

settings.pyINSTALLED_APPSapp1app2を追加します。DBは特に使わないですが、SQLiteにしておきます。また、URLも指定しておきます。

  • settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app1',
    'app2',
]
  • urls.py
from django.contrib import admin
from django.urls import path
from app1 import views as v1
from app2 import views as v2

urlpatterns = [
    path('admin/', admin.site.urls),
    path('app1/', v1.HomeView.as_view(), name="app1home"),
    path('app2/', v2.HomeView.as_view(), name="app2home"),
]

テンプレート

結論

標準のまま使うなら、

  • /templates/アプリ名というフォルダを作り、そこにファイルを格納する
  • ビューからテンプレートを参照する場合、アプリ名/ファイル名と記述する

根拠

テンプレート | Django documentation | Django

テンプレートは、viewでパス指定されて、ページを描画する際に使われます。

標準のまま使う場合、各サイトにtemplatesというフォルダを作って、そこにHTMLを配置すると、サイト表示時にそのテンプレートを表示します。

ただ、このテンプレートの決定方法は、フォルダを探索して最初に見つかったファイルを使用するという動作をしています。自動的にアプリ別に判定、とはなっていません。

よって、確実にファイルを判別するために、templates直下にアプリ名のフォルダを作り、そこを参照するようにしたほうが良いです。

例として、app2templates直下にhome.htmlを配置します。app1の同フォルダは空にします。そして、app1のビューでテンプレート名home.htmlを指定します。

f:id:hiroronn:20181216155702p:plain
app2のテンプレート

  • app1/views.py
from django.shortcuts import render
from django.views.generic import TemplateView

# Create your views here.

class HomeView(TemplateView):
    template_name = 'home.html' # app1のhome.htmlを期待しているが、まだ作っていない
    def get(self, request, *args, **kwargs):
        return render(request, self.template_name)
  • app1/home.html
<h1>Hello App1!</h1>
  • app2/home.html
<h1>Hello App2!</h1>

f:id:hiroronn:20181216160708p:plain
app1を参照

上記のように、app2home.htmlを参照して、表示してしまいます。これは、templatesの検索で、app2にあるhome.htmlが見つかるためです。

これを避けるために、全てのアプリで以下のようにフォルダを切り、

f:id:hiroronn:20181216161232p:plain
app2のフォルダ構成

  • app1/views.py
from django.shortcuts import render
from django.views.generic import TemplateView

# Create your views here.

class HomeView(TemplateView):
    template_name = 'app1/home.html' # templates/app1フォルダを参照
    def get(self, request, *args, **kwargs):
        return render(request, self.template_name)

フォルダを固定で指定できるようにしましょう*1

静的ファイル(static)

結論

  • 各アプリケーション直下に/static/アプリ名というフォルダを作り、そこにファイルを格納する
  • テンプレートでファイルを参照する場合、アプリ名/ファイル名と記述する

根拠

静的ファイル (画像、JavaScript、CSS など) を管理する | Django documentation | Django

静的ファイルとは、JavaScriptや画像など、ページに埋め込む形で使うファイルを指します。開発時は、各アプリ直下にstaticというフォルダを作り、そこにファイルを格納します。

このファイル、本番環境では一か所に集めて管理します。collectstatic*2コマンドで、静的ファイルを集められます。

試しに、staticフォルダに一つだけファイル(main.css)を置いて、collectstaticを実行します。

f:id:hiroronn:20181214084625p:plain
collectstatic後

すると、staticの直下に配置されてしまいます。これだと、複数のサイトで同じファイル名のファイルがあると、お互い上書きされます*3

なので、各アプリのstatic直下に、アプリ名のフォルダを作成して、ファイルが衝突しないようにしましょう。

テンプレートで参照する場合も、

  • app1/home.html
{% load static %}
<script type="text/javascript" src="{% static 'app1/main.css' %}"></script>

のように、パスにアプリ名を付ける必要があるため、開発着手時から対応しておいたほうが良いです。

static内にアプリ名のフォルダを作って、collectstaticを実施すると、以下のようになります。

f:id:hiroronn:20181216164145p:plain
アプリ分割後collectstatic

URL(urls.py)

結論

  • アプリごとに、urls.pyを作成する
  • 各アプリのurls.pyで、一意のapp_nameを定義する
  • プロジェクトのurls.pyで、各アプリの定義をincludeする
  • テンプレートでアプリ名を指定する

根拠

URL ディスパッチャ | Django documentation | Django

現状、プロジェクトにあるurls.pyは、以下のようになっています。

  • django_complex_sample/urls.py
from django.contrib import admin
from django.urls import path
from app1 import views as v1
from app2 import views as v2

urlpatterns = [
    path('admin/', admin.site.urls),
    path('app1/', v1.HomeView.as_view(), name="app1home"),
    path('app2/', v2.HomeView.as_view(), name="app2home"),
]

アプリでURLが増えた場合に、ここに定義を増やして対応することは可能です。

しかし、そうすると、

  • URLを組み替えるたびにプロジェクトの修正が発生
  • name命名規則がわかりづらい*4
  • プロジェクト側でアプリのURLをすべて管理するのは大変

というような問題が発生します。

こういうときのためにDjangoでは、includeという機能が用意されています。

まず、各アプリの直下(どこでもよい)に、urls.pyを作成します。

そして、自身のアプリに対するURLを記述します。

  • app1/urls.py
"""app1のURL定義"""
from django.urls import path
from .apps import App1Config as config
from . import views as v

app_name = config.name # app1が入る
urlpatterns = [
    path('', v.HomeView.as_view(), name='home'),
]

そして、プロジェクトのurls.pyでそれをincludeします。

    • django_complex_sample/urls.py
from django.contrib import admin
from django.urls import path, include
from app1 import urls as v1
from app2 import views as v2

urlpatterns = [
    path('admin/', admin.site.urls),
    path('app1/', include(v1)), # 変更(app1のURLを追加)
    path('app2/', v2.HomeView.as_view(), name="app2home"),
]

こうすると、自動的にapp1のURLへのルーティングを追加してくれます。

また、この機能を使った場合、viewでのURL指定方法が変わります。

まず、app1に適当なビューを追加します。

  • app1/views.py
from django.shortcuts import render
from django.views import View
from django.views.generic import TemplateView
from django.http import HttpResponse

# Create your views here.

class HomeView(TemplateView):
    template_name = 'app1/home.html'
    def get(self, request, *args, **kwargs):
        return render(request, self.template_name)

class ApplicationView(View):
    def get(self, request, *args, **kwargs):
        return HttpResponse('GET request!')
  • app1/urls.py
"""app1のURL定義"""
from django.urls import path
from .apps import App1Config as config
from . import views as v

app_name = config.name # app1が入る
urlpatterns = [
    path('', v.HomeView.as_view(), name='home'),
    path('app/', v.ApplicationView.as_view(), name='app'), # 追加
]

このViewに対し、home.htmlからリンクを作成します。(いろいろ加筆しています)

  • app1/templates/home.html
{% load static %}
<!DOCTYPE html>
<html lang="ja">
  <head>
    <link href="{% static 'app1/main.css' %}" rel="stylesheet">
  </head>
  <body>
    <h1>Hello App1!</h1>
    <a href={% url 'app1:app' %}>リンク</a>
  </body>

重要なのは

<a href={% url 'app1:app' %}>リンク</a>

の部分です。urlというDjangoのタグで、指定した名前のURLを設定してくれますが、{% url '[アプリ名]:[name]' %}hrefで指定すると、アプリ名を判定してそのリンクを作ってくれます。

これにより、アプリ間の名前競合を避けられます。

おわりに

まだまだ、複数アプリ開発での注意点はあるように思います。

現状、筆者が体験した中での話となっています。

原則としては、アプリの処理はアプリ内で完結させることと、アプリ外で扱いやすいような設計にしておくことの二つだと感じています。

理想は、pipでインストールしたときに簡単に扱えるようにしておくことです。

ほかにも気づいたことあったら、ちょっとずつ整理していきます。

*1:ここの指定は、settings.pyのTEMPLATESを使って柔軟にできるので、実現方法はいろいろある

*2:python manage.py collectstatic。settings.pyにSTATIC_ROOTという設定が必要

*3:上書きかどうかは不明だが、片方しか配置されない

*4:nameは、テンプレートからURLを参照する際に指定できる